/// <reference path="leaf.ts" />
/// <reference path="widget.ts" />
namespace Leaf {
    export class List<TListItem extends ListItem> extends Widget<any, any, any> {
        [index: number]: TListItem
        ListItemConstructor: WidgetConstructor
        constructor(el: HTMLElement) {
            super(el, "List", [])
            let arrayFnsNames = ["map", "forEach", "slice", "every", "some", "filter"]
            for (let name of arrayFnsNames) {
                this[name] = (...args) => {
                    let arr = this.toArray()
                    return arr[name].apply(arr, args)
                }
            }
            Object.defineProperty(this, "length", {
                get: () => {
                    return this._length
                },
                set: (v: number) => {
                    let toRemove: TListItem[] = []
                    if (v > this._length) {
                        throw new Error("can't asign length larger than the orig")
                    }
                    if (typeof v != "number") {
                        throw new TypeError()
                    }
                    for (let i = v; i < this.length; i++) {
                        toRemove.push(this[i])
                        delete this[i]
                    }
                    this._length = v
                    for (let item of toRemove) {
                        this.detach(item)
                    }
                }
            })
            this._length = 0
        }
        public fill(datas: any[], Cons: WidgetConstructor) {
            this.length = 0
            for (let item of datas) {
                let widget = new Cons(item)
                for (let name of widget._VMFields) {
                    if (typeof item[name] !== "undefined") {
                        widget.VM[name] = item[name]
                    }
                }
                widget.ListItemData = item
                this.push(widget as TListItem)
            }
        }
        public events: Leaf.EventEmitter<"child/add" | "child/change" | "child/remove">
        private _length: number = 0
        public length: number = 0
        public forEach: (callbackFn: (item: TListItem, index?: number, array?: TListItem[]) => void) => void
        public some: (callbackFn: (item: TListItem, index?: number, array?: TListItem[]) => boolean) => boolean
        public every: (callbackFn: (item: TListItem, index?: number, array?: TListItem[]) => boolean) => boolean
        public map: (callbackFn: (item: TListItem, index?: number, array?: TListItem[]) => any) => any[]
        public filter: (callbackFn: (item: TListItem, index?: number, array?: TListItem[]) => boolean) => TListItem[]
        public slice: (start?: number, end?: number) => TListItem[]
        public indexOf(item: TListItem): number {
            for (let i = 0; i < this.length; i++) {
                if (this[i] === item) {
                    return i
                }
            }
            return -1
        }
        private check(item: TListItem) {
            if (!(item instanceof Widget)) {
                throw new Error("List expect item to be Widget")
            }
            for (let i = 0; i < this.length; i++) {
                if (this[i] === item) {
                    throw new Error("Duplicate ListItem")
                }
            }
            return true
        }
        public push(...items: TListItem[]) {
            for (let item of items) {
                this.check(item)
                this[this._length] = item
                if (this._length != 0) {
                    item.after(this[this._length - 1])
                } else {
                    item.appendTo(this.node)
                }
                this._length += 1
                this.attach(item)
            }
        }
        public pop(): TListItem {
            if (this._length === 0) return null
            this._length -= 1
            let item = this[this._length]
            delete this[this._length]
            return item
        }
        public unshift(item: TListItem): number {
            this.check(item)
            if (this._length === 0) {
                item.prependTo(this.node)
                this[0] = item
                this._length = 1
                this.attach(item)
                return
            }
            for (let index = this._length; index > 0; index -= 1) {
                this[index] = this[index - 1]
            }
            this[0] = item
            this._length += 1
            item.before(this[1])
            this.attach(item)
            return this._length
        }
        public shift(): TListItem {
            let result = this[0]
            for (let index = 0; index < this.length; index++) {
                this[index] = this[index + 1]
            }
            this.length -= 1
            this.detach(result)
            return result
        }
        public splice(index: number, count?: number, ...toAdd: TListItem[]) {
            let result: TListItem[] = []
            let toRemoves: TListItem[] = []
            if (typeof count != "number" || index + count > this._length) {
                count === this._length - index
            }
            for (let i = 0; i < count; i++) {
                let item = this[index + i]
                toRemoves.push(item)
                result.push(item)
            }
            let toAddFinal = toAdd
            let frag = document.createDocumentFragment()
            toAddFinal.forEach((item) => {
                this.check(item)
                frag.appendChild(item.node)
            })
            if (index < this.length && this.length > 0) {
                this.node.insertBefore(frag, this[index].node)
            } else {
                this.node.appendChild(frag)
            }
            // make a hole
            let increase = toAddFinal.length - count
            if (increase < 0) {
                for (let origin = index + count; origin < this._length; origin++) {
                    this[origin + increase] = this[origin]
                }
            } else if (increase > 0) {
                for (let origin = this._length - 1; origin > index + count - 1; origin--) {
                    this[origin + increase] = this[origin]
                }
            }
            // fill the hole
            for (let offset = 0; offset < toAddFinal.length; offset++) {
                let item = toAddFinal[offset]
                this[index + offset] = item
            }
            this._length += increase
            for (let item of toRemoves) {
                this.detach(item)
            }
            for (let item of toAddFinal) {
                this.attach(item)
            }
            return result
        }
        public removeItem(item: TListItem) {
            let index = this.indexOf(item)
            if (index < 0) return
            this.splice(index, 1)
            return item
        }
        private attach(item: TListItem) {
            item["parentList"] = this
            this.events.emit("child/add", item)
            this.events.emit("child/change")
        }
        private detach(item: TListItem) {
            item["parentList"] = null
            let node = item.node
            if (node && node.parentElement === this.node) {
                this.node.removeChild(node)
            }
            this.events.emit("child/remove", item)
            this.events.emit("child/change")
        }
        public toArray(): TListItem[] {
            let result: TListItem[] = []
            for (let i = 0; i < this.length; i++) {
                result.push(this[i])
            }
            return result
        }
    }
    export interface ListItem extends Widget<any, any, any> {
    }
}
