/// <reference path="../../Elements/Classes/Say.ts" />
enum Direction {
    NORTH, NORTHEAST, EAST,
    SOUTHEAST, SOUTH, SOUTHWEST,
    WEST, NORTHWEST, UP, DOWN
}



var DirectionNames : {[id: string] : string} = (() => {
    enum names {
        NORTH = "North",
        SOUTH = "South",
        EAST = "East",
        WEST = "West",
        SOUTHEAST = "Southeast",
        NORTHWEST = "Northwest",
        SOUTHWEST = "Southwest",
        NORTHEAST = "Northeast",
        UP = "Down",
        DOWN = "Up"
    };
    let obj : {[id: string] : string} = {};
    for (let name in names) {
        obj[name] = names[name];
        obj[Direction[name]] = names[name];
    }
    return obj;
})();

var OppositeDirection : {[id : number] : Direction} = (() => {
    let obj = {};
    obj[Direction.NORTH] = Direction.SOUTH;
    obj[Direction.SOUTH] = Direction.NORTH;
    obj[Direction.EAST] = Direction.WEST;
    obj[Direction.WEST] = Direction.EAST;
    obj[Direction.SOUTHEAST] = Direction.NORTHWEST;
    obj[Direction.NORTHWEST] = Direction.SOUTHEAST;
    obj[Direction.SOUTHWEST] = Direction.NORTHEAST;
    obj[Direction.NORTHEAST] = Direction.SOUTHWEST;
    obj[Direction.UP] = Direction.DOWN;
    obj[Direction.DOWN] = Direction.UP;

    // Make it work with the name too
    for (let i = 0; i < Object.keys(Direction).length / 2; i++) {
        obj[Direction[i]] = obj[i];
    }
    return obj;
})();

class Room implements Printable {
    private name : string;
    public connections : Array<Room>;
    public description : Say = new Say();
    public fodder : boolean;

    public constructor (id? : string, fodder? : boolean) {
        this.name = id == undefined ? "Room" : id;
        this.connections = new Array(Room.DIRECTIONS.length); // Array the same size as directions, but filled with undefined

        this.fodder = fodder;
        if (fodder != true) {
            Room.addRoom(this);
        }
    }

    public getName () {
        return this.name;
    }

    public place (thing : Thing) {
        Thing.InsideRoomRelation.setRelation(this, thing);
    }

    public remove (thing : Thing) {
        // Don't remove stuff from other rooms
        if (Thing.InsideRoomRelation.getLeft(thing) == this) {
            Thing.InsideRoomRelation.unsetRight(thing);
        }
    }

    public getContained () : Array<Thing> {
        return <Array<Thing>> Thing.InsideRoomRelation.getRight(this);
    }

    public getContainedAndVisibleTo (observer : Thing) : Array<Thing> {
        let contained = this.getContained();
        let result = [];

        contained.forEach((value) => {
            if (value.visible && value !== observer) {
                result.push(value);
            }
        });

        return result;
    }

    public static DIRECTIONS : Array<Direction> = (() => {
        let directions : Array<Direction> = [];
        for (let i = 0; i < Object.keys(Direction).length / 2; i++) {
            directions.push(i);
        }
        return directions;
    })();

    public getContainedAndVisible () : Array<Thing> {
        return this.getContainedAndVisibleTo(WorldState.player);
    }

    public mapRoom (r : Room, direction : Direction) {
        let oppositeDirection = OppositeDirection[direction];
        if (this.connections[direction] != undefined) {
            console.warn("Replacing a connected room.", this, " connected through ", direction, " to ", this.connections[direction]);
            this.connections[direction].unmapRoom(oppositeDirection);
        }
        this.connections[direction] = r;

        if (r.connections[oppositeDirection] != undefined) {
            console.warn("Replacing a connected room.", r, " connected through ", oppositeDirection, " to ", r.connections[oppositeDirection]);
            r.unmapRoom(oppositeDirection);
        }
        r.connections[oppositeDirection] = this;
    }

    public unmapRoom (direction : Direction) {
        if (this.connections[direction] != undefined) {
            let r = this.connections[direction];
            this.connections[direction] = undefined;
            r.unmapRoom(OppositeDirection[direction]);
        }
    }

    public getPrintedName () {
        return this.name;
    }

    public getConnectedRooms () : Array<Room> {
        let rooms = [];
        this.connections.forEach(room => {
            if (room != undefined) {
                rooms.push(room);
            }
        });
        return rooms;
    }

    /**
     * This returns the best direction to follow if going from the current room to another room.
     * THIS CODE IS REALLY EXPENSIVE AND SHOULD ONLY BE USED AS A LAST RESORT
     * WARNING: THIS CODE WILL FAIL IF THE TARGET ROOM IS TOO FAR FROM THE CURRENT ROOM (FOR SECURITY REASONS).
     * This code runs through every. single. room. to find the best route to take.
     * If you need an NPC to stick to a region, don't let it go out of it in the first place!
     * @param room
     * @param validityCode
     * @returns {any}
     */
    // TODO: Make this shit fast
    public bestDirectionTo (room : Room, validityCode? : (room : Room) => boolean) {
        if (validityCode == undefined) validityCode = () => {return true};

        /**
         * Maximum amount of steps that will be considered for a route.
         * This is useful to prevent the code from spanning too many rooms, as each room can have Room.DIRECTIONS.length directions to make another call, etc.
         *
         * @type {number}
         */
        let maxSteps = 10;
        maxSteps = maxSteps > WorldState.getMaximumRememberedRooms() ? maxSteps : WorldState.getMaximumRememberedRooms();

        let recursiveBestPath = (cPath : Array<Room>, cRoom : Room, destination : Room) => {
            // Ignore "bad" rooms
            if (!validityCode(cRoom)) {
                return;
            }
            // Prevent the code from straying too far
            if (cRoom == destination) {
                maxSteps = maxSteps > cPath.length ? cPath.length : maxSteps; // Stop considering worse routes
                return cPath;
            } else if (cPath.length > maxSteps) {
                return undefined;
            } else {
                let paths = [];
                for (let index in Room.DIRECTIONS) {
                    let direction = Room.DIRECTIONS[index];
                    let nextRoom = cRoom.connections[direction];
                    if (nextRoom != undefined && cPath.indexOf(nextRoom) == -1 && validityCode(nextRoom)) {
                        let path = recursiveBestPath(cPath.concat([nextRoom]), nextRoom, destination);
                        if (path != undefined) {
                            paths.push(path);
                        }
                    }
                }
                let shortestIndex = 0;
                paths.forEach((value, index, array) => {
                    if (value.length < paths[shortestIndex].length) {
                        shortestIndex = index;
                    }
                });
                return paths[shortestIndex];
            }
        };

        let paths = Array(Room.DIRECTIONS.length);
        let shortestIndex;
        for (let index in Room.DIRECTIONS) {
            let direction = Room.DIRECTIONS[index];
            let nextRoom = this.connections[direction];
            if (nextRoom != undefined) {
                paths[direction] = recursiveBestPath([this, nextRoom], nextRoom, room);
                if (paths[direction] != undefined && (shortestIndex == undefined || paths[shortestIndex].length > paths[direction].length)) {
                    shortestIndex = direction;
                }
            }
        }
        return shortestIndex;
    }

    public static getDirectionXYZ (direction : Direction) {
        var y = 0;
        if ([Direction.NORTH, Direction.NORTHEAST, Direction.NORTHWEST].indexOf(direction) != -1) {
            y = 1;
        } else if ([Direction.SOUTH, Direction.SOUTHEAST, Direction.SOUTHWEST].indexOf(direction) != -1) {
            y = -1;
        }

        var x = 0;
        if ([Direction.EAST, Direction.SOUTHEAST, Direction.NORTHEAST].indexOf(direction) != -1) {
            x = 1;
        } else if ([Direction.WEST, Direction.SOUTHWEST, Direction.NORTHWEST].indexOf(direction) != -1) {
            x = -1;
        }

        var z = direction == Direction.UP ? 1 :
                 direction == Direction.DOWN ? -1 :
                 0;

        return [x, y, z];
    }
    public static shift (coordinates : Array<number>, direction : number) {
        let coordinatesVector = Room.getDirectionXYZ(direction);
        coordinates.forEach((value, index, array) => {
            coordinatesVector[index] += coordinates[index];
        });
        return coordinatesVector;
    }

    protected static rooms : {[id : string] : Room} = {};

    protected static addRoom (room : Room) {
        Room.rooms[room.name] = room;
    }

    public static getRooms () : Array<Room> {
        let rooms = [];
        for (let name in Room.rooms) {
            rooms.push(Room.rooms[name]);
        }
        return rooms;
    }

    public static getRoom (id : string) {
        return Room.rooms[id];
    }
}