/// <reference path="../../Elements/Classes/Say.ts" />
/// <reference path="Relations/RelationOneToMany.ts" />
/// <reference path="../../Elements/Classes/Say/SayImage.ts" />

interface ThingOptions {
    name? : string;
    properName? : string;
    description? : Say | any;
    unique? : boolean;
    image? : SayImage | string;
    shiny? : boolean;
}

// A thing is something that exists in the World
class Thing implements Printable {
    protected name : string;
    public properlyNamed : boolean = false;

    public scenery : boolean = false;
    public fixedInPlace : boolean = false;
    public animated : boolean = false;
    public visible : boolean = true;
    public unique : boolean = false;
    public image : SayImage;
    protected shiny : boolean = false;

    protected setAlterations : Array<(thisObject : Thing, simpleAlterationObject : {[id : string] : any}) => void> = [];
    protected getAlterations : Array<(thisObject : Thing) => {[id : string] : any}> = [];
    public clone : (includeChanges? : boolean) => Thing = function () {
        throw new Error("Non-unique Objects can't be cloned.");
    };
    public cloneOptions : ThingOptions;

    public description : Say | string | (() => Say | string);

    public static uniqueThings : {[id : string] : Thing} = {};
    public static things : {[id : string] : Array<Thing>} = {};

    // This is not safe.
    // public duplicate () {
    //     return new (<any> this.constructor)(<ThingOptions> {
    //         name : this.name,
    //         properName : this.properlyNamed ? this.name : undefined,
    //         description : this.description,
    //         unique : this.unique,
    //         image : this.image
    //     });
    // }

    public constructor (options? : ThingOptions) {
        options = options == undefined ? {} : options;

        if (options.properName != undefined) {
            this.name = options.properName;
            this.properlyNamed = true;
        } else if (options.name != undefined) {
            this.name = options.name;
        } else {
            this.name = (<any> this.constructor).name; // If there is no name, use Class as name
        }

        if (options.description != undefined) {
            if (options.description instanceof Say) {
                this.description = options.description;
            } else {
                this.description = new Say(options.description);
            }
        }

        if (options.unique) {
            Thing.storeUnique(this);
            this.unique = true;
        } else {
            Thing.storeNonUnique(this);
            this.cloneOptions = options;
            this.clone = (includeChanges? : boolean) => {
                let cons = <any> eval((<any> this.constructor).name);
                let newThing = <Thing> new cons(this.cloneOptions);

                if (includeChanges == undefined || includeChanges) {
                    newThing.setChanges(this.getChanges());
                }

                return newThing;
            };
        }

        if (options.image != undefined) {
            if (options.image instanceof SayImage) {
                this.image = options.image;
            } else {
                this.image = new SayImage(options.image);
            }
        }

        this.shiny = options.shiny == true;

        this.addGetAlterations((thing : Thing) => {
            function getClosestRoom (currentRoom : RoomRandom, rooms : Array<RoomRandom>) {
                if (currentRoom instanceof RoomRandom && rooms.length > 0) {
                    rooms.sort((a : RoomRandom, b : RoomRandom) => {
                        if (!(a instanceof RoomRandom)) return -1;
                        if (!(b instanceof RoomRandom)) return 1;
                        let da = a.getDistanceTo(<RoomRandom> currentRoom);
                        let db = b.getDistanceTo(<RoomRandom> currentRoom);
                        return da - db;
                    });
                    return {
                        Location : rooms[0].getName()
                    }
                }
            }

            if (Thing.EnclosedRelation.getLeft(thing) == thing.getRoom() && thing.getRoom() != undefined) {
                if (thing.getRoom().fodder) {
                    if (thing.isPlayer()) {
                        // put at closest remembered room
                        let rooms = WorldState.getRememberedRoomsAsRooms();
                        let currentRoom = thing.getRoom();
                        return getClosestRoom(<RoomRandom> currentRoom, <Array<RoomRandom>> rooms);
                    } else {
                        // put at closest room
                        let rooms = thing.getRoom().getConnectedRooms();
                        let currentRoom = thing.getRoom();
                        let foundRoom = getClosestRoom(<RoomRandom> currentRoom, <Array<RoomRandom>> rooms);
                        if (foundRoom != undefined) {
                            return foundRoom;
                        } else {
                            rooms = (<Region> Region.InRelation.getLeft(thing.getRoom())).getRooms();
                            return getClosestRoom(<RoomRandom> currentRoom, <Array<RoomRandom>> rooms);
                        }
                    }
                } else {
                    return {
                        Location: thing.getRoom().getName()
                    }
                }
            }
        });

        this.addSetAlterations((thing : Thing, changes) => {
            if (changes.Location != undefined) {
                let room = Room.getRoom(changes.Location);
                if (room != undefined) {
                    room.place(thing);
                } else {
                    console.error("Unable to place ", thing, " at room ", changes.Location);
                }
            }
        });
    }

    public addGetAlterations (newGet) {
        this.getAlterations.push(newGet);
    }

    /**
     * This adds a function to run over when loading from a save file.
     * Always remember that save files are NOT SAFE. Ideally we should check for invalid information,
     * but at least check if they exist, because if you added something new, old saves will not have them.
     * It's okay to break on bad information because if someone decided to hack their save, them they should
     * deal with the issues.
     * @param newSet
     */
    public addSetAlterations (newSet) {
        this.setAlterations.push(newSet);
    }

    public getChanges () : {[id : string] : any}{
        let changes = {};
        for (let i = 0; i < this.getAlterations.length; i++) {
            let change = this.getAlterations[i](this);
            for (let key in change) {
                changes[key] = change[key];
            }
        }
        return changes;
    }

    public setChanges (simpleAlterationObject : {[id : string] : any}) {
        for (let i = 0; i < this.setAlterations.length; i++) {
            this.setAlterations[i](this, simpleAlterationObject);
        }
    }

    public getShiny () {
        return this.shiny;
    }

    public setName (name : string) {
        this.name = name; // Don't restore. The only thing that changes names is player.
    }

    public getName () {
        return this.name;
    }

    public static storeNonUnique (thing : Thing) {
        if (Thing.things[thing.name] == undefined) {
            Thing.things[thing.name] = [thing];
        } else {
            Thing.things[thing.name].push(thing);
        }
    }

    public static getNonUnique (name : string) : Array<Thing> {
        return Thing.things[name] == undefined ? [] : Thing.things[name];
    }

    public static getOneThing (name : string) {
        let thing : Thing = Thing.getUnique(name);
        if (thing == undefined) {
            let things = Thing.getNonUnique(name);
            if (things.length > 0) {
                thing = things[0];
            }
        }
        return thing;
    }

    public static storeUnique (unique : Thing) {
        if (Thing.uniqueThings[unique.name] != undefined) {
            console.warn(unique.name, Thing.uniqueThings[unique.name], new Error("Unique Thing Already Exists"));
        } else {
            Thing.uniqueThings[unique.name] = unique;
        }
    }

    public static getUnique (name : string) {
        return Thing.uniqueThings[name];
    }

    public static getUniques () : Array<Thing> {
        let things = [];
        for (let name in Thing.uniqueThings) {
            things.push(Thing.uniqueThings[name]);
        }
        return things;
    }

    public getPrintedName() {
        return this.name;
    }

    public getPrintedDescription () {
        if (this.description == undefined) {
            return new Say ("You see nothing special about ", new SayThe(), this, ".");
        } else {
            return this.description;
        }
    }

    public static InsideRoomRelation : RelationOneToMany= new RelationOneToMany();
    public static PartRelation = new RelationOneToMany();
    public static CarryRelation = new RelationOneToMany();
    public static WieldRelation = new RelationOneToMany();
    public static WearRelation = new RelationOneToMany();
    public static EnclosedRelation = new RelationHandlerStrictOneToMany(Thing.InsideRoomRelation, Thing.PartRelation, Thing.CarryRelation, Thing.WieldRelation, Thing.WearRelation);

    public getPartOne () {
        return Thing.PartRelation.getLeft(this);
    }

    public getCarryOne () {
        return Thing.CarryRelation.getLeft(this);
    }

    public getWieldOne () {
        return Thing.WieldRelation.getLeft(this);
    }

    public getWearOne () {
        return Thing.WearRelation.getLeft(this);
    }

    public getEnclosedOne () : Thing | Room {
        return Thing.EnclosedRelation.getLeft(this);
    }

    public removeParts (partType? : any) {
        let parts = this.getParts(partType);
        for (let i = 0; i < parts.length; i++) {
            Thing.PartRelation.unsetRight(parts[i]);
        }
    }

    public getParts (partType? : any) {
        if (partType != undefined) {
            return Thing.PartRelation.getRightType(this, partType);
        }
        return Thing.PartRelation.getRight(this);
    }

    public getPartsByName (name : string) : Array<Thing> {
        let parts = this.getParts();
        return parts.filter((part : Thing) => {
            return (part.getName() == name);
        });
    }

    public getPart (partType? : any) {
        if (partType != undefined) {
            return Thing.PartRelation.getRightTypeOne(this, partType);
        }
        return Thing.PartRelation.getRight(this);
    }

    // Will usually return the room at which this is.
    // Will return the highest parent if that parent is out of world.
    public getHighestEnclosedOne () : Thing | Room {
        return Thing.EnclosedRelation.getLastLeft(this);
    }

    public getHighestEnclosedOneNotRoom () : Thing {
        let parent = Thing.EnclosedRelation.getLeft(this);
        if (parent != undefined) {
            let newParent = Thing.EnclosedRelation.getLeft(parent);
            while (newParent != undefined) {
                parent = newParent;
                newParent = Thing.EnclosedRelation.getLeft(parent);
            }
            return parent;
        }
        return this;
    }

    public getRoom () : Room {
        var partOf = <Thing | Room> Thing.EnclosedRelation.getLeft(this);
        if (partOf instanceof Room) {
            return partOf;
        } else if (partOf instanceof Thing) {
            return partOf.getRoom();
        }
    }

    public removeFromRoom () {
        this.getRoom().remove(this);
    }

    public isVisibleTo (thing : Thing) {
        // Either in the same room or part of /wielded / carried by person
        return (this.getRoom() == thing.getRoom() && this.visible);
        // TODO : Update this code for the cases where a creature is not visible but the observer can see them anyway
    }

    public isPlayer () {
        return false;
    }

    public addParts (...parts : Array<Thing>) {
        parts.forEach(part => {
            Thing.PartRelation.setRelation(this, part);
        });
    }

    public isUnique () {
        return this.unique;
    }

    public setCarried (thing : Thing) {
        Thing.CarryRelation.setRelation(this, thing);
    }

    public setWorn (thing : Thing) {
        Thing.WearRelation.setRelation(this, thing);
    }

    public setWielded (thing : Thing) {
        Thing.WieldRelation.setRelation(this, thing);
    }

    public unsetCarried (thing : Thing) {
        if (Thing.EnclosedRelation.getAllRight(this).indexOf(thing) != -1) {
            Thing.EnclosedRelation.unsetRight(thing);
            this.getRoom().place(thing);
        }
    }

    /**
     * So long as a thing is in a relation, it can't be garbage cleaned.
     * If you're throwing a thing away, do run destroy.
     * Warning: this will also destroy everything that's below it.
     */
    public destroy () {
        let relatedRight = Thing.EnclosedRelation.getAllRight(this);
        relatedRight.push(this);
        relatedRight.forEach(related => {
            Thing.EnclosedRelation.unset(related);
        });
    }
}