/// <reference path="leaf.ts" />
namespace Leaf {
    export class Widget<TUIs extends { [index: string]: HTMLElement }, TVMs, TVMField extends string> {
        // Global namespace
        static namespace = new Namespace()
        static events = new EventEmitter<"create">()
        // Use global namespace by default
        public namespace = Widget.namespace
        // Set custom template for this module
        static setTemplate(template: WidgetTemplate) {
            this.prototype.template = template
        }
        // VM data short cut and ViewModels
        public ListItemData: any
        public VM: TVMs
        public ViewModel: ViewModel<TVMs, TVMField>
        // If direct parent widget that contains this widget
        public parent: WidgetAny = null
        // By default every widget has a event emitter
        public events: EventEmitter<string> = new EventEmitter<string>()
        // All element with data-id
        public UI: TUIs = {} as TUIs
        // root of the current element
        public node: WidgetElement
        public templates: { [index: string]: string }
        // Custom DOM events that this widget may trigger
        public providingEvents: DOMEventInfo[] = []
        // Event that we should delegate (bubble or not)
        public interestingUnbubblingEvents: DOMEventInfo[] = Leaf.CoreLogic.UnbubbleEvents.slice(0)
        public interestingBubblingEvents: DOMEventInfo[] = Leaf.CoreLogic.BubbleEvents.slice(0)
        // Name of this widget
        public readonly name: string
        public readonly core: WidgetCoreDesign = new WidgetCore(this)
        public readonly delegates: WidgetDOMEventDelegator = new WidgetDOMEventDelegator(this)
        public readonly initializer: WidgetInitializerDesign = new WidgetInitializer(this)
        public toString() {
            return `<Widget ${this.name}>`
        }
        public before(who: WidgetAny | HTMLElement): boolean {
            let node: HTMLElement
            if (who instanceof Widget) {
                node = who.node
            } else {
                node = who
            }
            if (!node.parentElement) return false
            node.parentElement.insertBefore(this.node, node)
            return true
        }
        public after(who: WidgetAny | HTMLElement): boolean {
            let node: HTMLElement
            if (who instanceof Widget) {
                node = who.node
            } else {
                node = who
            }
            if (!node.parentElement) return false
            if (node.nextSibling) {
                node.parentElement.insertBefore(this.node, node.nextSibling)
            } else {
                node.parentElement.appendChild(this.node)
            }
            return true
        }
        public prependTo(who: WidgetAny | HTMLElement): boolean {
            let node: HTMLElement
            if (who instanceof Widget) {
                node = who.node
            } else {
                node = who
            }
            if (node.children.length === 0) {
                node.appendChild(this.node)
            } else {
                let first = node.children[0]
                node.insertBefore(this.node, first)
            }
            return true
        }
        public appendTo(who: WidgetAny | HTMLElement): boolean {
            let node: HTMLElement
            if (who instanceof Widget) {
                node = who.node
            } else {
                node = who
            }
            node.appendChild(this.node)
            return true
        }
        constructor(public template?: WidgetTemplate, name?: string, public _VMFields?: TVMField[]) {
            if (name) {
                this.name = this.name || ((this as any).constructor.name) || "AnonymouseWidget"
            }
            // Setup ViewModals
            if (!this._VMFields) {
                this._VMFields = []
            }
            this.ViewModel = new ViewModel<TVMs, TVMField>(this.name, this._VMFields)
            Property.define(this, "VM", {
                get: () => {
                    return this.ViewModel.data
                }
                , set: (values: any) => {
                    return this.ViewModel.sets(values)
                }
            })
            // If we don't have a template, dont initialize it
            // Maybe we are generated widget, or maybe we gonna do it latter.
            this.initializer.initTemplate(template)
            Widget.events.emit("create", this)
        }
        static makeList<TListItem extends ListItem>(el: HTMLElement) {
            return new Leaf.List<TListItem>(el)
        }
        protected emitDOMEvent(name: string, bubble: boolean, props: any = {}) {
            let valid = this.providingEvents.some((info) => {
                return info.name === name
            })
            if (!valid) {
                throw new Error(`Should add events ${name}to available events at construtor`)
            }
            let e = new CustomEvent(name, {
                bubbles: bubble,
                cancelable: true
            })
            for (let prop in props) {
                e[prop] = props[prop]
            }
            this.node.dispatchEvent(e)
        }
        protected emitWidgetEvent(name: string, value: any = {}) {
            this.events.emit(name, value)
            this.emitDOMEvent(name, false, value)
        }
        protected include(W) {
            this.namespace.include(W)
        }
        protected expose(name: string, remoteName?: string) {
            if (!remoteName) {
                remoteName = name
            }
            if (typeof this[name] === "function") {
                Object.defineProperty(this.node, remoteName, {
                    get: () => {
                        return this[name].bind(this)
                    }
                })
                return
            }
            let cap = Util.capitalize(name)
            let getterName = `onGet${cap}`
            let setterName = `onSet${cap}`
            Object.defineProperty(this.node, remoteName, {
                get: () => {
                    if (this[getterName])
                        return this[getterName]()
                    else
                        return this[name]
                },
                set: (value) => {
                    if (this[setterName])
                        return this[setterName](value)
                    else
                        return this[name]
                }
            })
        }
        protected makeList<T extends ListItem>(who: string | HTMLElement) {
            let el: HTMLElement
            let name: string
            if (typeof who === "string") {
                el = this.UI[who]
                name = who
            } else {
                for (let prop in this.UI) {
                    if (this.UI[prop] === who as HTMLElement) {
                        el = who
                        name = prop
                        break
                    }
                }
                if (!name) {
                    return
                }
            }
            let list = Leaf.Widget.makeList<T>(el)
            list.events.listenBy(this, "child/add", (item) => {
                let fn = this[`onChildAdd${Leaf.Util.capitalize(name)}`]
                if (fn) {
                    fn.call(this, item)
                }
            })
            list.events.listenBy(this, "child/remove", (item) => {
                let fn = this[`onChildRemove${Leaf.Util.capitalize(name)}`]
                if (fn) {
                    fn.call(this, item)
                }
            })
            if (!this[name]) {
                this[name] = list
            }
            return list
        }
    }
    export abstract class GeneratedWidget<TUIs extends { [index: string]: HTMLElement }, TVMs, TVMField extends string> extends Widget<TUIs, TVMs, TVMField>{
        constructor(private generator: Function, name: string, public _VMFields?: TVMField[]) {
            super(null, name, _VMFields)
            this.initGeneratedTemplate(generator.call(this))
        }
        public GeneratedConstructor: WidgetConstructor
        public BindedWidgets: {
            path: string
            name: string
        }[] = []
        public BindedLists: {
            name: string
            type: "inline" | "reference" | "void" | string
            reference: string
        }[] = []
        public TestDatas: {
            name: string
            content: string
            json: any
        }[] = []
        public InitialData: any = null
        initGeneratedTemplate(node: WidgetElement) {
            this.node = node
            this.core.bindUI(this.node, "node")
            if (!this.node.widget) {
                // in case it's a shared node by it's child
                // the child will own it
                this.node.widget = this
            }
        }
        protected _e(tagName: string, properties: ElementConfigProperties, children: WidgetElement[]): WidgetElement {
            return this.initializer.createElement(tagName, properties, children)
        }
        protected _t(rule: DynamicStringConfig): Text {
            return this.initializer.createText(rule)
        }
        renderRecursive(data: any = {}, R: any = this["R"] || {}) {
            for (let widget of this.BindedWidgets) {
                if (!this[widget.name]) {
                    let Cons = this.resolveWidgetByPath(R, widget.path)
                    if (!Cons) {
                        console.error("Invalid constructor", widget.path)
                        continue
                    }
                    this[widget.name] = new Cons()
                }
                if (data[widget.name] && this[widget.name]["renderRecursive"]) {
                    this[widget.name]["renderRecursive"](data[widget.name])
                }
            }
            for (let list of this.BindedLists) {
                if (this[list.name] && data[list.name] instanceof Array) {
                    let Cons = this.resolveWidgetByPath(R, list.reference)
                    if (!Cons) continue
                    for (let sub of data[list.name]) {
                        let child: WidgetAny = new Cons()
                        this[list.name].push(child)
                        if (child["renderRecursive"]) {
                            child["renderRecursive"](sub)
                        } else {
                            child.VM = sub
                        }
                    }
                }
            }
            for (let name of this._VMFields) {
                if (data[name as string]) {
                    this.VM[name as string] = data[name as string]
                }
            }
        }
        private resolveWidgetByPath(R: any, path: string): StandaloneWidgetConstructor {
            if (!path) return null
            let routes = path.split("/").filter(item => item)
            let cur = R
            for (let route of routes) {
                route = Leaf.Util.capitalize(route)
                if (!cur[route]) return null
                cur = cur[route]
            }
            return cur
        }
    }
    export interface StandaloneWidgetConstructor {
        new (): WidgetAny;
    }
    export interface WidgetConstructor {
        new (el: HTMLElement): WidgetAny;
    }
    export type WidgetAny = Widget<any, any, any>
}
