"use strict";
var Container = require("./Container");
var ArrayContainer = require("./ArrayContainer");
var ok = require("assert").ok;
var extend = require("raptor-util/extend");
var inspect = require("util").inspect;
var EventEmitter = require("events").EventEmitter;

function trim(textNode) {
    if (textNode.preserveWhitespace === true) {
        return;
    }

    var text = textNode.argument.value;
    var isFirst = textNode.isFirst;
    var isLast = textNode.isLast;

    if (isFirst) {
        //First child
        text = text.replace(/^\r?\n\s*/g, "");
    }
    if (isLast) {
        //Last child
        text = text.replace(/\r?\n\s*$/g, "");
    }
    if (/^\r?\n\s*$/.test(text)) {
        //Whitespace between elements
        text = "";
    }
    text = text.replace(/\s+/g, " ");
    textNode.argument.value = text;
}

class Node {
    constructor(type) {
        this.type = type;
        this.statement = false;
        this.container = null;
        this.pos = null; // The character index of the node in the original source file
        this.tagDef = null; // The tag definition associated with this Node
        this._codeGeneratorFuncs = null;
        this._flags = {};
        this._transformersApplied = new Set();
        this._preserveWhitespace = null;
        this._events = null;
        this._childTextNormalized = undefined;
        this.data = {};
        this._finalNode = false;
        this._trimStartEnd = false;
    }

    on(event, listener) {
        if (!this._events) {
            this._events = new EventEmitter();
        }

        this._events.on(event, listener);
    }

    emit() {
        if (this._events) {
            this._events.emit.apply(this._events, arguments);
        }
    }

    listenerCount(event) {
        if (this._events) {
            return this._events.listenerCount(event);
        } else {
            return 0;
        }
    }

    onBeforeGenerateCode(listener) {
        this.on("beforeGenerateCode", listener);
    }

    onAfterGenerateCode(listener) {
        this.on("afterGenerateCode", listener);
    }

    wrapWith(wrapperNode) {
        ok(this.container, "Node does not belong to a container: " + this);
        var replaced = this.container.replaceChild(wrapperNode, this);
        ok(replaced, "Invalid state. Child does not belong to the container");
        wrapperNode.appendChild(this);
    }

    replaceWith(newNode) {
        ok(this.container, "Node does not belong to a container: " + this);
        var replaced = this.container.replaceChild(newNode, this);
        ok(replaced, "Invalid state. Child does not belong to the container");
    }

    insertSiblingBefore(newNode) {
        ok(this.container, "Node does not belong to a container: " + this);
        this.container.insertChildBefore(newNode, this);
    }

    insertSiblingAfter(newNode) {
        ok(this.container, "Node does not belong to a container: " + this);
        this.container.insertChildAfter(newNode, this);
    }

    /**
     * Converts the provided `array` into a `ArrayContainer`. If the provided `array` is already an instance of a `Container` then it is simply returned.
     * @param  {[type]} array [description]
     * @return {[type]}       [description]
     */
    makeContainer(array) {
        if (array instanceof Container) {
            return array;
        }

        if (!Array.isArray(array) && array instanceof Node) {
            array = [array];
        }

        return new ArrayContainer(this, array);
    }

    prependChild(node) {
        ok(this.body, "Node does not support child nodes: " + this);
        this.body.prependChild(node);
    }

    appendChild(node) {
        ok(this.body, "Node does not support child nodes: " + this);
        this.body.appendChild(node);
    }

    appendChildren(nodes) {
        ok(this.body, "Node does not support child nodes: " + this);
        nodes.forEach(node => {
            this.body.appendChild(node);
        });
    }

    insertBefore(newNode, referenceNode) {
        ok(this.body, "Node does not support child nodes: " + this);
        this.body.insertBefore(newNode, referenceNode);
    }

    forEachChild(callback, thisObj) {
        if (this.body) {
            this.body.forEach(callback, thisObj);
        }
    }

    moveChildrenTo(targetNode) {
        ok(this.body, "Node does not support child nodes: " + this);
        ok(
            this !== targetNode,
            "Target node cannot be the same as the source node"
        );

        this.body.moveChildrenTo(targetNode);
    }

    removeChildren() {
        this.body.removeChildren();
    }

    forEachNextSibling(callback, thisObj) {
        var container = this.container;

        if (container) {
            container.forEachNextSibling(this, callback, thisObj);
        }
    }

    get firstChild() {
        var body = this.body;
        return body && body.firstChild;
    }

    get previousSibling() {
        var container = this.container;

        if (container) {
            return container.getPreviousSibling(this);
        }
    }

    get nextSibling() {
        var container = this.container;

        if (container) {
            return container.getNextSibling(this);
        }
    }

    isTransformerApplied(transformer) {
        return this._transformersApplied.has(transformer);
    }

    setTransformerApplied(transformer) {
        this._transformersApplied.add(transformer);
    }

    toString() {
        return inspect(this);
    }

    toJSON() {
        let result = extend({}, this);
        delete result.container;
        delete result.statement;
        delete result.pos;
        delete result._transformersApplied;
        delete result._codeGeneratorFuncs;
        delete result._flags;
        delete result.data;
        delete result.tagDef;
        delete result._preserveWhitespace;
        delete result._events;
        delete result._finalNode;
        delete result._trimStartEnd;
        delete result._childTextNormalized;
        return result;
    }

    detach() {
        if (this.container) {
            this.container.removeChild(this);
            this.container = null;
        }
    }

    /**
     * Returns true if the current node represents a compound expression (e.g. )
     * @return {Boolean} [description]
     */
    isCompoundExpression() {
        return false;
    }

    isDetached() {
        return this.container == null;
    }

    /**
     * Used by the Node.js require('util').inspect function.
     * We default to inspecting on the simplified version
     * of this node that is the same version we use when
     * serializing to JSON.
     */
    [inspect.custom]() {
        // We inspect in the simplified version of this object t
        return this.toJSON();
    }

    setType(newType) {
        this.type = newType;
    }

    setCodeGenerator(mode, codeGeneratorFunc) {
        if (arguments.length === 1) {
            codeGeneratorFunc = arguments[0];
            mode = null;
        }

        if (!this._codeGeneratorFuncs) {
            this._codeGeneratorFuncs = {};
        }
        this._codeGeneratorFuncs[mode || "DEFAULT"] = codeGeneratorFunc;
    }

    getCodeGenerator(mode) {
        if (this._codeGeneratorFuncs) {
            return (
                this._codeGeneratorFuncs[mode] ||
                this._codeGeneratorFuncs.DEFAULT
            );
        } else {
            return undefined;
        }
    }

    setFlag(name) {
        this._flags[name] = true;
    }

    clearFlag(name) {
        delete this._flags[name];
    }

    isFlagSet(name) {
        return this._flags.hasOwnProperty(name);
    }

    get bodyText() {
        var bodyText = "";

        this.forEachChild(child => {
            if (child.type === "Text") {
                var childText = child.argument;
                if (childText && childText.type === "Literal") {
                    bodyText += childText.value;
                }
            }
        });

        return bodyText;
    }

    get parentNode() {
        return this.container && this.container.node;
    }

    setPreserveWhitespace(isPreserved) {
        this._preserveWhitespace = isPreserved;
    }

    isPreserveWhitespace() {
        var preserveWhitespace = this._preserveWhitespace;
        if (preserveWhitespace == null) {
            preserveWhitespace = this.tagDef && this.tagDef.preserveWhitespace;
        }

        return preserveWhitespace === true;
    }

    setFinalNode() {
        this._finalNode = true;
    }

    setTrimStartEnd(trimStartEnd) {
        this._trimStartEnd = trimStartEnd;
    }

    _normalizeChildTextNodes(context, forceTrim, forceTrimStartEnd) {
        if (this._childTextNormalized) {
            return;
        }

        this._childTextNormalized = true;

        var trimStartEnd =
            forceTrimStartEnd === true || this._trimStartEnd === true;

        var isPreserveWhitespace = false;

        if (!forceTrim) {
            if (
                context.isPreserveWhitespace() ||
                this.preserveWhitespace === true ||
                this.isPreserveWhitespace()
            ) {
                isPreserveWhitespace = true;
            }
        }

        if (isPreserveWhitespace && trimStartEnd !== true) {
            return;
        }

        var body = this.body;
        if (!body) {
            return;
        }

        var isFirst = true;

        var currentTextLiteral = null;
        var literalTextNodes = [];

        body.forEach(curChild => {
            if (curChild.noOutput) {
                // Skip over AST nodes that produce no HTML output
                return;
            }

            if (curChild.type === "Text" && curChild.isLiteral()) {
                curChild.isFirst = null;
                curChild.isLast = null;

                if (
                    currentTextLiteral &&
                    currentTextLiteral.preserveWhitespace ===
                        curChild.preserveWhitespace &&
                    currentTextLiteral.escape === curChild.escape
                ) {
                    currentTextLiteral.argument.value +=
                        curChild.argument.value;
                    curChild.detach();
                } else {
                    currentTextLiteral = curChild;
                    literalTextNodes.push(currentTextLiteral);
                    if (isFirst) {
                        currentTextLiteral.isFirst = true;
                    }
                }
            } else {
                currentTextLiteral = null;
            }

            if (isFirst && curChild.type !== "Scriptlet") {
                isFirst = false;
            }
        });

        if (currentTextLiteral) {
            // Last child text
            currentTextLiteral.isLast = true;
        }

        if (trimStartEnd) {
            if (literalTextNodes.length) {
                // We will only trim the first and last nodes
                var firstTextNode = literalTextNodes[0];
                var lastTextNode =
                    literalTextNodes[literalTextNodes.length - 1];

                if (firstTextNode.isFirst) {
                    firstTextNode.argument.value = firstTextNode.argument.value.replace(
                        /^\s*/,
                        ""
                    );
                }

                if (lastTextNode.isLast) {
                    lastTextNode.argument.value = lastTextNode.argument.value.replace(
                        /\s*$/,
                        ""
                    );
                }
            }
        }

        if (!isPreserveWhitespace) {
            literalTextNodes.forEach(trim);
        }

        literalTextNodes.forEach(textNode => {
            if (textNode.argument.value === "") {
                textNode.detach();
            }
        });
    }

    get childCount() {
        ok(this.body, "Node does not support child nodes: " + this);
        return this.body.length;
    }
}

module.exports = Node;
