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

//----------------------------------------------------------------------
//
//  Copyright (C) 2015-2016 Artem Rodygin
//
//  This file is part of Linode API Client Library for PHP.
//
//  You should have received a copy of the MIT License along with
//  the library. If not, see <http://opensource.org/licenses/MIT>.
//
//----------------------------------------------------------------------

// Some known strings to replace with camelCase.

$vocabulary = [

    // reservered words
    'clone'                  => 'duplicate',
    'echo'                   => 'test',
    'list'                   => 'getList',

    // classes
    'nodebalancer'           => 'NodeBalancer',
    'professionalservices'   => 'ProfessionalServices',
    'stackscript'            => 'StackScript',

    // methods
    'addprivate'             => 'addPrivate',
    'addpublic'              => 'addPublic',
    'createfromdistribution' => 'createFromDistribution',
    'createfromimage'        => 'createFromImage',
    'createfromstackscript'  => 'createFromStackScript',
    'datacenters'            => 'dataCenters',
    'estimateinvoice'        => 'estimateInvoice',
    'getapikey'              => 'getApiKey',
    'linodeplans'            => 'linodePlans',
    'nodebalancers'          => 'nodeBalancers',
    'paybalance'             => 'payBalance',
    'setrdns'                => 'setReverseDNS',
    'stackscripts'           => 'stackScripts',
    'updatecard'             => 'updateCard',
    'webconsoletoken'        => 'webConsoleToken',
];

// The spec doesn't keep the order of parameters in consistency.
// To avoid changing of the order on each update we store predefined order for known functions.

$params_order = [
    'account.estimateinvoice'            => ['mode', 'LinodeID', 'PlanID', 'PaymentTerm'],
    'account.info'                       => [],
    'account.paybalance'                 => [],
    'account.updatecard'                 => ['ccNumber', 'ccExpMonth', 'ccExpYear'],
    'api.spec'                           => [],
    'avail.datacenters'                  => [],
    'avail.distributions'                => ['DistributionID'],
    'avail.kernels'                      => ['isXen', 'isKVM'],
    'avail.linodeplans'                  => ['PlanID'],
    'avail.nodebalancers'                => [],
    'avail.stackscripts'                 => ['DistributionID', 'DistributionVendor', 'keywords'],
    'domain.create'                      => ['Domain', 'Type', 'SOA_Email', 'Description', 'Refresh_sec', 'Retry_sec', 'Expire_sec', 'TTL_sec', 'lpm_displayGroup', 'status', 'master_ips', 'axfr_ips'],
    'domain.delete'                      => ['DomainID'],
    'domain.list'                        => ['DomainID'],
    'domain.resource.create'             => ['DomainID', 'Type', 'Name', 'Target', 'Priority', 'Weight', 'Port', 'Protocol', 'TTL_sec'],
    'domain.resource.delete'             => ['DomainID', 'ResourceID'],
    'domain.resource.list'               => ['DomainID', 'ResourceID'],
    'domain.resource.update'             => ['DomainID', 'ResourceID', 'Name', 'Target', 'Priority', 'Weight', 'Port', 'Protocol', 'TTL_sec'],
    'domain.update'                      => ['DomainID', 'Domain', 'Type', 'SOA_Email', 'Description', 'Refresh_sec', 'Retry_sec', 'Expire_sec', 'TTL_sec', 'lpm_displayGroup', 'status', 'master_ips', 'axfr_ips'],
    'image.delete'                       => ['ImageID'],
    'image.list'                         => ['ImageID', 'pending'],
    'image.update'                       => ['ImageID', 'label', 'description'],
    'linode.boot'                        => ['LinodeID', 'ConfigID'],
    'linode.clone'                       => ['LinodeID', 'DatacenterID', 'PlanID', 'PaymentTerm'],
    'linode.config.create'               => ['LinodeID', 'Label', 'KernelID', 'DiskList', 'RunLevel', 'RAMLimit', 'virt_mode', 'RootDeviceNum', 'RootDeviceCustom', 'RootDeviceRO', 'devtmpfs_automount', 'helper_distro', 'helper_xen', 'helper_disableUpdateDB', 'helper_depmod', 'helper_network', 'Comments'],
    'linode.config.delete'               => ['LinodeID', 'ConfigID'],
    'linode.config.list'                 => ['LinodeID', 'ConfigID'],
    'linode.config.update'               => ['LinodeID', 'ConfigID', 'Label', 'KernelID', 'DiskList', 'RunLevel', 'RAMLimit', 'virt_mode', 'RootDeviceNum', 'RootDeviceCustom', 'RootDeviceRO', 'devtmpfs_automount', 'helper_distro', 'helper_xen', 'helper_disableUpdateDB', 'helper_depmod', 'helper_network', 'Comments'],
    'linode.create'                      => ['DatacenterID', 'PlanID', 'PaymentTerm'],
    'linode.delete'                      => ['LinodeID', 'skipChecks'],
    'linode.disk.create'                 => ['LinodeID', 'Label', 'Type', 'Size', 'isReadOnly'],
    'linode.disk.createfromdistribution' => ['LinodeID', 'DistributionID', 'Label', 'Size', 'rootPass', 'rootSSHKey'],
    'linode.disk.createfromimage'        => ['LinodeID', 'ImageID', 'Label', 'size', 'rootPass', 'rootSSHKey'],
    'linode.disk.createfromstackscript'  => ['LinodeID', 'StackScriptID', 'StackScriptUDFResponses', 'DistributionID', 'Label', 'Size', 'rootPass', 'rootSSHKey'],
    'linode.disk.delete'                 => ['LinodeID', 'DiskID'],
    'linode.disk.duplicate'              => ['LinodeID', 'DiskID'],
    'linode.disk.imagize'                => ['LinodeID', 'DiskID', 'Label', 'Description'],
    'linode.disk.list'                   => ['LinodeID', 'DiskID'],
    'linode.disk.resize'                 => ['LinodeID', 'DiskID', 'size'],
    'linode.disk.update'                 => ['LinodeID', 'DiskID', 'Label', 'isReadOnly'],
    'linode.ip.addprivate'               => ['LinodeID'],
    'linode.ip.addpublic'                => ['LinodeID'],
    'linode.ip.list'                     => ['LinodeID', 'IPAddressID'],
    'linode.ip.setrdns'                  => ['IPAddressID', 'Hostname'],
    'linode.ip.swap'                     => ['IPAddressID', 'withIPAddressID', 'toLinodeID'],
    'linode.job.list'                    => ['LinodeID', 'JobID', 'pendingOnly'],
    'linode.kvmify'                      => ['LinodeID'],
    'linode.list'                        => ['LinodeID'],
    'linode.mutate'                      => ['LinodeID'],
    'linode.reboot'                      => ['LinodeID', 'ConfigID'],
    'linode.resize'                      => ['LinodeID', 'PlanID'],
    'linode.shutdown'                    => ['LinodeID'],
    'linode.update'                      => ['LinodeID', 'Label', 'lpm_displayGroup', 'Alert_cpu_enabled', 'Alert_cpu_threshold', 'Alert_diskio_enabled', 'Alert_diskio_threshold', 'Alert_bwin_enabled', 'Alert_bwin_threshold', 'Alert_bwout_enabled', 'Alert_bwout_threshold', 'Alert_bwquota_enabled', 'Alert_bwquota_threshold', 'backupWindow', 'backupWeeklyDay', 'watchdog', 'ms_ssh_disabled', 'ms_ssh_user', 'ms_ssh_ip', 'ms_ssh_port'],
    'linode.webconsoletoken'             => ['LinodeID'],
    'nodebalancer.config.create'         => ['NodeBalancerID', 'Port', 'Protocol', 'Algorithm', 'Stickiness', 'check', 'check_interval', 'check_timeout', 'check_attempts', 'check_path', 'check_body', 'check_passive', 'ssl_cert', 'ssl_key', 'cipher_suite'],
    'nodebalancer.config.delete'         => ['NodeBalancerID', 'ConfigID'],
    'nodebalancer.config.list'           => ['NodeBalancerID', 'ConfigID'],
    'nodebalancer.config.update'         => ['ConfigID', 'Port', 'Protocol', 'Algorithm', 'Stickiness', 'check', 'check_interval', 'check_timeout', 'check_attempts', 'check_path', 'check_body', 'check_passive', 'ssl_cert', 'ssl_key', 'cipher_suite'],
    'nodebalancer.create'                => ['DatacenterID', 'Label', 'ClientConnThrottle'],
    'nodebalancer.delete'                => ['NodeBalancerID'],
    'nodebalancer.list'                  => ['NodeBalancerID'],
    'nodebalancer.node.create'           => ['ConfigID', 'Label', 'Address', 'Weight', 'Mode'],
    'nodebalancer.node.delete'           => ['NodeID'],
    'nodebalancer.node.list'             => ['ConfigID', 'NodeID'],
    'nodebalancer.node.update'           => ['NodeID', 'Label', 'Address', 'Weight', 'Mode'],
    'nodebalancer.update'                => ['NodeBalancerID', 'Label', 'ClientConnThrottle'],
    'stackscript.create'                 => ['script', 'DistributionIDList', 'Label', 'Description', 'isPublic', 'rev_note'],
    'stackscript.delete'                 => ['StackScriptID'],
    'stackscript.list'                   => ['StackScriptID'],
    'stackscript.update'                 => ['StackScriptID', 'script', 'DistributionIDList', 'Label', 'Description', 'isPublic', 'rev_note'],
    'user.getapikey'                     => ['username', 'password', 'token', 'expires', 'label'],
];

// Get API specification.

$json = file_get_contents('https://api.linode.com/?api_action=api.spec');

$api = json_decode($json, true);
$api = $api['DATA'];

file_put_contents(__DIR__ . '/apicache.php', sprintf("<?php\n\nreturn %s;\n", var_export($api, true)));

$version = $api['VERSION'];

// Array of classes with nested arrays of methods.

$classes = [];

// Convert "linode.disk.createfromstackscript" names to ["Linode.Disk"]["createFromStackScript"].

foreach ($api['METHODS'] as $entry => $api_method) {

    if ($entry === 'test.echo') {
        continue;
    }

    $parts = explode('.', $entry);

    $method = array_pop($parts);

    if (array_key_exists($method, $vocabulary)) {
        $method = $vocabulary[$method];
    }

    foreach ($parts as &$value) {

        if (array_key_exists($value, $vocabulary)) {
            $value = $vocabulary[$value];
        }

        $value = ucfirst($value);
    }

    $class = implode('.', $parts);

    if (!array_key_exists($class, $classes)) {
        $classes[$class] = [];
    }

    $classes[$class][$method] = $api_method;
    $classes[$class][$method]['ACTION'] = $entry;
}

// Generate classes.

$class_template    = file_get_contents(__DIR__ . '/class.txt');
$function_template = file_get_contents(__DIR__ . '/function.txt');

foreach ($classes as $class => $methods) {

    ksort($methods);

    // Source code of class methods.
    $php = null;

    // Add spec version info into "Linode\Api" class.
    if ($class === 'Api') {
        $php .= "\n";
        $php .= "    /** API version */\n";
        $php .= "    const VERSION = '{$api['VERSION']}';\n";
    }

    // Enumerate class methods.
    foreach ($methods as $method => $meta) {

        // The original API spec contains optional and required parameters unordered.

        $parameters = [];

        // If we have predefined order for this function, use it.
        if (array_key_exists($meta['ACTION'], $params_order)) {

            // Check for new parameters.
            $diff = array_diff(array_keys($meta['PARAMETERS']), $params_order[$meta['ACTION']]);

            if (count($diff) !== 0) {
                printf("Unknown parameters in '{$meta['ACTION']}': %s\n", implode(', ', $diff));
                exit;
            }

            foreach ($params_order[$meta['ACTION']] as $parameter) {

                // Known parameter has gone - update predefined order first.
                if (!array_key_exists($parameter, $meta['PARAMETERS'])) {
                    print("Known parameter '{$parameter}' has gone from '{$meta['ACTION']}'.\n");
                    exit;
                }

                $parameters[] = $meta['PARAMETERS'][$parameter];
            }
        }
        // Otherwise we have to prepare it first.
        else {
            print("Unknown API: '{$meta['ACTION']}'.\n");
            exit;
        }

        // Replace original parameters with reordered ones.
        $meta['PARAMETERS'] = $parameters;

        // Parameters annotation.
        $annotation = null;

        // Function parameters.
        $function_params = [];

        // API call parameters.
        $call_params = null;

        // Enumnerate method parameters.
        foreach ($meta['PARAMETERS'] as $parameter) {

            // Map of spec types to PHP types.
            $types = [
                'boolean' => 'bool',
                'numeric' => 'int',
                'string'  => 'string',
            ];

            // Add parameter's annotation.
            $annotation .= sprintf("%s* @param %s $%s [%s] %s\n",
                str_repeat(' ', 5),
                $types[$parameter['TYPE']],
                $parameter['NAME'],
                $parameter['REQUIRED'] ? 'required' : 'optional',
                $parameter['DESCRIPTION']
            );

            // Add parameter to function's declaration.
            $function_params[] = sprintf('$%s%s',
                $parameter['NAME'],
                $parameter['REQUIRED'] ? null : ' = null'
            );

            // Add parameter to API call inside the function.
            $call_params .= sprintf("%s'%s' => $%s,\n",
                str_repeat(' ', 12),
                $parameter['NAME'],
                $parameter['NAME']
            );
        }

        // Split long descriptions.
        $description = wordwrap($meta['DESCRIPTION'], 100, "\n     * ");

        // Generate complete function.
        $function = sprintf($function_template,
            $description,
            $annotation,
            $method,
            implode(', ', $function_params),
            $meta['ACTION'],
            $call_params
        );

        // Remove empty array from API call.
        $function = str_replace(", [\n        ]", null, $function);

        $php .= $function;
    }

    // Convert class name into fully-qualified namespace (e.g. "NodeBalancer.Node" => "Linode\NodeBalancer\Node").
    $class     = explode('.' , 'Linode.' . $class);
    $classname = array_pop($class);
    $namespace = implode('\\', $class);

    // Append class name with "Api" suffix.
    if ($classname !== 'Api') {
        $classname .= 'Api';
    }

    // Convert namespace into FS path.
    $dir = str_replace('\\', '/', $namespace);
    $dir = substr($dir, 6);

    // Combine all parts into file contents.
    $php = sprintf($class_template, $namespace, $version, $classname, $php);

    // Remove unnecessary "use" operators.
    if ($namespace === 'Linode') {
        $php = str_replace("use Linode\\BaseLinodeApi;\n\n", null, $php);
    }

    // Flush the generated file contents.
    if (!file_exists("src{$dir}")) {
        mkdir("src{$dir}");
    }

    file_put_contents("src{$dir}/{$classname}.php", $php);
}
