/// <reference path="../../World/Classes/Rulebook.ts" />
/// <reference path="../../World/Classes/Rule.ts" />
/// <reference path="Say/OneOf.ts" />
/// <reference path="Say/SayImage.ts" />

interface Printable {
    getPrintedName () : string;
}

interface PrintableElement {
    getPrintedElement () : Array<Element>;
}

interface SayNoun {
    say : Say;
    noun : any;
}

class SayableObject {}

class Say {
    // TODO: Separate own sequence from processing queue. This way a Say with functions/other says can be reutilized with fresh values.
    public sequence : Array <Object> = [];
    public skipbreaks : boolean = false;

    public static LINE_BREAK : Object = new SayableObject();
    public static PARAGRAPH_BREAK : Object = new SayableObject();
    public static RUN_PARAGRAPH : Object = new SayableObject();
    public static RUN_PARAGRAPH_OFF : Object = new SayableObject();
    public static CENTERED : Object = new SayableObject();
    public static b : Object = new SayableObject();
    public static DO_PARAGRAPH_BREAK = new SayableObject();
    public static DO_LINE_BREAK = new SayableObject();
    public static COCK = new SayableObject();
    public static PUSSY = new SayableObject();

    private centered : boolean = false;

    public constructor (...objs) {
        this.add(...objs);
    }

    public add (...objs : Array<Say | OneOf | Object | Printable | string | number | String | ((say : Say) => string)>) {
        this.sequence.push(...objs);
    }

    public remove (...objs) {
        for (let i = 0; i < objs.length; i++) {
            let index = this.sequence.indexOf(objs[i]);
            if (index >= 0) {
                this.sequence.splice(index, 1);
            }
        }
    }

    public isEmpty () {
        return this.sequence.length < 1;
    }

    public paragraphs : Array<Array<Element | Text>>;
    public currentParagraph : Array<Element | Text>;
    public sequenceRunner : number;

    // TODO: Create a single function to get the element of anything
    public async getTextOf (index : number, seq : any) : Promise<string> {
        let elements = await this.getElementFor(index, seq);

        let div = document.createElement("div");
        for (let i = 0; i < elements.length; i++) {
            if (typeof elements[i] != "number") {
                div.appendChild(elements[i]);
            }
        }
        return div.innerText;
    }

    public doLineBreak () {
        if (this.currentParagraph.length > 0 && !this.skipbreaks) {
            let br = document.createElement("br");
            br.classList.add("linebreak");
            let ti = document.createElement("span");
            ti.classList.add("textIndenter");
            this.currentParagraph.push(br, ti);
        }
    }

    public doParagraphBreak () {
        if (this.currentParagraph.length > 0 && !this.skipbreaks) {
            this.paragraphs.push(this.currentParagraph);
            this.currentParagraph = [];
        }
    }

    public async getParagraphs () : Promise<Array<Array<Element|Text>>> {
        this.paragraphs = [];
        this.currentParagraph = [];
        this.skipbreaks = false;

        for (this.sequenceRunner = 0; this.sequenceRunner < this.sequence.length; this.sequenceRunner++) {
            let seq = this.sequence[this.sequenceRunner];
            if (seq instanceof OneOf) {
                seq = seq.getOne();
            }

            if (seq == Say.CENTERED) {
                this.setCentered(true);
            } else if (seq == Say.b) {
                let boldObjects = [];
                for (let i = this.sequenceRunner + 1; i < this.sequence.length; i++) {
                    let candidate = this.sequenceRunner[i];
                    if (candidate == Say.b) {
                        this.sequence.splice(i, 1);
                        break;
                    } else {
                        boldObjects.push(this.sequence.splice(i, 1));
                    }
                }
                if (boldObjects.length > 0) {
                    let bold = new SayBold(...boldObjects);
                    this.sequence.splice(this.sequenceRunner + 1, 0, bold);
                }
            } else if (seq == Say.COCK) {
                if (HumanoidPenis != undefined) {
                    let cock = HumanoidPenis.getSynonym();
                    this.currentParagraph.push(document.createTextNode(cock))
                }
            }  else if (seq == Say.PUSSY) {
                if (HumanoidVagina != undefined) {
                    let vagina = HumanoidVagina.getSynonym();
                    this.currentParagraph.push(document.createTextNode(vagina))
                }
            } else if (seq == Say.LINE_BREAK) {
                this.doLineBreak();
            } else if (seq == Say.PARAGRAPH_BREAK) {
                this.doParagraphBreak();
            } else if (seq == Say.RUN_PARAGRAPH) {
                this.skipbreaks = true;
            } else if (seq == Say.RUN_PARAGRAPH_OFF) {
                this.skipbreaks = false;
            } else if (typeof seq == "function") {
                let fObj = (<(s: Say) => any> seq)(this);
                if (Array.isArray(fObj)) {
                    for (let k = fObj.length - 1; k >= 0; k--) {
                        this.sequence.splice(this.sequenceRunner + 1, 0, fObj[k]);
                    }
                } else if (fObj != undefined) {
                    this.sequence.splice(this.sequenceRunner + 1, 0, fObj);
                }
                this.sequence.splice(this.sequenceRunner, 1);
                this.sequenceRunner--;
            } else if (seq.constructor == this.constructor) {
                for (let k = (<Say> seq).sequence.length - 1; k >= 0; k--) {
                    this.sequence.splice(this.sequenceRunner + 1, 0, (<Say> seq).sequence[k]);
                }
                this.sequence.splice(this.sequenceRunner, 1);
                this.sequenceRunner--;
            } else if (seq != undefined) {
                let elements = await this.getElementFor(this.sequenceRunner, seq);
                for (let i = 0; i < elements.length; i++) {
                    if (elements[i] === Say.DO_LINE_BREAK) {
                        this.doLineBreak();
                    } else if (elements[i] === Say.DO_PARAGRAPH_BREAK) {
                        this.doParagraphBreak();
                    } else {
                        this.currentParagraph.push(elements[i]);
                    }
                }
            }
        }

        // TODO: Remove line break + text indenter if they are the last in the say
        if (this.currentParagraph.length > 0) {
            this.paragraphs.push(this.currentParagraph);
        }

        return this.paragraphs;
    }

    /**
     * Lord Have mercy, I wish to never have to debug this piece of god.
     * @param {number} index
     * @param {Say | OneOf | Object | Printable | string | number | ((say: Say) => string) | ((say: Say) => Promise<string>) | Element | Text} obj
     * @returns {Promise<Array<Element | Text>>}
     */
    public async getElementFor (index : number, obj : Say | OneOf | Object | Printable | string | number | String | ((say : Say) => string) | ((say : Say) => Promise<string>) | Element | Text) : Promise<Array<Element|Text>> {
        if (obj instanceof Promise) {
            obj = await obj;
        }
        if (typeof obj == "string" || obj instanceof String) {
            return [document.createTextNode(<string> obj)];
        } else if (typeof obj == "number" || obj instanceof Number) {
            return [document.createTextNode((parseFloat((<number> obj).toFixed(2))/1).toString())];
        } else if (typeof obj == "function") {
            let elements = await this.getElementFor(-1, (<any> obj)(this));
            return elements;
        } else if (obj instanceof SayImage) {
            return [obj.getImageElement()];
        } else if (obj instanceof SayLeftRight) {
            return (await obj.getPureElements());
        } else if (obj instanceof Say) {
            let elements = await obj.getPureElements(this);
            return elements;
        } else if (this.isProperElement(obj)) {
            return [<Element> obj];
        } else if (obj instanceof Object) {
            let elements = await this.printName(obj);
            return elements;
        }
    }

    public async getPureElements (say? : Say) : Promise<Array<Element | Text>> {
        let paragraphs = await this.getParagraphs();
        return paragraphs.length == 1 ? paragraphs[0] : Array.prototype.concat.apply([], paragraphs);
    }

    public setCentered (bool : boolean) {
        this.centered = bool;
    }

    public async getHTML (tagName : string, classList : Array<string>, singleParagraph? : boolean) : Promise<Array<HTMLElement>> {
        let paragraphs = await this.getParagraphs();

        // Reduce to single paragraph
        if (singleParagraph == true && paragraphs.length > 1) {
            paragraphs = [Array.prototype.concat.apply([], paragraphs)];
        }

        let elements = [];


        for (let i = 0, paragraph = paragraphs[i]; paragraph != undefined; paragraph = paragraphs[++i]) {
            let parent = <HTMLElement> document.createElement(tagName);
            if (classList.length > 0) {
                parent.classList.add(...classList);
            }

            for (let k = 0, ele = paragraph[k]; ele!= undefined; ele = paragraph[++k]) {
                parent.appendChild(ele);
            }
            elements.push(parent);

            if (this.centered) {
                parent.classList.add("centered");
            }
        }

        return elements;
    }

    public getHTMLContent () : Promise<Array<HTMLElement>> {
        return this.getHTML("p", ["content"]);
    }

    public isProperElement (o) : boolean {
        return (
            typeof Node === "object" ? o instanceof Node :
                o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName==="string"
        ) || (
            typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
                o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName==="string"
        );
    }

    public static beforePrinting = new Rulebook<any>("Before printing the name of something");
    public static printing = new Rulebook<any>("Printing the name of something");
    public static afterPrinting = new Rulebook<any>("After printing the name of something");

    public currentNoun : any;
    public currentNounElements : Array<Element | Text>;

    public async printName (thing : any) : Promise<Array<Element | Text>> {
        this.currentNoun = thing;
        this.currentNounElements = [];
        let before = Say.beforePrinting.execute({noun : this});
        await before;
        let print = Say.printing.execute({noun : this});
        await print;
        let after = Say.afterPrinting.execute({noun : this});
        await after;
        return this.currentNounElements;
    }

    public static hisHersIts (target : Thing, startOfSentence? : boolean) {
        return new SayHisHersIts(target);
        // let result : String;
        // if (target == WorldState.player) {
        //     result = "your";
        // } else if (target instanceof Person) {
        //     // TODO: Figure out whether target is male or female
        //     result = "his";
        // } else {
        //     result = "its";
        // }
        // if (startOfSentence == true) {
        //     result = result.charAt(0).toUpperCase() + result.substr(1, result.length -1);
        // }
        // return result;
    }
}

Say.printing.addRule(new Rule({
    name : "Printing the name of a Printable Element",
    firstPriority : Rule.PRIORITY_LOW,
    code : (rulebook : RulebookRunner<any>) => {
        let say = <Say> rulebook.noun;
        if ((<any> say.currentNoun).getPrintedElement) {
            say.currentNounElements.push(...(<PrintableElement> say.currentNoun).getPrintedElement());
            return true; // We only want to print something once, so return true to stop others from running
        }
    }
}));

Say.printing.addRule(new Rule({
    name : "Printing the name of a Printable",
    firstPriority : Rule.PRIORITY_LOW,
    code : (rulebook : RulebookRunner<any>) => {
        let say = <Say> rulebook.noun;
        if ((<any> say.currentNoun).getPrintedName) {
            let thingEle = document.createTextNode(
                (<Printable> say.currentNoun).getPrintedName()
            );
            say.currentNounElements.push(thingEle);
            return true; // We only want to print something once, so return true to stop others from running
        }
    }
}));

Say.printing.addRule(new Rule({
    name : "Printing the name of an unknown object",
    firstPriority : Rule.PRIORITY_LOWEST,
    priority : Rule.PRIORITY_LOWEST,
    code : (rulebook : RulebookRunner<any>) => {
        let say = <Say> rulebook.noun;
        if ((<any> say.currentNoun).getPrintedName) {
            say.currentNounElements.push(
                (document.createTextNode((<Object> say.currentNoun).toString()))
            );
            return true; // We only want to print something once, so return true to stop others from running
        }
    }
}));

// var msg = new Say ("Hello! Welcome to The Obelisk! This is a game with ", johnTheOgre, " so you must be careful!");
//
// var otherSay = new Say (msg, "Will have to learn how to handle dots.");