#!/usr/bin/env php
<?php
use OpenApi\Logger;
use OpenApi\Analysis;
use const OpenApi\UNDEFINED;
use const OpenApi\COLOR_RED;
use const OpenApi\COLOR_YELLOW;
use const OpenApi\COLOR_STOP;

error_reporting(E_ALL);
// Possible options and their default values.

$options = [
    'output' => false,
    'format' => 'auto',
    'exclude' => [],
    'bootstrap' => false,
    'help' => false,
    'debug' => false,
    'processor' => [],
];
$aliases = [
    'o' => 'output',
    'e' => 'exclude',
    'b' => 'bootstrap',
    'h' => 'help',
    'd' => 'debug',
    'p' => 'processor',
    'f' => 'format'
];
$needsArgument = [
    'output',
    'format',
    'exclude',
    'bootstrap',
    'processor',
];
$paths = [];
$error = false;
define('OpenApi\COLOR_RED', "\033[31m");
define('OpenApi\COLOR_YELLOW', "\033[33m");
define('OpenApi\COLOR_STOP', "\033[0m");

try {
    // Parse cli arguments
    for ($i = 1; $i < $argc; $i++) {
        $arg = $argv[$i];
        if (substr($arg, 0, 2) === '--') { // longopt
            $option = substr($arg, 2);
        } elseif ($arg[0] === '-') { // shortopt
            if (array_key_exists(substr($arg, 1), $aliases)) {
                $option = $aliases[$arg[1]];
            } else {
                throw new Exception('Unknown option: "' . $arg . '"');
            }
        } else {
            $paths[] = $arg;
            continue;
        }
        if (array_key_exists($option, $options) === false) {
            throw new Exception('Unknown option: "' . $arg . '"');
        }
        if (in_array($option, $needsArgument)) {
            if (empty($argv[$i + 1]) || $argv[$i + 1][0] === '-') {
                throw new Exception('Missing argument for "' . $arg . '"');
            }
            if (is_array($options[$option])) {
                $options[$option][] = $argv[$i + 1];
            } else {
                $options[$option] = $argv[$i + 1];
            }
            $i++;
        } else {
            $options[$option] = true;
        }
    }
} catch (Exception $e) {
    $error = $e->getMessage();
}

if (!$error && $options['bootstrap']) {
    if (is_readable($options['bootstrap']) === false) {
        $error = 'Invalid `--bootstrap` value: "'.$options['bootstrap'].'"';
    } else {
        require_once($options['bootstrap']);
    }
}
if (count($paths) === 0) {
    $error = 'Specify at least one path.';
}
if ($options['help'] === false && $error) {
    error_log('');
    error_log(COLOR_RED.'Error: '.$error.COLOR_STOP);
    $options['help'] = true; // Show help
}
if ($options['help']) {
    $help = <<<EOF

Usage: openapi [--option value] [/path/to/project ...]

Options:
  --output (-o)     Path to store the generated documentation.
                    ex: --output openapi.yaml
  --exclude (-e)    Exclude path(s).
                    ex: --exclude vendor,library/Zend
  --bootstrap (-b)  Bootstrap a php file for defining constants, etc.
                    ex: --bootstrap config/constants.php
  --processor       Register an additional processor.
  --format          Force yaml or json.
  --debug           Show additional error information.
  --help (-h)       Display this help message.


EOF;
    error_log($help);
    exit(1);
}

if (class_exists(Logger::class) === false) {
    if (file_exists(__DIR__.'/../vendor/autoload.php')) {  // cloned / dev environment?
        require_once(__DIR__.'/../vendor/autoload.php');
    } else {
        require_once(realpath(__DIR__.'/../../../').'/autoload.php');
    }
}
$errorTypes = [
    E_ERROR => 'Error',
    E_WARNING => 'Warning',
    E_PARSE => 'Parser error',
    E_NOTICE => 'Notice',
    E_STRICT => 'Strict',
    E_DEPRECATED => 'Deprecated',
    E_CORE_ERROR => 'Error(Core)',
    E_CORE_WARNING => 'Warning(Core)',
    E_COMPILE_ERROR => 'Error(compile)',
    E_COMPILE_WARNING => 'Warning(Compile)',
    E_RECOVERABLE_ERROR => 'Error(Recoverable)',
    E_USER_ERROR => 'Error',
    E_USER_WARNING => 'Warning',
    E_USER_NOTICE => 'Notice',
    E_USER_DEPRECATED => 'Deprecated',
];
set_error_handler(function ($errno, $errstr, $file, $line) use ($errorTypes, $options) {
    if (!(error_reporting() & $errno)) {
        return; // This error code is not included in error_reporting
    }
    $type = array_key_exists($errno, $errorTypes) ? $errorTypes[$errno] : 'Error';
    $color = (substr($type, 0, 5) === 'Error') ? COLOR_RED: COLOR_YELLOW;
    error_log(COLOR_RED.$type. ': '.$errstr.COLOR_STOP);
    if ($options['debug']) {
        error_log(' in '.$file.' on line '.$line);
    }
    if (substr($type, 0, 5) === 'Error') {
        exit($errno);
    }
});
set_exception_handler(function ($exception) use ($options) {
    if ($options['debug']) {
        error_log($exception);
    } else {
        error_log(COLOR_RED.'Exception: '.$exception->getMessage().COLOR_STOP);
        // if ($options['debug']) {
        //     error_log(' in '.$exception->getFile().' on line '.$exception->getLine());
        // }
    }
    exit($exception->getCode() ?: 1);
});
$exit = 0;
Logger::getInstance()->log = function ($entry, $type) use ($options, &$exit) {
    $exit = 1;
    if ($type === E_USER_NOTICE) {
        $type = '';
        $color = COLOR_YELLOW;
    } else {
        $type = 'Warning: ';
        $color = COLOR_RED;
    }
    if ($entry instanceof Exception) {
        error_log(COLOR_RED."Error: " . $entry->getMessage().COLOR_STOP);
        if ($options['debug']) {
            error_log('Stack trace:'.PHP_EOL.$entry->getTraceAsString());
        }
    } else {
        error_log($color. $type . $entry.COLOR_STOP);
        if ($options['debug']) {
            // Show backtrace in debug mode
            $e = (string)(new Exception('trace'));
            $trace = explode("\n", substr($e, strpos($e, 'Stack trace:')));
            foreach ($trace as $i => $entry) {
                if ($i === 0) {
                    error_log($entry);
                }
                if ($i <= 3) {
                    continue;
                }
                preg_match('/#([0-9]+) (.*)$/', $entry, $match);
                error_log('#' .($match[1] - 2).' '.$match[2]);
            }
        }
    }
};
$exclude = null;
if ($options['exclude']) {
    $exclude = $options['exclude'];
    if (strpos($exclude[0], ',') !== false) {
        $exploded = explode(',', $exclude[0]);
        error_log(COLOR_RED.'Comma-separated exclude paths are deprecated, use multiple --exclude statements: --exclude '.$exploded[0].' --exclude '.$exploded[1]).COLOR_STOP;
        $exclude[0] = array_shift($exploded);
        $exclude = array_merge($exclude, $exploded);
    }
}

foreach ($options["processor"] as $processor) {
    $class = '\OpenApi\Processors\\'.$processor;
    if (class_exists($class)) {
        $processor = new $class();
    } elseif (class_exists($processor)) {
        $processor = new $processor();
    }
    Analysis::registerProcessor($processor);
}

$openapi = OpenApi\scan($paths, ['exclude' => $exclude]);

if ($exit !== 0) {
    error_log('');
}
if ($options['output'] === false) {
    if (strtolower($options['format']) === 'json') {
        echo $openapi->toJson();
    } else {
        echo $openapi->toYaml();
    }
    echo "\n";
} else {
    if (is_dir($options['output'])) {
        $options['output'] .= '/openapi.yaml';
    }
    $openapi->saveAs($options['output'], $options['format']);
}
exit($exit);
