'use strict';

const isArray = Array.isArray;
const Node = require('./ast/Node');
const Literal = require('./ast/Literal');
const Identifier = require('./ast/Identifier');
const ok = require('assert').ok;
const Container = require('./ast/Container');
const isValidJavaScriptVarName = require('./util/isValidJavaScriptVarName');

class CodeWriter {
    constructor(options, builder) {
        ok(builder, '"builder" is required');
        options = options || {};
        this.builder = builder;
        this.root = null;
        this._indentStr = options.indent != null ? options.indent : '  ';
        this._indentSize = this._indentStr.length;

        this._code = '';
        this.currentIndent = '';
    }

    getCode() {
        return this._code;
    }

    writeBlock(body) {
        if (!body) {
            this.write('{}');
            return;
        }

        if (typeof body === 'function') {
            body = body();
        }

        if (!body ||
            (Array.isArray(body) && body.length === 0) ||
            (body instanceof Container && body.length === 0)) {
            this.write('{}');
            return;
        }

        this.write('{\n')
            .incIndent();

        this.writeStatements(body);

        this.decIndent()
            .writeLineIndent()
            .write('}');
    }

    writeStatements(nodes) {
        if (!nodes) {
            return;
        }

        ok(nodes, '"nodes" expected');
        let firstStatement = true;

        var writeNode = (node) => {
            if (Array.isArray(node) || (node instanceof Container)) {
                node.forEach(writeNode);
                return;
            } else {
                if (firstStatement) {
                    firstStatement = false;
                } else {
                    this._write('\n');
                }

                this.writeLineIndent();

                if (typeof node === 'string') {
                    this._write(node);
                } else {
                    node.statement = true;
                    this.write(node);
                }

                if (this._code.endsWith('\n')) {
                    // Do nothing
                } else if (this._code.endsWith(';')) {
                    this._code += '\n';
                }  else if (this._code.endsWith('\n' + this.currentIndent)) {
                    // Do nothing
                } else {
                    this._code += ';\n';
                }
            }
        };

        if (nodes instanceof Node) {
            writeNode(nodes);
        } else {
            nodes.forEach(writeNode);
        }
    }

    write(code) {
        if (code == null || code === '') {
            return;
        }

        if (code instanceof Node) {
            let node = code;
            if (!node.writeCode) {
                throw new Error('Node does not have a `writeCode` method: ' + JSON.stringify(node, null, 4));
            }
            node.writeCode(this);
        } else if (isArray(code) || code instanceof Container) {
            code.forEach(this.write, this);
            return;
        } else if (typeof code === 'string') {
            this._code += code;
        }  else if (typeof code === 'boolean' || typeof code === 'number') {
            this._code += code.toString();
        } else {
            throw new Error('Illegal argument: ' + JSON.stringify(code));
        }

        return this;
    }

    _write(code) {
        this._code += code;
        return this;
    }

    incIndent(count) {
        if (count != null) {
            for (let i=0; i<count; i++) {
                this.currentIndent += ' ';
            }
        } else {
            this.currentIndent += this._indentStr;
        }

        return this;
    }

    decIndent(count) {
        if (count == null) {
            count = this._indentSize;
        }

        this.currentIndent = this.currentIndent.substring(
            0,
            this.currentIndent.length - count);

        return this;
    }

    writeLineIndent() {
        this._code += this.currentIndent;
        return this;
    }

    writeIndent() {
        this._code += this._indentStr;
        return this;
    }

    isLiteralNode(node) {
        return node instanceof Literal;
    }

    isIdentifierNode(node) {
        return node instanceof Identifier;
    }

    writeLiteral(value) {
        if (value === null) {
            this.write('null');
        } else if (value === undefined) {
            this.write('undefined');
        } else if (typeof value === 'string') {
            this.write(JSON.stringify(value));
        } else if (value === true) {
            this.write('true');
        } else if (value === false) {
            this.write('false');
        }  else if (isArray(value)) {
            if (value.length === 0) {
                this.write('[]');
                return;
            }

            this.write('[\n');
            this.incIndent();

            for (let i=0; i<value.length; i++) {
                let v = value[i];

                this.writeLineIndent();

                if (v instanceof Node) {
                    this.write(v);
                } else {
                    this.writeLiteral(v);
                }

                if (i < value.length - 1) {
                    this.write(',\n');
                } else {
                    this.write('\n');
                }
            }

            this.decIndent();
            this.writeLineIndent();
            this.write(']');
        } else if (typeof value === 'number') {
            this.write(value.toString());
        } else if (value instanceof RegExp) {
            this.write(value.toString());
        } else if (typeof value === 'object') {
            let keys = Object.keys(value);
            if (keys.length === 0) {
                this.write('{}');
                return;
            }

            this.incIndent();
            this.write('{\n');
            this.incIndent();

            for (let i=0; i<keys.length; i++) {
                let k = keys[i];
                let v = value[k];

                this.writeLineIndent();

                if (isValidJavaScriptVarName(k)) {
                    this.write(k + ': ');
                } else {
                    this.write(JSON.stringify(k) + ': ');
                }

                if (v instanceof Node) {
                    this.write(v);
                } else {
                    this.writeLiteral(v);
                }

                if (i < keys.length - 1) {
                    this.write(',\n');
                } else {
                    this.write('\n');
                }
            }

            this.decIndent();
            this.writeLineIndent();
            this.write('}');
            this.decIndent();
        }
    }
}

module.exports = CodeWriter;