import { TiledLayerData, TiledTilemapData } from "../../Wolfie2D/DataTypes/Tilesets/TiledData";
import Vec2 from "../../Wolfie2D/DataTypes/Vec2";
import MapTemplate, { Entrance, RoomTemplate } from "./DataTypes/MapTemplate";

export default class RandomMapGenerator {
    private template: MapTemplate;
    private minX: number;
    private maxX: number;
    private minY: number;
    private maxY: number;
    private map: TiledTilemapData;
    private roomWithLeftEntrance: Array<RoomTemplate>;
    private roomWithLeftEntranceWeight: number;
    private roomWithRightEntrance: Array<RoomTemplate>;
    private roomWithRightEntranceWeight: number;
    private roomWithUpEntrance: Array<RoomTemplate>;
    private roomWithUpEntranceWeight: number;
    private roomWithDownEntrance: Array<RoomTemplate>;
    private roomWithDownEntranceWeight: number;
    private rooms: Array<Room>
    private gen: any;
    private hasExit: boolean;
    private minRoom: number;
    private roomPlaced: number;
    private exitFacing: Facing;

    constructor(JSONFilePath: string, seed: any) {
        let xhr = new XMLHttpRequest();
        xhr.overrideMimeType("application/json");
        xhr.open('GET', JSONFilePath, false);
        xhr.send(null);
        this.template = JSON.parse(xhr.responseText);
        this.roomWithLeftEntrance = new Array();
        this.roomWithRightEntrance = new Array();
        this.roomWithUpEntrance = new Array();
        this.roomWithDownEntrance = new Array();
        this.minX = this.minY = this.maxX = this.maxY =
            this.roomWithLeftEntranceWeight = this.roomWithRightEntranceWeight =
            this.roomWithUpEntranceWeight = this.roomWithDownEntranceWeight = 0;

        this.map = new TiledTilemapData();
        this.rooms = new Array();
        let gen = require('random-seed');
        this.gen = new gen(seed);
        this.hasExit = false;
        this.minRoom = this.template.minroom;
        this.roomPlaced = 0;
        this.exitFacing = this.getEntranceFacing(this.template.exit.entrances[0], this.template.exit.width);


        this.template.rooms.forEach((room) => {
            let left = false, right = false, up = false, down = false;
            room.entrances.forEach((entrance) => {
                let facing = this.getEntranceFacing(entrance, room.width);
                switch (facing) {
                    case Facing.LEFT:
                        left = true;
                        break;
                    case Facing.RIGHT:
                        right = true;
                        break;
                    case Facing.UP:
                        up = true;
                        break;
                    case Facing.DOWN:
                        down = true;
                        break;
                    default:
                        break;
                }
            })
            if (left) {
                this.roomWithLeftEntrance.push(room);
                this.roomWithLeftEntranceWeight += room.weight;
            }
            if (right) {
                this.roomWithRightEntrance.push(room);
                this.roomWithRightEntranceWeight += room.weight;
            }
            if (up) {
                this.roomWithUpEntrance.push(room);
                this.roomWithUpEntranceWeight += room.weight;
            }
            if (down) {
                this.roomWithDownEntrance.push(room);
                this.roomWithDownEntranceWeight += room.weight;
            }
        })
    }

    getMap(): TiledTilemapData {
        let room = this.copyRoom(this.template.entrance, 0, 0);
        this.rooms.push(room);

        let facing = this.getEntranceFacing(this.template.entrance.entrances[0], this.template.entrance.width);
        let position = new Vec2(this.template.entrance.entrances[0].x, this.template.entrance.entrances[0].y);

        this.putNextRoom(position, this.getOppositeFacing(facing));

        if (!this.hasExit)
            throw new Error("Fail to generate a map with exit!");

        this.fillData();
        return this.map;
    }

    private putNextRoom(position: Vec2, facing: Facing): boolean {
        switch (facing) {
            case Facing.LEFT:
                position.x += 1;
                break;
            case Facing.RIGHT:
                position.x -= 1;
                break;
            case Facing.UP:
                position.y += 1;
                break;
            case Facing.DOWN:
                position.y -= 1;
                break;
            default:
                break;
        }
        if (this.roomPlaced >= this.minRoom && facing == this.exitFacing) {
            this.putExitRoom(position);
            return true;
        }

        let nextRoom = this.getRandomRoom(facing);
        let nextPosition: Vec2 = undefined;
        let thisEntrance: Entrance = undefined;
        for (let index = 0; index < nextRoom.entrances.length; index++) {
            const entrance = nextRoom.entrances[index];
            if (this.getEntranceFacing(entrance, nextRoom.weight) == facing) {
                let tmpPosition = new Vec2(position.x - entrance.x, position.y - entrance.y);
                if (this.isValidRoom(tmpPosition, new Vec2(tmpPosition.x + nextRoom.width - 1, tmpPosition.y + nextRoom.height - 1))) {
                    thisEntrance = entrance;
                    nextPosition = tmpPosition;
                }
            }
        }
        if (!thisEntrance) {
            return false;
        }
        let room = this.copyRoom(nextRoom, nextPosition.x, nextPosition.y);
        this.rooms.push(room);
        this.roomPlaced += 1;
        if (this.hasExit && this.gen.range() <= 0.1) {
            return false;
        }
        for (let index = 0; index < nextRoom.entrances.length; index++) {
            const entrance = nextRoom.entrances[index];
            if (entrance != thisEntrance) {
                let facing = this.getEntranceFacing(entrance, nextRoom.width);
                let position = new Vec2(nextPosition.x + entrance.x, nextPosition.y + entrance.y);

                if (this.putNextRoom(position, this.getOppositeFacing(facing))) {
                    this.removeEntrance(room, entrance, facing);
                }
            }
        }
        return true;
    }

    private putExitRoom(position: Vec2): void {
        position = new Vec2(position.x - this.template.exit.entrances[0].x, position.y - this.template.exit.entrances[0].y);
        if (!this.isValidRoom(position, new Vec2(position.x + this.template.exit.width - 1, position.y + this.template.exit.height - 1))) {
            throw new Error("Cannot put exit room!!! Position is invalid!!! Please check order of entrances in map template.");
        }
        let room = this.copyRoom(this.template.exit, position.x, position.y);
        this.rooms.push(room);
        this.hasExit = true;
    }

    private removeEntrance(room: Room, entrance: Entrance, facing: Facing): void {
        let width = room.bottomRight.x - room.topLeft.x + 1;
        if (facing == Facing.LEFT || facing == Facing.RIGHT) {
            for (let index = 0; index < entrance.width; index++)
                room.topLayer[(entrance.y + index) * width + entrance.x] = 0;
            if (entrance.y > 0)
                room.topLayer[(entrance.y - 1) * width + entrance.x] = entrance.alt_tile[0];
            if (entrance.y + entrance.width <= (room.bottomRight.y - room.topLeft.y))
                room.topLayer[(entrance.y + entrance.width) * width + entrance.x] = entrance.alt_tile[1];
        }
        else {
            for (let index = 0; index < entrance.width; index++)
                room.topLayer[(entrance.y) * width + entrance.x + index] = 0;
            if (entrance.x > 0)
                room.topLayer[(entrance.y) * width + entrance.x - 1] = entrance.alt_tile[0];
            if (entrance.x + entrance.width <= (room.bottomRight.x - room.topLeft.x))
                room.topLayer[(entrance.y) * width + entrance.x + entrance.width] = entrance.alt_tile[1];
        }
    }

    private fillData() {
        let width = this.maxX - this.minX + 1;
        let height = this.maxY - this.minY + 1;
        this.map.layers = new Array(2);
        this.map.layers[0] = new TiledLayerData;
        this.map.layers[1] = new TiledLayerData;
        this.map.width = this.map.layers[0].width = this.map.layers[1].width = width;
        this.map.height = this.map.layers[0].height = this.map.layers[1].height = height;
        this.map.tileheight = this.template.tileheight;
        this.map.tilewidth = this.template.tilewidth;
        this.map.orientation = "orthogonal";
        this.map.layers[0].x = this.map.layers[0].y = this.map.layers[1].x = this.map.layers[1].y = 0;
        this.map.layers[0].opacity = this.map.layers[1].opacity = 1;
        this.map.layers[0].visible = this.map.layers[1].visible = true;
        this.map.layers[0].type = this.map.layers[1].type = "tilelayer";
        this.map.layers[0].name = "Floor";
        this.map.layers[1].name = "Wall";
        this.map.layers[0].properties = [{
            name: "Collidable",
            type: "bool",
            value: false
        }]
        this.map.layers[1].properties = [{
            name: "Collidable",
            type: "bool",
            value: true
        }]
        this.map.tilesets = [{
            columns: this.template.columns,
            tilewidth: this.template.tilewidth,
            tileheight: this.template.tileheight,
            tilecount: this.template.tilecount,
            firstgid: this.template.firstgid,
            imageheight: this.template.imageheight,
            imagewidth: this.template.imagewidth,
            margin: this.template.margin,
            spacing: this.template.spacing,
            name: this.template.name,
            image: this.template.image
        }]

        this.map.layers[0].data = new Array(width * height).fill(this.template.background);
        this.map.layers[1].data = new Array(width * height);

        for (let index = 0; index < this.rooms.length; index++) {
            const room = this.rooms[index];
            let roomWidth = room.bottomRight.x - room.topLeft.x + 1;
            let roomHeight = room.bottomRight.y - room.topLeft.y + 1;
            for (let i = 0; i < roomHeight; i++)
                for (let j = 0; j < roomWidth; j++) {
                    this.map.layers[0].data[(room.topLeft.y + i) * width + room.topLeft.x + j] = room.bottomLayer[i * roomWidth + j];
                    this.map.layers[1].data[(room.topLeft.y + i) * width + room.topLeft.x + j] = room.topLayer[i * roomWidth + j];
                }
        }
    }

    private isValidRoom(topLeft: Vec2, bottomRight: Vec2): boolean {
        for (let index = 0; index < this.rooms.length; index++) {
            const room = this.rooms[index];
            if (room.topLeft.x <= bottomRight.x &&
                room.bottomRight.x >= topLeft.x &&
                room.topLeft.y <= bottomRight.y &&
                room.bottomRight.y >= topLeft.y)
                return false;
        }
        console.warn("Found an invalid room! TopLeft:", topLeft.toString, "BottomRight:", bottomRight.toString);
        return true;
    }

    private getEntranceFacing(entrance: Entrance, width: number): Facing {
        if (entrance.x === 0)
            return Facing.LEFT;
        else if (entrance.x === width - 1)
            return Facing.RIGHT
        else if (entrance.y === 0)
            return Facing.UP;
        return Facing.DOWN;
    }

    private getOppositeFacing(facing: Facing): Facing {
        switch (facing) {
            case Facing.LEFT:
                return Facing.RIGHT;
            case Facing.RIGHT:
                return Facing.LEFT;
            case Facing.UP:
                return Facing.DOWN;
            case Facing.DOWN:
                return Facing.UP;
        }
    }

    private getRandomRoom(facing: Facing): RoomTemplate {
        let array = this.getRoomArray(facing), weight = this.getRoomWeight(facing);
        let value = this.gen(weight);

        if (value >= weight)
            throw new Error("Random number " + value + " is larger than total weight " + weight);

        for (let index = 0; index < array.length; index++) {
            let room = array[index];
            if (value < room.weight)
                return room;
            value -= room.weight;
        }
        throw new Error("Cannot find Room! \nValue: " + value + "\nRooms: " + JSON.stringify(array));
    }

    private getRoomArray(facing: Facing): Array<RoomTemplate> {
        switch (facing) {
            case Facing.LEFT:
                return this.roomWithLeftEntrance;
            case Facing.RIGHT:
                return this.roomWithRightEntrance;
            case Facing.UP:
                return this.roomWithUpEntrance;
            case Facing.DOWN:
                return this.roomWithDownEntrance;
        }
    }

    private getRoomWeight(facing: Facing): number {
        switch (facing) {
            case Facing.LEFT:
                return this.roomWithLeftEntranceWeight;
            case Facing.RIGHT:
                return this.roomWithRightEntranceWeight;
            case Facing.UP:
                return this.roomWithUpEntranceWeight;
            case Facing.DOWN:
                return this.roomWithDownEntranceWeight;
        }
    }

    private copyRoom(old: RoomTemplate, posX: number, posY: number): Room {
        let room = new Room();
        room.topLeft = new Vec2(posX, posY);
        room.bottomRight = new Vec2(posX + old.width - 1, posY + old.height - 1);
        room.topLayer = [...old.topLayer];
        room.bottomLayer = [...old.bottomLayer];
        if (posX < this.minX)
            this.minX = posX;
        if (posY < this.minY)
            this.minY = posY;
        if (posX + old.width - 1 > this.maxX)
            this.maxX = posX + old.width - 1;
        if (posY + old.height - 1 > this.maxY)
            this.maxY = posY + old.height - 1;
        return room;
    }
}

class Room {
    topLeft: Vec2;
    bottomRight: Vec2;
    topLayer: Array<number>;
    bottomLayer: Array<number>;
}

enum Facing {
    LEFT = "left",
    RIGHT = "right",
    UP = "up",
    DOWN = "down"
}