#!/usr/bin/env php
<?php

require $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php';

use function Drewlabs\CodeGenerator\Proxy\PHPClassMethod;
use function Drewlabs\CodeGenerator\Proxy\PHPClassProperty;
use function Drewlabs\CodeGenerator\Proxy\PHPFunctionParameter;
use function Drewlabs\CodeGenerator\Proxy\PHPInterface;
use function Drewlabs\CodeGenerator\Proxy\PHPTrait;
use Drewlabs\CodeGenerator\Contracts\Blueprint;
use Drewlabs\CodeGenerator\Contracts\CallableInterface;
use Drewlabs\CodeGenerator\Contracts\ImplementableStruct;
use Drewlabs\CodeGenerator\Contracts\TraitableStruct;
use Drewlabs\CodeGenerator\Contracts\OOPStructInterface;
use Drewlabs\CodeGenerator\Contracts\ValueContainer;
use Drewlabs\CodeGenerator\Models\PHPClass;
use Drewlabs\CodeGenerator\Models\PHPClassMethod as ClassMethod;

// #region Defaults
const MDL_DEFAULT_HEADER = <<<EOT
/*
 * This file is auto generated using the drewlabs/mdl UML model class generator package
 *
 * (c) Sidoine Azandrew <contact@liksoft.tg>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
*/

EOT;
// #endregion Defaults
/**
 * Throws a runtime exception if any of the values provided in the definition fails
 * to passe validation
 * 
 * @param array $rules 
 * @param array $definition 
 * @param array $validators 
 * @return true 
 * @throws RuntimeException 
 */
function md_validate_definition(array $rules, array $definition, array $validators = [])
{
    $basic_validators = array_merge([
        'required' => function (array $values, $key) {
            return isset($values[$key]) && null !== $values[$key];
        },
        'array' => function (array $values, $key) {
            return array_key_exists($key, $values) && is_array($values[$key]);
        },
        'integer' => function (array $values, $key) {
            return array_key_exists($key, $values) && is_int($values[$key]);
        },
        'numeric' => function (array $values, $key) {
            return array_key_exists($key, $values) && is_numeric($values[$key]);
        },
        'string' => function (array $values, $key) {
            return array_key_exists($key, $values) && is_string($values[$key]);
        }
    ], $validators ?? []);
    $errors = [];
    foreach ($rules as $key => $value) {
        foreach ($value as $rule) {
            if (!array_key_exists($rule, $basic_validators)) {
                continue;
            }
            if (false === $basic_validators[$rule]($definition, $key)) {
                $errors[] = "Validation rule ($rule) fails for $key";
            }
        }
    }
    return $errors;
}

// #region Command line utilities
/**
 * Resolve command options from command arguments
 * 
 * @param array $args 
 * @return array<array-key, mixed> 
 */
function mdl_cmd_options(array $args)
{
    $options = array_filter($args, function ($arg) {
        return substr($arg, 0, 1) === '-' || substr($arg, 0, 2) === '--';
    });

    $fn = function (array $opts) {
        foreach ($opts as $value) {
            $option = str_replace('-', '', $value);
            if (false !== ($pos = strpos($option, '='))) {
                yield substr($option, 0, $pos) => substr($option, $pos);
            } else if (false !== ($pos = strpos($option, ' '))) {
                yield substr($option, 0, $pos) => substr($option, $pos);
            } else {
                yield trim($option) => true;
            }
        }
    };

    return iterator_to_array($fn($options));
}

/**
 * Resolve command arguments from command parameters
 * 
 * @param array $args 
 * @return array 
 */
function mdl_cmd_arguments(array $args)
{
    return array_values(array_filter($args, function ($arg) {
        return substr($arg, 0, 1) !== '-' && substr($arg, 0, 2) !== '--';
    }));
}

// #endregion Command line utilities

// #region IO
/**
 * Recursively create directory if the later does not exists
 * 
 * @param string $dirname 
 * @return void 
 */
function mdl_create_directory_if_not_exists(string $dirname)
{
    // Create the path directory if not exists
    if (!is_dir($dirname)) {
        mkdir($dirname, 0777, true);
    }
}

function mdl_resolve_parent_directory_path(string $path, string $base = __DIR__)
{
    $substr = substr($path, 0, 3);
    // Handle relative path
    if (('../' === $substr) || ('..\\' === $substr)) {
        $directory = $base;
        $_substr = substr($path, 0, 3);
        while (('../' === $_substr) || ('..\\' === $_substr)) {
            $directory = dirname($directory);
            $path = substr($path, 3);
            $_substr = substr($path, 0, 3);
        }
        $path = $directory . DIRECTORY_SEPARATOR . $path;
    }

    return $path;
}

function mdl_resolve_relative_path($path, string $base = __DIR__)
{
    $substr = substr($path, 0, 2);
    // Handle relative path
    if (('./' === $substr) || ('.\\' === $substr)) {
        $path = $base . DIRECTORY_SEPARATOR . substr($path, 2);
    }
    return $path;
}

function mdl_resolve_path(string $path, string $base = __DIR__)
{
    $substr = substr($path, 0, 3);
    $path = ('../' === $substr) || ('..\\' === $substr) ? mdl_resolve_parent_directory_path($path, $base) : (($subsustr = substr($substr, 0, 2)) && (('./' === $subsustr) || ('.\\' === $subsustr)) ? mdl_resolve_relative_path($path, $base) : $path);
    // If the path does not starts with '/' we append the current 
    if ('/' !== substr($path, 0, 1)) {
        $path = $base . DIRECTORY_SEPARATOR . $path;
    }
    return $path;
}

/**
 * Creates a file writer instance
 * 
 * @param string $path 
 * @param string $mode 
 * @param bool $include_path 
 * @param resource $context 
 * @return object
 * @throws RuntimeException 
 */
function mdl_io_create_writer(string $path, $mode = 'r', $include_path = false, $context = null)
{
    try {
        $descriptor = @fopen($path, $mode, $include_path, $context);
        clearstatcache(true, $path);
        if (false === $descriptor) {
            throw new \RuntimeException(sprintf('Error opening stream at path: %s. %s', $path, error_get_last()['message'] ?? ''));
        }
        return new class($descriptor)
        {
            /**
             * @var int|resource
             */
            private $descriptor;

            /**
             * Creates a read writer instance.
             *
             * @param mixed $descriptor
             *
             * @return void
             */
            public function __construct($descriptor)
            {
                $this->descriptor = $descriptor;
            }

            public function __destruct()
            {
                $this->close();
            }

            public function write(string $data, ?int $length = null, $operation = \LOCK_EX | \LOCK_NB)
            {
                // Case the read writer is not a resource, we simply return false
                if (!is_resource($this->descriptor)) {
                    return false;
                }
                $bytes = false;
                if ($this->descriptor && @flock($this->descriptor, $operation)) {
                    $bytes = @fwrite($this->descriptor, $data, $length);
                    @flock($this->descriptor, \LOCK_UN);
                }
                return $bytes;
            }

            public function close()
            {
                if (null !== $this->descriptor && is_resource($this->descriptor)) {
                    fclose($this->descriptor);
                    $this->descriptor = null;
                }
            }
        };
    } catch (\Throwable $e) {
        throw new \RuntimeException("Error while opening file: " . $e->getMessage());
    }
}
// #region IO

// #region Type utilities
/**
 * Returns value as array
 * 
 * @param string|string[] $value 
 * @return mixed[] 
 */
function mdl_array_wrap($value)
{
    return is_array($value) ? $value : [$value];
}

/**
 * Create a unique list of values from the source value
 * 
 * @param \iterable|array $iterable 
 * @param string|null $key 
 * @return array 
 */
function mdl_array_unique($iterable, string $key = null)
{
    if (null === $key && is_array($iterable)) {
        return array_unique($iterable);
    }
    // # Returns the output
    return iterator_to_array(mdl_unique_iterable($iterable, $key));
}

/**
 * Creates an iterable from duplicated key traversable
 * 
 * @param \iterable $iterable 
 * @param string|null $key 
 * @return Generator<int, mixed, mixed, void> 
 */
function mdl_unique_iterable($iterable, string $key = null)
{
    $keys = [];
    foreach ($iterable as $k => $value) {
        # code...
        // Case the searched key does not exist, we simply add it to the output
        if (!isset($value[$key])) {
            yield $k => $value;
            continue;
        }
        if (array_key_exists($value[$key], $keys)) {
            continue;
        }
        $keys[$value[$key]] = true;
        yield $k => $value;
    }
}

/**
 * Returns a camel case version of the haystack string
 * 
 * @param string $haystack 
 * @param bool $capitalize 
 * 
 * @return string 
 */
function mdl_str_camelize(string $haystack, bool $capitalize = true)
{
    $search_replace = static function (string $h, string $d) {
        return str_replace($d, '', ucwords($h, $d));
    };
    $first_capital = static function ($param) use ($capitalize) {
        return !$capitalize ? lcfirst($param) : $param;
    };
    return $first_capital($search_replace($haystack, '_'));
}
// #region Type utilities

// #region MDL components functions
function mdl_create_function_parameter(array $definition)
{
    // Validate the functiion parameter if required
    $parameter = PHPFunctionParameter(preg_replace("/\s+/", "", $definition['name']), isset($definition['type']) ? str_replace("\\\\", "\\", $definition['type']) : null, $definition['default'] ?? null);

    if (isset($definition['optional']) && (true === boolval($definition['optional']))) {
        $parameter = $parameter->asOptional();
    }

    if (isset($definition['variadic']) && (true === boolval($definition['variadic']))) {
        $parameter = $parameter->asVariadic();
    }

    if (isset($definition['reference']) && (true === boolval($definition['reference']))) {
        $parameter = $parameter->asReference();
    }
    return $parameter;
}

function mdl_create_property(array $definition)
{
    if (is_null($name = $definition['name'] ?? null)) {
        printf("Error: Invalid property definition %s, name property is required\n\n", json_encode($definition));
        die();
    }
    $property = PHPClassProperty(
        preg_replace("/\s+/", "", $name),
        isset($definition['type']) ? str_replace("\\\\", "\\", $definition['type']) : null,
        // Class property as defined as private by default
        $definition['modifier'] ?? 'private',
        $definition['default'] ?? null,
        $definition['comment'] ?? null
    );
    if (isset($definition['constant']) && (true === boolval($definition['constant']))) {
        $property = $property->asConstant();
    }
    return $property;
}

function mdl_create_method(array $definition)
{
    if (is_null($name = $definition['name'] ?? null)) {
        printf("Error: Invalid method definition %s, name property is required\n\n", json_encode($definition));
        die();
    }
    /**
     * @var ClassMethod
     */
    $method = PHPClassMethod(preg_replace("/\s+/", "", $name), [], isset($definition['returns']) ? str_replace("\\\\", "\\", $definition['returns']) : null, $definition['modifier'] ?? 'public', $definition['comment'] ?? null);
    if (isset($definition['throws'])) {
        /**
         * @var ClassMethod
         */
        $method = $method->throws(array_map(function ($item) {
            return str_replace("\\\\", "\\", $item);
        }, mdl_array_wrap($definition['throws'])));
    }

    if (isset($definition['parameters'])) {
        foreach (mdl_array_wrap($definition['parameters']) as $value) {
            /**
             * @var ClassMethod
             */
            $method = $method->addParameter(mdl_create_function_parameter($value));
        }
    }
    if (isset($definition['static']) && (true === boolval($definition['static']))) {
        /**
         * @var ClassMethod $method
         */
        $method = $method->asStatic(true);
    }
    if (isset($definition['lines']) && ($lines = mdl_array_wrap($definition['lines']))) {
        foreach ($lines as $line) {
            $method = $method->addLine($line);
        }
    }
    return $method;
}

/**
 * Creates a PHP trait or mixin instance
 * 
 * @param array $definition 
 * @param bool $addSetters 
 * @param bool $addGetters 
 * @return TraitableStruct 
 */
function mdl_create_mixin(array $definition, bool $addSetters, bool $addGetters)
{
    if (is_null($name = $definition['name'] ?? null)) {
        printf("Error: Invalid mixin definition %s, name property is required\n\n", json_encode($definition));
        die();
    }

    $addSetters = boolval($definition['setter'] ?? $definition['setters'] ?? $addSetters);
    $addGetters = boolval($definition['getter'] ?? $definition['getters'] ?? $addSetters);
    $setters = [];
    $getters = [];
    $properties = mdl_array_wrap($definition['properties'] ?? []);
    $methods = mdl_array_wrap($definition['methods'] ?? []);
    $immutable = boolval($definition['immutable'] ?? false);

    $mixin = PHPTrait(
        preg_replace("/\s+/", "", $name),
        array_map(function ($item) {
            return mdl_create_method($item);
        }, $methods),
        array_map(function ($item)  use (&$getters, &$setters, $addSetters, $addGetters, $immutable) {
            $property = mdl_create_property($item);
            // # Create setters if required
            if ($addSetters && (false === boolval($item['readonly'] ?? false))) {
                $setters[] = mdl_create_property_setter($property, $item['type'] ?? null, $immutable || boolval($item['immutable'] ?? false));
            }
            // # Create getters if required
            if ($addGetters) {
                $getters[] = mdl_create_property_getter($property, $item['type'] ?? null);
            }
            return $property;
        }, $properties)
    );
    foreach (array_merge($setters, $getters) as $value) {
        /**
         * @var TraitableStruct
         */
        $mixin = $mixin->addMethod($value);
    }
    if (isset($definition['mixins'])) {
        foreach (mdl_array_wrap($definition['mixins']) as $value) {
            $mixin = $mixin->addTrait($value);
        }
    }
    return $mixin;
}

/**
 * Creates an interface
 * 
 * @param array $definition 
 * @return ImplementableStruct 
 */
function mdl_create_interface(array $definition)
{
    if (is_null($name = $definition['name'] ?? null)) {
        printf("Error: Invalid interface definition %s, name property is required\n\n", json_encode($definition));
        die();
    }

    $interface = PHPInterface(
        preg_replace("/\s+/", "", $name),
        array_map(function ($item) {
            return mdl_create_method($item);
        }, mdl_array_wrap($definition['methods'] ?? []))
    );
    if (isset($definition['extends'])) {
        foreach (mdl_array_wrap($definition['extends']) as $value) {
            $interface = $interface->setBaseInterface($value);
        }
    }
    return $interface;
}

/**
 * Property setter factory function
 * 
 * @param ValueContainer $property 
 * @param string|null $type 
 * @return CallableInterface 
 */
function mdl_create_property_setter(ValueContainer $property, string $type = null, bool $immutable = false)
{
    $type = str_replace("\\\\", "\\", $type);
    $methodPrefix = $immutable ? 'with' : 'set';
    $method = PHPClassMethod($methodPrefix . mdl_str_camelize($name = $property->getName()), [PHPFunctionParameter('value', $type)], 'self', 'public', "Set $name property value");
    return $immutable ? $method->addLine("\$self = clone \$this")->addLine("\$self->" . $name . " = \$value")->addLine("return \$self") : $method->addLine("\$this->" . $name . " = \$value")->addLine("return \$this");
}

/**
 * Property getter factory function
 * 
 * @param ValueContainer $property 
 * @param string|null $type 
 * @return CallableInterface 
 */
function mdl_create_property_getter(ValueContainer $property, string $type = null)
{
    $type = str_replace("\\\\", "\\", $type);
    return PHPClassMethod('get' . mdl_str_camelize($name = $property->getName()), [], $type, 'public', "Get $name property value")->addLine("return \$this->" . $name);
}

/**
 * Creates a class constructor instance
 * 
 * @param ValueContainer[] $properties 
 * @return CallableInterface 
 */
function mdl_create_class_constructor(array $properties)
{
    $required = array_map(function ($item) {
        return mdl_create_function_parameter([
            'name' => $item['name'],
            'type' => $item['type'] ?? null,
            'optional' => false
        ]);
    }, array_filter($properties, function ($item) {
        return !isset($item['optional']) || (isset($item['optional']) && boolval($item['optional']) === false);
    }));
    $optional = array_map(function ($item) {
        return mdl_create_function_parameter([
            'name' => $item['name'],
            'type' => $item['type'] ?? null,
            'optional' => true,
            'default' => $item['default'] ?? null
        ]);
    }, array_filter($properties, function ($item) {
        return boolval($item['optional'] ?? false) && boolval($item['init'] ?? false);
    }));
    $parameters = [...$required, ...$optional];
    $constructor =  PHPClassMethod('__construct', $parameters, 'void', 'public', 'Creates new class instance');
    foreach ($parameters as $parameter) {
        $constructor = $constructor->addLine("\$this->" . $parameter->name() . ' = ' . "\$" . $parameter->name());
    }
    return $constructor;
}

/**
 * Check if class definition provides a custom constructor definition
 * @param array $methods 
 * @return bool 
 */
function mdl_has_constructor_method(array $methods)
{
    foreach ($methods as $value) {
        if (!is_array($value)) {
            continue;
        }
        if (isset($value['name']) && (strtolower(strval($value['name'])) === '__construct')) {
            return true;
        }
    }
    return false;
}

/**
 * Creates a class definition object
 * 
 * @param array $definition 
 * @param bool $addSetters 
 * @param bool $addGetters 
 * @param bool $useReflection
 * @return Blueprint 
 */
function mdl_create_class(array $definition, bool $addSetters, bool $addGetters, bool $force, bool $useReflection = false)
{
    if (is_null($name = $definition['name'] ?? null)) {
        printf("Error: Invalid class definition %s, name property is required\n\n", json_encode($definition));
        die();
    }
    $addSetters = boolval($definition['setter'] ?? $definition['setters'] ?? $addSetters);
    $addGetters = boolval($definition['getter'] ?? $definition['getters'] ?? $addSetters);
    $useReflection = boolval($definition['reflection'] ?? $useReflection);
    $setters = [];
    $getters = [];
    $properties = mdl_array_wrap($definition['properties'] ?? []);
    $methods = mdl_array_wrap($definition['methods'] ?? []);
    $immutable = boolval($definition['immutable'] ?? false);
    $noConstructor = !boolval($definition['constructor'] ?? false) && !mdl_has_constructor_method($methods);
    $jsonnable = boolval($definition['json'] ?? $definition['jsonnable'] ?? false);

    if ($jsonnable) {
        if (!$useReflection && !$noConstructor && !$force) {
            printf("Warning: disabling reflection usage for jsonnable class %s having a constructor, can lead to error while generating %s::fromJson() source code, please review your configuration or pass --force flag to bypass this warning \n\n", $name, $name);
            die();
        }
        //#region Case using reflection we add a constant __JSON__PROPERTIES__ property to the class definition
        if ($useReflection) {
            $jsonProperties = (function (array $props) {
                foreach ($props as $prop) {
                    if (!isset($prop['name'])) {
                        continue;
                    }
                    yield $prop['name'] => $prop['json_property'] ?? $prop['json'] ?? $prop['name'];
                }
            })($properties ?? []);
            $properties[] = [
                'name' => '__JSON__PROPERTIES__',
                'constant' => true,
                'modifier' => 'public',
                'type' => 'string[]',
                'comment' => 'List of properties that can be converted to json',
                'default' => iterator_to_array($jsonProperties),
                'optional' => true,
                'init' => false,
                'readonly' => true,
                'getter' => false
            ];
        }
        //#endregion Case using reflection we add a constant __JSON__PROPERTIES__ property to the class definition

        //#region Add toJson and fromJson method to the class if class is jsonnable
        $toJsonLines = (function (array $props) {
            yield "return [";
            foreach ($props as $prop) {
                if (!isset($prop['name']) || (true === boolval($prop['constant'] ?? false))) {
                    continue;
                }
                yield sprintf("\t'%s' => %s,", $prop['json_property'] ?? $prop['json'] ?? $prop['name'], sprintf("\$this->%s", $prop['name']));
            }
            yield "]";
        })($properties ?? []);

        $fromJsonLines = (function (array $props) use ($useReflection) {
            // Should be reconsidered in futur release as 
            if ($useReflection) {
                yield "\$reflector = new \ReflectionClass(__CLASS__)";
                yield "\$self = \$reflector->newInstanceWithoutConstructor()";
            } else {
                yield "\$self = new static";
            }
            foreach ($props as $prop) {
                if (!isset($prop['name'])) {
                    continue;
                }
                $jsonPropName = $prop['json_property'] ?? $prop['json'] ?? $prop['name'];
                yield sprintf("\$self->%s = \$json['%s'] ?? null", $prop['name'], $jsonPropName);
            }
            yield "return \$self";
        })($properties ?? []);
        $methods[] = [
            'name' => 'toJson',
            'comment' => 'Returns a dictionnary/hash map representation of the current instance',
            'returns' => 'array|mixed',
            'lines' => iterator_to_array($toJsonLines)
        ];

        $methods[] = [
            'name' => 'fromJson',
            'static' => true,
            'comment' => 'Initialize class instance and properties from a dictionnary/hash map',
            'return' => 'static',
            'parameters' => [
                [
                    'name' => 'json',
                    'type' => 'array',
                    'default' => []
                ]
            ],
            'lines' => $useReflection ? [
                "\$reflector = new \ReflectionClass(__CLASS__)",
                "\$self = \$reflector->newInstanceWithoutConstructor()",
                "foreach(static::__JSON__PROPERTIES__ as \$key => \$value) {",
                "\tif (is_null(\$propValue = \$json[\$value] ?? null)) {",
                "\t\tcontinue",
                "\t}",
                "\t\$self->{\$key} = \$json[\$value] ?? null",
                "\t\$property = \$reflector->getProperty(\$key)",
                "\t\$property->setAccessible(true)",
                "\t\$property->setValue(\$self, \$propValue)",
                "}",
                "return \$self"
            ] : iterator_to_array($fromJsonLines)
        ];
        //#region Add toJson and fromJson method to the class if class is jsonnable
    }

    /** @var BluePrint */
    $blueprint = new PHPClass(
        preg_replace("/\s+/", "", $name),
        mdl_array_wrap($definition['implements'] ?? []),
        array_map(function ($item) {
            return mdl_create_method($item);
        }, $methods),
        array_map(function ($item) use (&$getters, &$setters, $addSetters, $addGetters, $immutable) {
            $property = mdl_create_property($item);
            // # Create setters if required
            if ($addSetters && (false === boolval($item['readonly'] ?? false))) {
                $setters[] = mdl_create_property_setter($property, $item['type'] ?? null, $immutable || boolval($item['immutable'] ?? false));
            }
            // # Create getters if required
            if ($addGetters && (false !== boolval($item['get'] ?? $item['getter'] ?? true))) {
                $getters[] = mdl_create_property_getter($property, $item['type'] ?? null);
            }
            return $property;
        },  $properties)
    );

    foreach (array_merge($setters, $getters) as $value) {
        /**
         * @var BluePrint
         */
        $blueprint = $blueprint->addMethod($value);
    }
    if (isset($definition['mixins'])) {
        foreach (mdl_array_wrap($definition['mixins']) as $value) {
            # code...
            /**
             * @var BluePrint
             */
            $blueprint = $blueprint->addTrait($value);
        }
    }

    if (isset($definition['extends'])) {
        $blueprint = $blueprint->setBaseClass($definition['extends']);
    }

    if (isset($definition['abstract']) && (boolval($definition['abstract']) === true)) {
        $blueprint = $blueprint->asAbstract();
    }

    if (isset($definition['final'])) {
        $blueprint = $blueprint->asFinal();
    }

    // TODO : Add constructor
    if (isset($definition['constructor']) && (boolval($definition['constructor']) === true) && !mdl_has_constructor_method($methods)) {
        $blueprint = $blueprint->addMethod(mdl_create_class_constructor(mdl_array_unique(array_merge($properties, $definition['mixin_properties'] ?? []), 'name')));
    }
    return $blueprint;
}
// #endregion MDL components functions

// #region MDL miscellanous functions
function mdl_create_sub_namespace(string $base, string $directory)
{
    return rtrim($base, "\\") . "\\" . implode("\\", explode('/', $directory ?? ''));
}

function mdl_create_write_directory(string $base, string $directory)
{
    return rtrim($base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, explode('/', $directory ?? ''));
}

/**
 * Create/Compile component source code
 * 
 * @param OOPStructInterface|ImplementableStruct $component 
 * @param string $path 
 * @param string $namespace 
 * @param bool $strict 
 * @param string|null $headers 
 * @param string|null $directory 
 * @return string[] 
 */
function mdl_create_component_source($component, string $path, string $namespace, string $directory = null, bool $strict = true, string $headers = null)
{
    return [
        'path' => ($directory ? mdl_create_write_directory($path, $directory) : $path) . DIRECTORY_SEPARATOR . $component->getName(),
        'source' => mdl_compile_source_code(
            $component->addToNamespace($directory ? mdl_create_sub_namespace($namespace, $directory) : $namespace)->__toString(),
            $strict,
            $headers
        )
    ];
}

/**
 * Compile source code
 * 
 * @param string $source 
 * @param bool $strict 
 * @param string|null $headers 
 * @return string 
 */
function mdl_compile_source_code(string $source, bool $strict = true, string $headers = null)
{
    // #region Build source code
    // Insert PHP script start
    $parts[] = '<?php';
    $parts[] = '';
    // Add strict type definition if required
    if ($strict) {
        $parts[] = 'declare(strict_types=1);';
        $parts[] = '';
    }
    // Add File headers
    $parts[] = null !== $headers ? implode(\PHP_EOL, mdl_array_wrap($headers)) : MDL_DEFAULT_HEADER;
    // Convert the stringeable content to string
    $parts[] = $source;
    // #endregion build source code
    return implode(\PHP_EOL, $parts);
}

/**
 * Write individual component to disk
 * 
 * @param mixed $component 
 * @param bool $strict
 * @return void 
 * @throws RuntimeException 
 */
function mdl_write_component_to_disk($component)
{
    // TODO: Validate component structure
    $path = mdl_resolve_path($component['path'] ?? './');
    if ('.php' !== substr($path, strlen($path) - 4)) {
        $path = $path . '.php';
    }
    // TODO : Create directory if not exits
    mdl_create_directory_if_not_exists(dirname($path));
    mdl_io_create_writer($path, 'w')->write($component['source']);
}

function mdl_fix_class_import_paths(string $namespace, array $blueprint, array $blueprints, array $mixins, array $interfaces, $extends = null)
{
    if ($extends) {
        $result = mdl_find_namespace_component($blueprints, $extends);
        $extends = (false !== $result) && isset($result['directory']) ? (mdl_create_sub_namespace($namespace, $result['directory']) . "\\" . $result['name']) : (false === strpos($extends, "\\") ? $namespace . "\\" . $extends : $extends);
        $blueprint['extends'] = str_replace("\\\\", "\\", $extends);
    }
    // Fix namespace for interfaces
    if (isset($blueprint['implements'])) {
        $blueprint['implements'] = array_map(function ($implement) use ($namespace, $interfaces) {
            $result = mdl_find_namespace_component($interfaces, $implement);
            $implement = (false !== $result) && isset($result['directory']) ? (mdl_create_sub_namespace($namespace, $result['directory']) . "\\" . $result['name']) : (false === strpos($implement, "\\") ? $namespace . "\\" . $implement : $implement);
            return str_replace("\\\\", "\\", $implement);
        }, mdl_array_wrap($blueprint['implements'] ?? []));
    }
    // #endregion Fix namespace for interfaces

    // Fix namespace for mixins
    if (isset($blueprint['mixins'])) {
        $blueprint['mixins'] = array_map(function ($mixin) use ($namespace, $mixins) {
            $result = mdl_find_namespace_component($mixins, $mixin);
            $mixin = (false !== $result) && isset($result['directory']) ? (mdl_create_sub_namespace($namespace, $result['directory']) . "\\" . $result['name']) : (false === strpos($mixin, "\\") ? $namespace . "\\" . $mixin : $mixin);
            return str_replace("\\\\", "\\", $mixin);
        }, mdl_array_wrap($blueprint['mixins'] ?? []));
    }
    // #endregion Fix namespace for mixins

    return $blueprint;
}

/**
 * Fix mixin import namespace path
 * 
 * @param string $namespace 
 * @param array $blueprint 
 * @return array 
 */
function mdl_fix_trait_import_paths(string $namespace, array $blueprint, array $mixins)
{
    // Fix namespace for mixins
    if (isset($blueprint['mixins'])) {
        $blueprint['mixins'] = array_map(function ($mixin) use ($namespace, $mixins) {
            $result = mdl_find_namespace_component($mixins, $mixin);
            $mixin = (false !== $result) && isset($result['directory']) ? (mdl_create_sub_namespace($namespace, $result['directory']) . "\\" . $result['name']) : (false === strpos($mixin, "\\") ? $namespace . "\\" . $mixin : $mixin);
            return str_replace("\\\\", "\\", $mixin);
        }, $blueprint['mixins'] ?? []);
    }
    // #endregion Fix namespace for mixins
    return $blueprint;
}

/**
 * Fix interface import namespace path
 * 
 * @param string $namespace 
 * @param array $blueprint 
 * @return array 
 */
function mdl_fix_interface_import_paths(string $namespace, array $blueprint, array $interfaces)
{
    // Fix namespace for mixins
    if (isset($blueprint['extends'])) {
        $blueprint['extends'] = array_map(function ($iterator) use ($namespace, $interfaces) {
            $result = mdl_find_namespace_component($interfaces, $iterator);
            $extends = (false !== $result) && isset($result['directory']) ? (mdl_create_sub_namespace($namespace, $result['directory']) . "\\" . $result['name']) : (false === strpos($iterator, "\\") ? $namespace . "\\" . $iterator : $iterator);
            return str_replace("\\\\", "\\", $extends);
        }, mdl_array_wrap($blueprint['extends'] ?? []));
    }
    // #endregion Fix namespace for mixins
    return $blueprint;
}

/**
 * Return the component class path or FALSE if missing
 * 
 * @param array $values 
 * @param mixed $name 
 * @return string|false 
 */
function mdl_find_namespace_component(array $values, $name)
{
    foreach ($values as $value) {
        $directory = $value['directory'] ?? null;
        $local_name = isset($value['name']) ? preg_replace("/\s+/", "", $value['name']) : null;
        if (null === $local_name) {
            continue;
        }
        if (trim($directory ? mdl_create_sub_namespace('', $directory) . "\\" . $local_name : $local_name, '\\') === trim($name, '\\')) {
            return $value;
        }
    }
    return false;
}

/**
 * Add directory to component definition if missing
 * 
 * @param array $definition 
 * @param string $directory
 * 
 * @return array 
 */
function mdl_set_component_directory(array $definition, string $directory)
{
    if (!isset($definition['directory'])) {
        $definition['directory'] = $directory;
    }
    return $definition;
}

function mdl_resolve_interface_methods($iterable, array $interface_definitions, array $excludes, &$methods)
{
    foreach (mdl_array_wrap($iterable) as $value) {
        if ((substr($value, 0, 1) !== "\\") && (false !== ($match = mdl_find_namespace_component($interface_definitions, $value)))) {
            if (isset($match['extends'])) {
                mdl_resolve_interface_methods($match['extends'] ?? [], $interface_definitions, $excludes, $methods);
            }
            // #region Filter out method that already are defined in mixin classes
            $actual_methods = array_filter($match['methods'] ?? [], function ($current) use ($excludes) {
                return is_array($current) && isset($current['name']) && !in_array($current['name'], $excludes);
            });
            // #endregion Filter out method that already are defined in mixin classes
            $methods = array_merge($methods ?? [], $actual_methods);
        }
    }
}

/**
 * Resolve base classes implementations
 * 
 * @param string|mixed $extends 
 * @param array $blueprint_definitions 
 * @param array $interfaces 
 * @return void 
 */
function mdl_resolve_base_class_implementations($extends, $blueprint_definitions, &$interfaces)
{
    $result = mdl_find_namespace_component($blueprint_definitions, $extends);
    if (isset($result['implements'])) {
        $interfaces = array_merge($result['implements'] ?? [], $interfaces ?? []);
    }
    if (isset($result['extends'])) {
        mdl_resolve_base_class_implementations($result['extends'], $blueprint_definitions, $interfaces);
    }
}

/**
 * Fixes duplicate members in component definitions
 * 
 * @param array $definition 
 * @return array 
 */
function mdl_component_fix_duplicate_members(array $definition)
{
    if (isset($definition['methods'])) {
        $definition['methods'] = mdl_array_unique(mdl_array_wrap($definition['methods']), 'name');
    }
    if (isset($definition['properties'])) {
        $definition['properties'] = mdl_array_unique(mdl_array_wrap($definition['properties']), 'name');
    }
    return $definition;
}

/**
 * Find mixin methods
 * 
 * @param mixed $mixin 
 * @param mixed $mixin_definitions 
 * @param mixed $methods 
 * @return void 
 */
function mdl_find_mixing_methods($mixin, $mixin_definitions, &$methods)
{
    $result = mdl_find_namespace_component($mixin_definitions, $mixin);
    if (isset($result['mixins'])) {
        foreach (mdl_array_wrap($result['mixins']) as $value) {
            if (!is_string($value)) {
                continue;
            }
            mdl_find_mixing_methods($value, $mixin_definitions, $methods);
        }
    }
    $methods = array_merge($methods ?? [], array_map(function ($method) {
        return $method['name'];
    }, array_filter(mdl_array_wrap($result['methods'] ?? []), function ($item) {
        return is_array($item) && isset($item['name']);
    })));
}

/**
 * Find mixin properties
 * 
 * @param mixed $mixin 
 * @param mixed $mixin_definitions 
 * @param mixed $properties 
 * @return void 
 */
function mdl_find_mixing_properties($mixin, $mixin_definitions, &$properties)
{
    $result = mdl_find_namespace_component($mixin_definitions, $mixin);
    if (isset($result['mixins'])) {
        foreach (mdl_array_wrap($result['mixins']) as $value) {
            if (!is_string($value)) {
                continue;
            }
            mdl_find_mixing_properties($value, $mixin_definitions, $properties);
        }
    }
    $filter = array_filter(mdl_array_wrap($result['properties'] ?? []), function ($item) {
        return is_array($item) && isset($item['name']);
    });
    $properties = array_merge($properties ?? [], $filter);
}
// #region MDL miscellanous functions

// #region MDL namespace
/**
 * 
 * @param array $definition 
 * @param bool $strict 
 * @return void 
 * @throws RuntimeException 
 */
function mdl_create_namespace(array $definition, bool $strict = true, bool $setters = false, bool $getters = false)
{
    $useReflection = mdl_attribute_is_truthy($definition, 'reflection');
    $force = mdl_attribute_is_truthy($definition, 'force');
    $path = $definition['path'] ?? './src';
    $namespace = preg_replace("/\s+/", "", str_replace("\\\\", "\\", $definition['name'] ?? 'App'));
    // TODO : Add interface method to class if interface is in namespace
    $blueprint_definitions = isset($definition['directories']['classes']) ? array_map(function ($class_definition) use ($definition) {
        return mdl_set_component_directory($class_definition, $definition['directories']['classes']);
    }, mdl_array_wrap($definition['classes'] ?? [])) : mdl_array_wrap($definition['classes'] ?? []);
    $interface_definitions = isset($definition['directories']['interfaces']) ? array_map(function ($interface) use ($definition) {
        return mdl_set_component_directory($interface, $definition['directories']['interfaces']);
    }, mdl_array_wrap($definition['interfaces'] ?? [])) : mdl_array_wrap($definition['interfaces'] ?? []);
    $mixin_definitions = isset($definition['directories']['mixins']) ? array_map(function ($mixins) use ($definition) {
        return mdl_set_component_directory($mixins, $definition['directories']['mixins']);
    }, mdl_array_wrap($definition['mixins'] ?? [])) : mdl_array_wrap($definition['mixins'] ?? []);
    $blueprint_definitions = array_map(function ($blueprint) use ($interface_definitions, $blueprint_definitions, $mixin_definitions, $namespace) {
        // #region Rewrite extended class path
        $extends = isset($blueprint['extends']) ? (is_array($blueprint['extends']) ? array_values($blueprint['extends'])[0] : $blueprint['extends']) : null;
        // #endregion Rewrite extended class path
        if ($blueprint['abstract'] ?? false) {
            return mdl_fix_class_import_paths(
                $namespace,
                $blueprint,
                $blueprint_definitions,
                $mixin_definitions,
                $interface_definitions,
                $extends
            );
        }
        $methods = [];
        $mixin_methods = [];
        $mixin_properties = [];
        // #region Find mixin methods that might be excluded from class inherited method definitions
        if (isset($blueprint['mixins'])) {
            foreach ($blueprint['mixins'] as $mixin_name) {
                if (!is_string($mixin_name)) {
                    continue;
                }
                mdl_find_mixing_methods($mixin_name, $mixin_definitions, $mixin_methods);
                mdl_find_mixing_properties($mixin_name, $mixin_definitions, $mixin_properties);
            }
        }
        // #endregion Find mixin methods that might be excluded from class inherited method definitions

        // #region Merge the extended class implementation with the current class implementations
        $iterable = mdl_array_wrap($blueprint['implements'] ?? []);
        if ($extends) {
            mdl_resolve_base_class_implementations($extends, $blueprint_definitions, $iterable);
        }
        // #endregion Merge the extended class implementation with the current class implementations
        mdl_resolve_interface_methods($iterable, $interface_definitions, $mixin_methods, $methods);
        $blueprint['methods'] = array_merge($blueprint['methods'] ?? [], $methods);
        $blueprint['mixin_properties'] = $mixin_properties;
        return mdl_fix_class_import_paths(
            $namespace,
            $blueprint,
            $blueprint_definitions,
            $mixin_definitions,
            $interface_definitions,
            $extends
        );
    }, $blueprint_definitions);

    $mixins = $mixin_definitions ? array_map(function ($mixin) use ($path, $namespace, $strict, $setters, $getters) {
        // TODO : Validate the component
        return mdl_create_component_source(
            mdl_create_mixin(mdl_component_fix_duplicate_members($mixin), $setters, $getters),
            $path,
            $namespace,
            $mixin['directory'] ?? null,
            $strict
        );
    }, array_map(function ($mixin) use ($namespace, $mixin_definitions) {
        return mdl_fix_trait_import_paths($namespace, $mixin, $mixin_definitions);
    }, $mixin_definitions)) : [];

    printf("\nCompiling component, please wait...\n");

    $implementations = $interface_definitions ? array_map(function ($interface) use ($path, $namespace, $strict) {
        // TODO : Validate the component
        return mdl_create_component_source(
            mdl_create_interface(mdl_component_fix_duplicate_members($interface)),
            $path,
            $namespace,
            $interface['directory'] ?? null,
            $strict
        );
    }, array_map(function ($current) use ($namespace, $interface_definitions) {
        return mdl_fix_interface_import_paths($namespace, $current, $interface_definitions);
    }, $interface_definitions)) : [];

    $classes = $blueprint_definitions ? array_map(function ($class) use ($path, $namespace, $strict, $setters, $getters, $useReflection, $force) {
        // TODO : Validate the component
        return mdl_create_component_source(
            mdl_create_class(mdl_component_fix_duplicate_members($class), $setters, $getters, $force, $useReflection),
            $path,
            $namespace,
            $class['directory'] ?? null,
            $strict
        );
    }, $blueprint_definitions) : [];

    // Merged the generated components
    $components = array_merge($mixins, $implementations, $classes);

    printf("\nWriting component, to disk...");

    // # Write compiled component to disk
    foreach ($components as $value) {
        mdl_write_component_to_disk($value);
        printf(".");
    }
    printf("\n");
}
// #region MDL namespace


/**
 * Check if option value resolve to true
 * 
 * @param array $options 
 * @param string $name 
 * @return bool 
 */
function mdl_attribute_is_truthy(array $options, string $name)
{
    return isset($options[$name]) && (boolval($options[$name]) === true);
}

/**
 * Main program
 * 
 * @param array $args 
 * @return void 
 * @throws Error 
 * @throws RuntimeException 
 */
function main(array $args = [])
{
    printf("Program: Namespace source code generator\n");

    // #region Load command line arguments and options
    $arguments = mdl_cmd_arguments($args);
    $options = mdl_cmd_options($args);
    // #endregion Load command line arguments and options

    $path = $arguments[0] ?? null;
    if (null === $path) {
        throw new Error('Program expect an argument which is the path to the namspace configuration file');
    }
    // Resolve the realpath to the path specified by the program user
    $path = @mdl_resolve_path($path, __DIR__);

    $definition = mdl_attribute_is_truthy($options, 'json') ? @json_decode(file_get_contents($path), true) : \yaml_parse(file_get_contents($path));
    $definition['reflection'] = $definition['reflection'] ?? (mdl_attribute_is_truthy($options, 'reflection') || mdl_attribute_is_truthy($options, 'reflect'));
    $definition['force'] = $definition['force'] ?? mdl_attribute_is_truthy($options, 'force');

    // Set the memory limit for the current script execution
    ini_set('memory_limit', '-1');
    set_time_limit(0);

    // TODO : Execute the generator process
    mdl_create_namespace($definition, mdl_attribute_is_truthy($options, 'strict'), mdl_attribute_is_truthy($options, 'set') || mdl_attribute_is_truthy($options, 'setters'), mdl_attribute_is_truthy($options, 'get') || mdl_attribute_is_truthy($options, 'getters'));

    printf("\nProcess completed successfully. Thanks for using our service!\n");
}

main(array_slice($argv, 1));
