import { ContainerElements } from '../container/exported';
import { ContainerAttributes } from '../container/exported';
import { Cache } from '../utils/exported';
import { AttributeController } from './attributeController';
import { ElementController } from './elementController';
import { InterpolateController } from './interpolateController';
import { ViewController } from './viewController';
import { IElement, IControllerArray, IBindingContext, ITemplateCache } from '../interface/exported';
import { DOM } from '../utils/exported';


/**
 * This class have methods to parse the string/template and initiate our custom elements/attributes/binding
 *
 */
export class View {


    /**
     * creates element and parses template string
     * just used insternally for creating root
     * @internal
     * @param _class - class to use
     * @param element - element node
     * @param bindingContext - binding Context
     * @param templateString - template string
     * @param viewController - viewController
     */
    public static parseAndCreateElement(
        _class: IElement,
        element: Node,
        bindingContext: IBindingContext,
        templateString: string,
        viewController: ViewController
    ): ElementController {

        const elementController = new ElementController(bindingContext, element, _class, null, templateString, viewController);
        const controller = elementController.init();

        return controller;
    }



    /**
     * cleans a html template
     * @param template - template element to clean up
     */
    public static cleanTemplate(template: HTMLElement): void {

        const loopNodes = (node: Node) => {
            for (let n = 0; n < node.childNodes.length; n++) {
                let child = node.childNodes[n];

                if (child.nodeType === 8 || (child.nodeType === 3 && !/\S/.test(child.nodeValue))) {

                    // 8 Represents a comment
                    // Represents textual content in an element or attribute, but check if empty
                    node.removeChild(child);
                    n--;

                } else if (child.nodeType === 1) {

                    // 1 = Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference
                    loopNodes(child);
                }
                child = null;
            }
        };
        loopNodes(template);


    }



    /**
     * creates a template from string markup
     * @param markup - string markup to add- remember to wrap inside <template>
     */
    public static createTemplate(markup: string): Element {
        let template: any;

        if (!Cache.templateMap.has(markup)) {

            const container = DOM.document.createElement('div');
            container.innerHTML = (<any>markup).default || markup;
            const fragment = container.firstElementChild;
            if (!(<any>fragment).content) {
                // ie11 fix
                (<any>fragment).content = DOM.document.createDocumentFragment();
                while (fragment.childNodes[0]) {
                    (<any>fragment).content.appendChild(fragment.childNodes[0]);
                }
            }

            template = DOM.document.createElement('mf-template');
            View.cleanTemplate((fragment as any).content);
            template.appendChild((fragment as any).content);
            Cache.templateMap.set(markup, template);

        } else {
            template = Cache.templateMap.get(markup);
        }

        const x = template.cloneNode(true);
        template = null;

        return x;
    }



    /**
     * adds template and calls attached
     * @param template - template to attach
     * @param toNode - node you want to attach it to
     * @param controllers - controllers to attach
     */
    public static attachTemplate(template: any, toNode: Element, controllers: IControllerArray): Element[] {
        const el = [];

        while (template.firstChild) {
            el.push(template.firstChild);
            toNode.appendChild(template.firstChild);
        }
        controllers.forEach((x) => {
            if (x.attached) {
                x.attached();
            }
        });

        return el;
    }



    /**
     * helper for clearing views on array of elements
     * optional viewController you can pass in too
     * if you are to lazy for calling viewController.clearView() :-)
     * @param elements - elements to remove
     * @param viewController - viewcontrolelrs to remove
     */
    public static clearViews(elements: Element[], viewController?: ViewController) {
        if (viewController) {
            viewController.clearView();
        }

        elements.forEach((el) => {
            // this will traverse down to every child and tell them to detach
            if (el.parentNode) {
                el.parentNode.removeChild(el);
            }
        });
    }



    /**
     * parses nodes and adds initiates elements/interpolate and attributes
     * use cache version when generating many
     *
     * @param template - template to use
     * @param bindingContext - binding context
     * @param viewController - viewcontroller
     */
    public static parseTemplate(template: Node, bindingContext: IBindingContext, viewController: ViewController): IControllerArray {

        const templateCache = View.createTemplateCache(template);
        const controllers = View.parseTemplateCache(template, bindingContext, viewController, templateCache);

        return controllers;

    }



    /**
     * parses nodes and adds initiates elements/interpolate and attributes
     * this uses cache to loop, use createTemplateCache to generate cache
     *
     * @internal
     * @param template - template to use
     * @param bindingContext - bindingcontext to use
     * @param viewController - viewController
     * @param cacheX - template cache
     */
    public static parseTemplateCache(
        template: Node,
        bindingContext: IBindingContext,
        viewController: ViewController,
        cacheX: ITemplateCache[]): IControllerArray {

        const arr: IControllerArray = [];
        const cache = cacheX.slice().reverse();

        // loops and pushes into our node map/array
        const loopNodes = function (_node: Node) {

            // loop children
            for (let n = 0; n < _node.childNodes.length; n++) {
                const htmlNode = _node.childNodes[n];
                const curcach = cache.pop();

                curcach.attributes.forEach((attr) => {
                    let controller: any;
                    let attrNode: any;
                    switch (attr.type) {
                        case 'controller':
                            attrNode = (htmlNode as Element).getAttributeNode(attr.name);
                            controller = new AttributeController(bindingContext, htmlNode, attrNode, attr.container, viewController);
                            arr.push(controller);
                            break;
                        case 'value':
                            attrNode = (htmlNode as Element).getAttributeNode(attr.name);
                            controller = new InterpolateController(bindingContext, attrNode, viewController, true);
                            arr.push(controller);
                            break;
                        case 'attribute':
                            attrNode = (htmlNode as Element).getAttributeNode(attr.name);
                            controller = new AttributeController(bindingContext, htmlNode, attrNode, attr.container, viewController);
                            arr.push(controller);
                            break;
                    }
                });

                switch (curcach.type) {
                    case 'element':
                        const elementController = new ElementController(bindingContext, htmlNode, null, curcach.container, null, viewController);
                        arr.push(elementController);
                        break;
                    case 'text':
                        const interpolateController = new InterpolateController(bindingContext, htmlNode, viewController, false);
                        arr.push(interpolateController);
                        break;
                    default:
                        if (curcach.gotoChild) {
                            loopNodes(htmlNode);
                        }
                }

            }
        };


        const tempTemplate = ({ childNodes: [template] } as any);
        loopNodes(tempTemplate);

        arr.forEach((instance) => {
            instance.init();
        });

        return arr;

    }



    /**
     * parses nodes and creates cache to be used in repeat
     * @param template - template to use
     */
    public static createTemplateCache(template: Node): ITemplateCache[] {

        const arr: ITemplateCache[] = [];

        // loops and pushes into our node map/array
        const loopNodes = function (_node: Node) {

            // loop children
            for (let n = 0; n < _node.childNodes.length; n++) {

                const currentNode = _node.childNodes[n] as Element;
                const instance: ITemplateCache = {
                    type: 'blank',
                    attributes: ([] as any),
                    container: null,
                    gotoChild: false
                };
                arr.push(instance);


                // check controllers first
                let isControllerAttribute = false;
                if (currentNode.getAttribute) {
                    // TODO: need to add option to add own here
                    ['repeat.for', 'if.bind'].forEach((attribute: string) => {
                        if (!isControllerAttribute) { // only 1 controller per element
                            const attributeNode = currentNode.getAttributeNode(attribute);
                            if (attributeNode) {
                                isControllerAttribute = true;
                                const customAttribute = ContainerAttributes.findAttribute(attributeNode.name);
                                instance.attributes.push({
                                    type: 'controller',
                                    name: attribute,
                                    container: customAttribute
                                });
                            }
                        }
                    });
                }



                if (!isControllerAttribute) {

                    const childAttributes: Attr[] = [];
                    const length = currentNode.attributes && currentNode.attributes.length || 0;
                    for (let i = 0; i < length; i++) {
                        childAttributes.push(currentNode.attributes[i]);
                    }

                    for (let i = 0; i < childAttributes.length; i++) {

                        const attributeNode = childAttributes[i];
                        let customAttribute = ContainerAttributes.findAttribute(attributeNode.name);
                        if (!customAttribute && attributeNode.name) {
                            customAttribute = ContainerAttributes.findAttribute(attributeNode.name.replace('.bind', ''));
                            if (!customAttribute && attributeNode.name) {
                                customAttribute = ContainerAttributes.findAttribute(View.getAttributeExpression(attributeNode.name));
                            }
                        }


                        if (customAttribute) {
                            instance.attributes.push({
                                type: 'attribute',
                                name: attributeNode.name,
                                container: customAttribute
                            });
                        } else {
                            // attribute value
                            if (attributeNode.value.indexOf('${') !== -1 || attributeNode.value.indexOf('@{') !== -1) {
                                if (attributeNode.name.indexOf('.bind') === -1) {
                                    instance.attributes.push({
                                        type: 'value',
                                        name: attributeNode.name,
                                        container: null
                                    });
                                }
                            }
                        }
                    }
                }



                if (!isControllerAttribute) {
                    if (currentNode.nodeType === 1) {
                        const customElement = ContainerElements.findElement((<Element>currentNode).localName);
                        if (customElement) {
                            instance.type = 'element';
                            instance.container = customElement;
                        } else {
                            if (currentNode.childNodes) {
                                instance.gotoChild = true;
                                loopNodes(currentNode);
                            }
                        }
                    } else {
                        if ((<any>currentNode).textContent) {
                            if ((<any>currentNode).textContent.indexOf('${') !== -1 || (<any>currentNode).textContent.indexOf('@{') !== -1) {
                                instance.type = 'text';
                            }
                        }
                    }
                }
            }
        };


        const tempTemplate = ({ childNodes: [template] } as any);
        loopNodes(tempTemplate);


        return arr;

    }



    /**
     * Gets custom attibute search expressions form string
     * @param stringAttribute - string attibute to check
     * @internal
     *
     * click.trigger === #VARIABLE#.trigger
     * some-custom-event.bind === #VARIABLE#.bind
     * style@background-color.bind ===  style#VARIABLE#.bind
     * style@background-color === style#VARIABLE#
     *
     * NB! I first check for just value, and just value + '.bind'
     * This is just if 2 two checks fails
     */
    public static getAttributeExpression(stringAttribute: string) {
        const atIndex = stringAttribute.indexOf('@') !== -1 ? stringAttribute.indexOf('@') : 0;
        const dotIndex = stringAttribute.lastIndexOf('.') !== -1 ? stringAttribute.lastIndexOf('.') : stringAttribute.length;
        const toReplace = stringAttribute.substring(atIndex, dotIndex);

        return stringAttribute.replace(toReplace, '#VARIABLE#');
    }


}
