const yaml = require('js-yaml');
const Type = require('js-yaml/lib/js-yaml/type');
const Schema = require('js-yaml/lib/js-yaml/schema');

const { is, descriptors } = require('./utils');
const { doc, type, requires, defaultValue } = descriptors;

const UNDEFINED = Symbol('undefined');
// output null as empty in yaml
const YAML_SCHEMA = new Schema({
    include: [
        require('js-yaml/lib/js-yaml/schema/default_full')
    ],
    implicit: [
        new Type('tag:yaml.org,2002:null', {
            kind: 'scalar',
            resolve(data) {
                if (data === null) {
                    return true;
                }
                const max = data.length;
                return (max === 1 && data === '~') ||
                    (max === 4 && (data === 'null' || data === 'Null' || data === 'NULL'));
            },
            construct: () => null,
            predicate: object => object === null,
            represent: {
                empty: function () { return ''; }
            },
            defaultStyle: 'empty'
        })
    ]
});

function appendDoc(spec, defaults) {
    if (defaults === null) {
        return null;
    }
    if (is.array(defaults) && spec.hasOwnProperty('*')) {
        return defaults.map(value => appendDoc(spec['*'], value));
    } else if (is.object(defaults)) {
        const _defaults = {};
        for (let key in defaults) {
            if (spec.hasOwnProperty(key) && spec[key].hasOwnProperty(doc)) {
                let i = 0;
                for (let line of spec[key][doc].split('\n')) {
                    _defaults['#' + key + i++] = line;
                }
            }
            _defaults[key] = appendDoc(spec.hasOwnProperty(key) ? spec[key] : {}, defaults[key]);
        }
        return _defaults;
    }
    return defaults;
}

function generate(spec, parentConfig = null) {
    if (!is.spec(spec)) {
        return UNDEFINED;
    }
    if (spec.hasOwnProperty(requires) && !spec[requires](parentConfig)) {
        return UNDEFINED;
    }
    if (spec.hasOwnProperty(defaultValue)) {
        return appendDoc(spec, spec[defaultValue]);
    }
    const types = is.array(spec[type]) ? spec[type] : [spec[type]];
    if (types.includes('object')) {
        let defaults = UNDEFINED;
        for (let key in spec) {
            if (key === '*') {
                continue;
            }
            const value = generate(spec[key], defaults);
            if (value !== UNDEFINED) {
                if (defaults === UNDEFINED) {
                    defaults = {};
                }
                defaults[key] = value;
            }
        }
        return appendDoc(spec, defaults);
    } else if (types.includes('array') && spec.hasOwnProperty('*')) {
        return [generate(spec['*'], {})];
    }
    return UNDEFINED;
}

class ConfigGenerator {
    constructor(spec) {
        this.spec = spec;
    }

    generate() {
        return yaml.safeDump(generate(this.spec), {
            indent: 4,
            lineWidth: 1024,
            schema: YAML_SCHEMA
        }).replace(/^(\s*)\'#.*?\':\s*\'*(.*?)\'*$/mg, '$1# $2'); // make comment lines
    }
}

module.exports = ConfigGenerator;