import GitHubInfo from './GitHubInfo';
import program from 'commander';
import { getGrenConfig } from './_utils.js';

/** Class creating a Commander program, managing the options passed via bash and config file. */
class Program {
    constructor(props) {
        const { programOptions, defaults } = this._consumeOptions(props.options);

        this.name = props.name;
        this.description = props.description;
        this.examples = props.examples;
        this.defaults = defaults;
        this.program = this._programWithEvents(this._programWithOptions(program, programOptions), props.events)
            .name(this.name)
            .description(this.description)
            .parse(props.argv);

        this.options = Object.assign(
            {},
            getGrenConfig(props.cwd, program.config),
            this._getOptionsFromObject(this.program, this.defaults)
        );
    }

    /**
     * Initialise the module
     *
     * @since 0.10.0
     * @public
     *
     * @return {Promise}
     */
    async init() {
        const options = await this._getEnvOptions();
        this.options = this._filterObject(this._camelCaseObjectKeys(
            Object.assign(
                {},
                this.defaults,
                Object.assign({}, ...[].concat(options)),
                this.options
            )
        ));

        return this.options;
    }

    /**
     * Get informations from the local folder
     *
     * @since 0.10.0
     * @private
     *
     * @return {Promise}
     */
    _getEnvOptions() {
        const githubInfo = new GitHubInfo();
        const { username, repo } = this.options;

        if (username && repo) {
            return githubInfo.token;
        }

        return githubInfo.options;
    }

    /**
     * Remove all the properties that have undefined values from an object
     *
     * @since  0.10.0
     * @private
     *
     * @param  {Object} object
     *
     * @return {Object}
     */
    _filterObject(object) {
        return Object.entries(object)
            .filter(([key, value]) => value !== undefined)
            .reduce((carry, [key, value]) => {
                carry[key] = value;
                return carry;
            }, {});
    }

    /**
     * Add all the given events to a program
     *
     * @since  0.10.0
     * @private
     *
     * @param  {Commander} program
     * @param  {Object} events
     *
     * @return {Commander}
     */
    _programWithEvents(program, events) {
        if (!events || !events.length) {
            return program;
        }

        Object.entries(events).forEach(([event, action]) => {
            program.on(event, action);
        });

        return program;
    }

    /**
     * Add all the given options to a program
     *
     * @since 0.10.0
     * @private
     *
     * @param  {Commander} program
     * @param  {Array} options
     *
     * @return {Commander}
     */
    _programWithOptions(program, options) {
        options.forEach(({ name, description, action, defaultValue }) => program.option(...[name, description, action, defaultValue].filter(Boolean)));
        return program;
    }

    /**
     * Consume the options from the properties and provide get the defaults and the programOptions
     *
     * @since  0.10.0
     * @private
     *
     * @param  {Array} opts
     *
     * @return {Object}
     */
    _consumeOptions(opts = []) {
        if (!Array.isArray(opts)) {
            return {
                programOptions: [],
                defaults: {}
            };
        }

        const programOptions = opts.map(({ short, name, valueType, description, defaultValue, action }) => ({
            name: short && name ? `${short}, --${name} ${valueType || ''}` : ' ',
            description,
            defaultValue,
            action
        }));

        const defaults = this._camelCaseObjectKeys(
            opts.reduce((carry, opt) => {
                carry[opt.name] = opt.defaultValue;
                return carry;
            }, {})
        );

        return {
            programOptions,
            defaults
        };
    }

    /**
     * Extrapulate the options from a program
     *
     * @since  0.10.0
     * @private
     *
     * @param  {Object} defaults
     *
     * @return {Object}
     */
    _getOptionsFromObject(object = {}, defaults = {}) {
        if (typeof object !== 'object' || Array.isArray(object)) {
            return {};
        }

        return Object.keys(defaults).reduce((carry, option) => {
            if (object[option] && object[option] !== defaults[option]) {
                carry[option] = object[option];
            }

            return carry;
        }, {});
    }

    /**
     * Converts all Object values to camel case
     *
     * @param  {Object} object
     *
     * @return {Object}
     */
    _camelCaseObjectKeys(object = {}) {
        if (typeof object !== 'object' || Array.isArray(object)) {
            return {};
        }

        return Object.entries(object).reduce((carry, [key, value]) => {
            carry[this._dashToCamelCase(key)] = value;
            return carry;
        }, {});
    }

    /**
    * Transforms a dasherize string into a camel case one.
    *
    * @since 0.3.2
    * @private
    *
    * @param  {string} value The dasherize string
    *
    * @return {string}       The camel case string
    */
    _dashToCamelCase(value = '') {
        if (typeof value !== 'string') {
            return '';
        }

        return value
            .replace(/-([a-z])/g, (match) => match[1].toUpperCase());
    }
}

export default Program;
