/// <reference path="leaf.ts" />
namespace Leaf {
    interface FeedInfo<TSignalEnum> {
        feedListener?: { (): void }
        signals: any[]
        name: TSignalEnum
    }
    interface Feeds<TSignalEnum> {
        [id: string]: FeedInfo<TSignalEnum>
    }
    export type TStateDefaultEnum = "void" | "panic"
    export class States<TStateEnum, TSignalEnum extends string>{
        public events = new Leaf.EventEmitter<string>()
        public state: TStateEnum | TStateDefaultEnum = "void"
        public previousState: TStateEnum | TStateDefaultEnum
        private sole: number = 1
        private soleEmitted: number = 1
        private lastException: Error = null
        private rescues = []
        protected data: any = {}
        private feeds: Feeds<TSignalEnum> = {} as Feeds<TSignalEnum>
        private isDebugging: boolean = false
        private clearHandler: { (): void } = null
        public readonly name: string
        public panicError: Error = null
        public panicState: TStateEnum | TStateDefaultEnum = null
        constructor() {
            this.reset()
        }
        public reset() {
            this.data = {}
            this.feeds = {}
            this.respawn()
            this.events.emit("reset")
        }
        public setState(state: TStateEnum | TStateDefaultEnum, ...args: any[]): void {
            this._setState(state, ...args)
        }
        private _stateFinish() {
            this.events.emit("state", this.state)
            this.events.emit("state/" + this.state)
            this.soleEmitted = this.sole
            this._updateStateAssertion(this.state as TStateEnum)
        }
        protected clearConsumers() {
            if (this.feeds) {
                for (let prop in this.feeds) {
                    let feed = this.feeds[prop]
                    feed.feedListener = null
                }
            }
        }
        private _setState(state: TStateEnum | TStateDefaultEnum, ...args: any[]) {
            if (this.state === "panic" && state !== "void") {
                return
            }
            // set state syncly from previous state
            // so we emit the previous state here
            // That is, all state/${state} is guaranteed to be emitted
            // after the state finish and before the next state start.
            if (this.soleEmitted < this.sole) {
                this._stateFinish()
            }
            // clear all consumers
            this.clearConsumers()

            // new sole
            this.sole += 1
            this.previousState = this.state
            this.state = state
            if (this.isDebugging) {
                this.debugStateHandler()
            }
            let stateHandler = "at" + state[0].toUpperCase() + (state as string).substring(1)
            if (this[stateHandler]) {
                let sole = this.sole
                this[stateHandler](function () {
                    return sole !== this.sole
                }.bind(this), ...args)
                // didn't set state during the process, we can safely emit state here
                if (sole === this.sole) {
                    this._stateFinish()
                }
            } else if (state !== "void") {
                this.error(new Error(`InvalidState ${state}`))
            }
        }
        public error(err: Error) {
            this.panicError = err
            this.panicState = this.state
            if (this.panicError) {
                this.setState("panic")
            }
        }
        protected atPanic() {
            console.error(this.panicState, this.panicError)
            this.events.emit("panic", this.panicError, this.panicState)
        }
        public recover(recoverState?: TStateEnum | TStateDefaultEnum) {
            let error = this.panicError
            let state = this.panicState
            this.respawn()
            if (!recoverState) {
                recoverState = ("void")
            }
            this.setState(recoverState)
            return { error, state }
        }
        public respawn() {
            this.sole = this.sole || 1
            this.sole += 1
            this.panicError = null
            this.panicState = null
            this.setState("void")
            this.clear()
        }
        public clear(handler?: { (): void }) {
            if (handler) {
                if (this.clearHandler) {
                    throw new Error("Already clearing with a handler")
                }
                this.clearHandler = handler
            } else {
                handler = this.clearHandler
                this.clearHandler = null
                if (handler) {
                    handler()
                }
            }
        }
        private ensureFeed(name): FeedInfo<TSignalEnum> {
            if (this.feeds[name]) return this.feeds[name];
            let feed = this.feeds[name] = {
                feedListener: null,
                signals: [],
                name: name
            }
            return feed
        }
        public feed(name: TSignalEnum, signal?: any) {
            if (!this.feeds[name as string]) {
                this.feeds[name as string] = {
                    feedListener: null,
                    signals: [],
                    name: name
                }
            }
            let feed = this.feeds[name as string]
            feed.signals.push(signal)
            let listener = feed.feedListener
            if (listener) {
                feed.feedListener = null
                listener()
            }
        }
        public consumeAll(name: string): number {
            let feed = this.feeds[name]
            if (feed) {
                let length = feed.signals.length
                feed.signals.length = 0
                return length
            }
            return 0
        }
        public hasFeed(name: string): boolean {
            return this.feeds[name] && this.feeds[name].signals.length > 0
        }
        public consume(name: string): boolean | any {
            if (!this.hasFeed(name)) return null
            let feed = this.feeds[name]
            if (feed) {
                return feed.signals.shift() || true
            }
            return null
        }
        public consumeWhenAvailableMergeToLast(name: TSignalEnum, callback: { (signal: any): void }) {
            this.consumeWhenAvailable(name, (detail) => {
                let last = null
                while (true) {
                    last = this.consume(name)
                    if (!last) break;
                }
                if (last) {
                    callback(last)
                } else {
                    callback(detail)
                }
            })
        }
        public consumeWhenAvailable(name: TSignalEnum, callback: { (signal: any): void }) {
            let feed = this.feeds[name as string]
            if (!feed) {
                feed = this.ensureFeed(name)
            }
            if (feed.signals.length > 0) {
                callback(this.consume(name))
            } else {
                feed.feedListener = () => callback(this.consume(name))
                this.events.emit("starve", name)
                this.events.emit("starve/" + name)
            }

        }
        public debug(option?: any) {
            this.isDebugging = true
        }
        private debugStateHandler(option?: { name?: string }, message?: string) {
            option = option || {}
            let name = option.name || this.name || "Anonymous"
            let log = console.debug || console.log
            log.call(console, `${name}: state - ${this.state}`)
        }
        private _assertingStateSequence: TStateEnum[] = null
        private _assertingStateCallback: Function = null
        private _assertingStateIndex = 0
        public assertStateSequence(states: TStateEnum[], callback: (err: Error) => void) {
            if (this._assertingStateSequence) {
                callback(new Error("Already asserting"))
                return
            }
            this._assertingStateSequence = states
            this._assertingStateCallback = callback
            this._assertingStateIndex = 0
        }
        private _endStateAssertion(err?: Error) {
            let cb = this._assertingStateCallback
            if (!cb) return
            this._assertingStateSequence = null
            this._assertingStateIndex = 0
            this._assertingStateCallback = null
            cb(err)

        }
        private _updateStateAssertion(state: TStateEnum) {
            if (!this._assertingStateSequence) return
            let should = this._assertingStateSequence[this._assertingStateIndex]
            if (should !== state) {
                this._endStateAssertion(new Error(`State at index ${this._assertingStateIndex} should be ${should} but is ${state} during sequence ${this._assertingStateSequence.map((s, index: number) => { if (index === this._assertingStateIndex) return "->" + s; return s; }).join("\n")}`))
            }
        }
    }
}
