import { customAttribute } from '../decorator/exported';
import { IAttribute, IBindingContext, ITemplateCache } from '../interface/exported';
import { View, AttributeController } from '../view/exported';
import { ViewController } from '../view/exported';
import { BindingEngine } from '../binding/exported';
import { ArrayMethodCallHandler, ArrayPropertyChange, PropertyChangeSimple } from './repeatAttributeSubscriberHelpers';
import { DOM } from '../utils/exported';
import { createBindingContext } from '../binding/createBindingContext';


export interface ITemplateArray {
    template: Node | Element | Node[] | Element[];
    ctx: IBindingContext;
    $view: ViewController;
}

/**
 *  repeat for attribute
 *
 */
@customAttribute('repeat.for')
export class RepeatAttribute implements IAttribute {

    // added by template engine
    public $element: HTMLElement;
    public $attribute: Attr;
    public $bindingContext: IBindingContext;
    public $controller: AttributeController;

    // internal vars
    public value: any;
    public isAttached: boolean;
    private elementClone: Node;
    private anchor: Node;
    public arrayVariableName: string;
    public arrayExpression: string;
    public rowInstanceName: string;
    public arrayMethodCallHandler: ArrayMethodCallHandler;
    public arrayPropertyChangeHandler: ArrayPropertyChange;
    private propertyChangeHandlerSimple: PropertyChangeSimple;
    public templateArray: ITemplateArray[] = [];
    public loopBinded: Function;
    private repeatForArray: string[];
    private $view: ViewController;
    public arrayType = 'object';
    public arrayPropertyChangeHandlerLocal: ArrayPropertyChange;
    public $array: any;
    private templateElement: boolean;
    private cache: ITemplateCache[];



    /**
     * sets local variables in the context of each row
     *
     */
    public setArrayLocalVariables(ctx: any, i: number) {
        ctx.$index = i;
        ctx.$even = i % 2 === 0 ? true : false;
        ctx.$odd = i % 2 === 0 ? false : true;
        ctx.$last = i === this.templateArray.length - 1 ? true : false;
        ctx.$first = i === 0 ? true : false;
    }



    /**
     * created
     *
     */
    public created() {

        this.value = this.$attribute.value;

        this.loopBinded = this.loopArray.bind(this);

        // get view instance, will need this when we create rows
        this.$view = this.$controller.getView();

        // remove repeat attribute, so its not initiated again
        this.$element.attributes.removeNamedItem('repeat.for');

        // clone of our row dom
        this.elementClone = this.$element.cloneNode(true);
        this.templateElement = (this.elementClone as any).tagName === 'TEMPLATE' ? true : false;
        if ((this.elementClone as Element).getAttribute('if.bind') && this.templateElement) {
            // if we have if.bind we need to pass entire template over
            this.templateElement = false;

        }



        // hide it for now, we will remove it when we are attached to parent
        this.$element.style.display = 'None';

        // create dummy element, so viewparser can look at children
        const x = DOM.document.createElement('div'); // this is not needed if template tag (when I add)
        const template = this.elementClone.cloneNode(true);

        if (this.templateElement) {
            // IE11
            if (!(<any>template).content) {
                (<any>template).content = DOM.document.createDocumentFragment();
                while (template.childNodes[0]) {
                    (<any>template).content.appendChild(template.childNodes[0]);
                }
            }
        }
        x.appendChild(this.templateElement ? (<any>template).content : template);

        // anchor so we know where to insert it, also add view to anchor, needed for viewparser to call detached
        this.anchor = DOM.document.createComment('mf-repeat-for');



        // need something better... but good enough for now
        this.repeatForArray = this.value.split(' of ');
        this.repeatForArray = this.repeatForArray.map(x => x.trim());
        if (this.repeatForArray.length !== 2) {
            console.error('unknown expression in repeat:' + this.value);
        } else {
            this.rowInstanceName = this.repeatForArray[0];
            this.arrayVariableName = this.repeatForArray[1];
            this.arrayExpression = this.repeatForArray[1];
            if (this.arrayVariableName.indexOf('|')) {
                const split = this.arrayVariableName.split('|').map(x => x.trim());
                this.arrayVariableName = split[0];
            }
            this.$array = BindingEngine.evaluateExpression(this.arrayExpression, this.$bindingContext);
            const propertyType = this.$array;


            if (!Array.isArray(propertyType)) {
                // not array, this can be a number or a string.
                // do I want to support object or date ?
                // map and set Im not going to bother atm..
                if (typeof propertyType === 'number') {
                    this.arrayType = 'number';
                    this.subscribePropSimple();
                } else {
                    if (typeof propertyType === 'string') {
                        this.arrayType = 'string';
                        this.subscribePropSimple();
                    } else {
                        // console.error('unknown type, only support array, string and number');
                        // object array
                        this.subscribeArray();
                        this.subscribePropArray();
                        // todo, add option for repeat.string, repeat.number
                    }
                }

            } else {
                // object array
                this.subscribeArray();
                this.subscribePropArray();

            }
        }

    }



    /**
     * loops array and updates values/context
     *
     */
    public loopArray(changed?: boolean, remove?: number, add?: number) {
        const array = this.$array;
        if (array) {
            if (changed) {
                if (remove) {
                    const syncLength = this.templateArray.length - remove;
                    for (let i = 0; i < this.templateArray.length; i++) {
                        if (i < syncLength) {
                            if (this.templateArray[i].ctx.$context[this.rowInstanceName] !== array[i]) {
                                this.templateArray[i].ctx.$context[this.rowInstanceName] = array[i];
                            }
                        } else {
                            const temp = this.templateArray.pop();
                            i--; // this isnt very elegant...
                            this.clearInRow(temp);
                        }
                    }
                }
                if (add) {
                    const syncLength = array.length - add;
                    for (let i = 0; i < array.length; i++) {
                        if (i < syncLength) {
                            if (this.templateArray[i].ctx.$context[this.rowInstanceName] !== this.$array[i]) {
                                this.templateArray[i].ctx.$context[this.rowInstanceName] = this.$array[i];
                            }
                        } else {
                            this.push(array[i]);
                        }
                    }
                }
            } else {
                for (let i = 0; i < this.$array.length; i++) {
                    if (this.templateArray[i].ctx.$context[this.rowInstanceName] !== array[i]) {
                        this.templateArray[i].ctx.$context[this.rowInstanceName] = array[i];
                    }
                }
            }
            this.updateInternals();
        }
    }


    /**
     * loops array/string and updates values/context
     *
     */
    public loopArrayNumber(changed?: boolean, remove?: number, add?: number) {
        const num = this.templateArray.length + 1;
        if (num) {
            if (changed) {
                if (remove) {
                    const syncLength = this.templateArray.length - remove;
                    for (let i = 0; i < this.templateArray.length; i++) {
                        if (i >= syncLength) {
                            const temp = this.templateArray.pop();
                            i--; // this isnt very elegant...
                            this.clearInRow(temp);
                        }
                    }
                }
                if (add) {
                    const syncLength = num;
                    for (let i = 0; i < num + add; i++) {
                        if (i >= syncLength) {
                            this.push(i);
                        }
                    }
                }
            } else {
                for (let i = 0; i < this.$array; i++) {
                    if (this.templateArray[i].ctx.$context[this.rowInstanceName] !== i) {
                        this.templateArray[i].ctx.$context[this.rowInstanceName] = i;
                    }
                }
            }
            this.updateInternals();
        }
    }



    /**
     * detached, unsubscribes events
     *
     */
    public detached() {


        this.clearTemplateArray();


        if (this.arrayMethodCallHandler) {
            BindingEngine.unSubscribeClassArray(this.$bindingContext, this.arrayMethodCallHandler);
        }


        if (this.arrayPropertyChangeHandler) {
            BindingEngine.unSubscribeClassProperty(this.$bindingContext, this.arrayPropertyChangeHandler);
        }


        if (this.arrayPropertyChangeHandlerLocal) {
            BindingEngine.unSubscribeClassProperty(this.$bindingContext, this.arrayPropertyChangeHandlerLocal);
        }

        if (this.propertyChangeHandlerSimple) {
            BindingEngine.unSubscribeClassProperty(this.$bindingContext, this.propertyChangeHandlerSimple);
        }


        if (this.anchor) {
            this.anchor.parentNode.removeChild(this.anchor);
            this.anchor = null;
        }

        this.$view = null;
        this.$array = null;
        this.arrayMethodCallHandler = null;
        this.arrayPropertyChangeHandler = null;
        this.arrayPropertyChangeHandlerLocal = null;
        this.propertyChangeHandlerSimple = null;

    }



    /**
     * attached, update rows
     *
     */
    public attached() {

        // replace dom with anchor
        this.remove();

        // var to set state
        this.isAttached = true;
        this.$array = BindingEngine.evaluateExpression(this.arrayExpression, this.$bindingContext);
        const array = this.$array;
        if (Array.isArray(array)) {
            array.forEach((ctx: any) => {
                this.push(ctx);
            });
        } else {
            if (this.arrayType === 'number') {
                if (this.templateArray.length !== array) {
                    this.clearTemplateArray();
                    for (let i = 0; i < array; i++) {
                        this.push(i + 1);
                    }
                }
            }
            if (this.arrayType === 'string') {
                const stringLength = typeof array === 'string' ? array.length : 0;
                if (this.templateArray.length !== stringLength) {
                    this.clearTemplateArray();
                    for (let i = 0; i < stringLength; i++) {
                        this.push(array[i]);
                    }
                }
            }
        }
        this.updateInternals();

    }



    /**
     * subscribe array in parent
     *
     */
    public subscribeArray() {
        if (!this.arrayMethodCallHandler) {
            this.arrayMethodCallHandler = new ArrayMethodCallHandler(this);
        }
        BindingEngine.subscribeClassArray(this.$bindingContext, this.arrayVariableName, this.arrayMethodCallHandler);
    }



    /**
     * subscribe array prop in parent
     *
     */
    public subscribePropArray() {
        if (!this.arrayPropertyChangeHandler) {
            this.arrayPropertyChangeHandler = new ArrayPropertyChange(this);
        } else {
            console.error('subscribePropArray fail, called when set', this.arrayExpression, this.arrayVariableName);
        }
        BindingEngine.subscribeClassProperty(this.$bindingContext, this.arrayVariableName, this.arrayPropertyChangeHandler);

        if (this.arrayVariableName !== this.arrayExpression) {
            // if expression is used we need to have a local handler
            // why local handler ?
            // - if we dont have a local handler we will be editing the main array, we need this for later
            // - if we do filter on main array and set it to the filtered array then we lose the original data
            if (!this.arrayPropertyChangeHandlerLocal) {
                this.arrayPropertyChangeHandlerLocal = new ArrayPropertyChange(this);
            }
            // replace original expression variable name with local "$array" used in this class
            // this will be the array we use for data
            const classKey = this.arrayExpression.replace(this.arrayVariableName, '$array');
            BindingEngine.subscribeClassProperty(createBindingContext(this), classKey, this.arrayPropertyChangeHandlerLocal);
        }

    }



    /**
     * subscribe array in parent
     *
     */
    public subscribePropSimple() {
        if (!this.propertyChangeHandlerSimple) {
            this.propertyChangeHandlerSimple = new PropertyChangeSimple(this);
        }
        BindingEngine.subscribeClassProperty(this.$bindingContext, this.arrayVariableName, this.propertyChangeHandlerSimple);
    }



    /**
     * pushes new row
     *
     */
    public push(ctx: any, i?: number) {


        const template = this.elementClone.cloneNode(true);
        if (this.templateElement) {
            // IE11
            if (!(<any>template).content) {
                (<any>template).content = DOM.document.createDocumentFragment();
                while (template.childNodes[0]) {
                    (<any>template).content.appendChild(template.childNodes[0]);
                }
            }
        }

        // parse node for custom elements and attributes with context object
        const context = createBindingContext({ [this.rowInstanceName]: ctx }, createBindingContext(this.$bindingContext, this.$bindingContext));



        // append new child
        const temp = DOM.document.createElement('div');
        temp.appendChild(this.templateElement ? (<any>template).content : template);
        const $view = new ViewController(template, this.$view);
        if (!this.cache) {
            this.cache = View.createTemplateCache(temp);
        }

        const controllers = View.parseTemplateCache(temp, context, $view, this.cache);

        let childNodes: undefined | any[];
        if (this.templateElement) {
            childNodes = [];
            let anchor = DOM.document.createComment('repeat-template-row-anchor-start');
            childNodes.push(anchor);

            for (let i = 0, ii = temp.childNodes.length; i < ii; i++) {
                childNodes.push(temp.childNodes[i]);
            }

            anchor = DOM.document.createComment('repeat-template-row-anchor-end');
            childNodes.push(anchor);
        }

        if (i === undefined) {
            const length = this.templateArray.length;
            this.templateArray.push({
                ctx: context,
                template: this.templateElement ? childNodes : template,
                $view: $view
            });

            this.setArrayLocalVariables(context.$context, this.templateArray.length - 1);
            if (this.templateElement) {
                if (length) {
                    const u = (this.templateArray[length - 1].template as Element[] | Node[]).length - 1;
                    this.anchor.parentNode.insertBefore(childNodes[0], this.templateArray[length - 1].template[u].nextSibling);
                    for (let i = 1, ii = childNodes.length; i < ii; i++) {
                        this.anchor.parentNode.insertBefore(childNodes[i], childNodes[i - 1].nextSibling);
                    }
                } else {
                    this.anchor.parentNode.insertBefore(childNodes[0], this.anchor.nextSibling);
                    for (let i = 1, ii = childNodes.length; i < ii; i++) {
                        this.anchor.parentNode.insertBefore(childNodes[i], childNodes[i - 1].nextSibling);
                    }
                }
            } else {
                if (length) {
                    this.anchor.parentNode.insertBefore(template, (this.templateArray[length - 1].template as Element | Node).nextSibling);
                } else {
                    this.anchor.parentNode.insertBefore(template, this.anchor.nextSibling);
                }
            }


        } else {
            this.templateArray.splice(i, 0, {
                ctx: context,
                template: this.templateElement ? childNodes : template,
                $view: $view
            });
            if (this.templateElement) {
                if (this.templateArray[i + 1]) {
                    const u = (this.templateArray[length - 1].template as Element[] | Node[]).length - 1;
                    this.anchor.parentNode.insertBefore(childNodes[0], this.templateArray[length - 1].template[u].nextSibling);
                    for (let i = 1, ii = childNodes.length; i < ii; i++) {
                        this.anchor.parentNode.insertBefore(childNodes[i], childNodes[i - 1].nextSibling);
                    }
                } else {
                    this.anchor.parentNode.insertBefore(childNodes[0], this.anchor.nextSibling);
                    for (let i = 1, ii = childNodes.length; i < ii; i++) {
                        this.anchor.parentNode.insertBefore(childNodes[i], childNodes[i - 1].nextSibling);
                    }
                }
            } else {
                if (this.templateArray[i + 1]) {
                    this.anchor.parentNode.insertBefore(template, (this.templateArray[i + 1].template as Element | Node));
                } else {
                    this.anchor.parentNode.appendChild(template);
                }
            }

        }


        // call attached
        controllers.forEach((contr: any) => {

            // if it have attached method
            if (contr.attached) {
                contr.attached();
            }
        });

    }



    /**
     * pops row
     *
     */
    public pop() {
        if (this.templateArray.length > 0) {
            const temp = this.templateArray.pop();
            this.clearInRow(temp);
            this.updateInternals();
        }
    }


    public updateInternals() {
        this.templateArray.forEach((context: any, i: number) => {
            this.setArrayLocalVariables(context.ctx.$context, i);
        });
    }

    /**
     * shift row
     *
     */
    public shift() {
        if (this.templateArray.length > 0) {
            const temp = this.templateArray.shift();
            this.clearInRow(temp);
            this.updateInternals();
        }
    }



    /**
     * splice row
     *
     */
    public splice(args: any) {
        if (this.templateArray.length > 0) {
            const index = args[0];
            const deleted = args[0] === 0 && args[1] === undefined ? this.templateArray.length : args[1];
            const added = [];
            for (let i = 2; i < args.length; i++) {
                if (args[i]) {
                    added.push(args[i]);
                }
            }

            if (deleted) {
                const newIndex = index + deleted - 1;
                for (let i = newIndex; i > index - 1; i--) {
                    const temp = this.templateArray.splice(i, 1)[0];
                    this.clearInRow(temp);
                }
            }

            added.forEach((ctx) => {
                this.push(ctx, index);
            });
            this.updateInternals();
            // get our queue
        }
    }



    /**
     * clear template array
     *
     */
    public clearTemplateArray() {
        // create temp div
        let x = DOM.document.createElement('div');
        this.templateArray.forEach((temp: any) => {
            if (this.templateElement) {
                for (let i = 0, ii = temp.template.length; i < ii; i++) {
                    x.appendChild(temp.template[i]);
                }
            } else {
                x.appendChild(temp.template);
            }

        });

        // now clear all views
        this.templateArray.forEach((temp: any) => {
            temp.$view.clearView();
        });

        // clear all rows
        x = null;

        // reset template array
        this.templateArray = [];
    }


    /**
     * remove all
     *
     */
    private remove() {
        // clear if any
        this.clearTemplateArray();

        // my repeat fail fast if it have no parent, could this happend?
        this.$element.parentNode.replaceChild(this.anchor, this.$element);

    }


    /**
     * clear 1 row
     *
     */
    private clearInRow(rowData: any) {
        if (rowData) {

            if (this.templateElement) {
                for (let i = 0, ii = rowData.template.length; i < ii; i++) {
                    rowData.$view.clearView();
                    if (rowData.template[i].parentNode) {
                        rowData.template[i].parentNode.removeChild(rowData.template[i]);
                    }
                }
            } else {
                if (rowData.template.parentNode) {
                    rowData.template.parentNode.removeChild(rowData.template);
                }
                rowData.$view.clearView();
            }



        }
    }

}
