import { intHash } from "@common/utils";
import { ALL_DIRECTIONS, Direction, NEXT_DIRECTION, exp, getDirection, getOppositeDirection, isDirection, movePoint, pointsEquals, trunc } from "./utils";

export enum TileType {
    DESTINATION,
    SOURCE,
    EXTRACTOR,
    CONVEYOR,
    NOT,
    AND,
    OR,
}

export enum PortDirection {
    INPUT,
    OUTPUT
}

export enum ResourceType {
    NUMBER,
    ITEM,
}

export enum ResourceItemType {

}

interface TileLimit {
    cooldown: number;
    resourcesRequired: number;
    capacity: number;
    maxOutputs: number;
}

export const LIMITS: Record<TileType, TileLimit> = {
    [TileType.CONVEYOR]: { cooldown: 300, resourcesRequired: 1, capacity: 1, maxOutputs: 4 },

    [TileType.DESTINATION]: { cooldown: 0, resourcesRequired: 1, capacity: 4, maxOutputs: 0 },
    [TileType.SOURCE]: { cooldown: 10000, resourcesRequired: 0, capacity: 0, maxOutputs: 0 },
    [TileType.EXTRACTOR]: { cooldown: 300, resourcesRequired: 0, capacity: 0, maxOutputs: 4 },
    [TileType.NOT]: { cooldown: 3000, resourcesRequired: 1, capacity: 1, maxOutputs: 1 },
    [TileType.AND]: { cooldown: 6000, resourcesRequired: 2, capacity: 2, maxOutputs: 1 },
    [TileType.OR]: { cooldown: 3000, resourcesRequired: 2, capacity: 2, maxOutputs: 1 },
};

interface BaseResource {
}

interface ResourceNumber extends BaseResource {
    type: ResourceType.NUMBER;
    value: number;
}

interface ResourceItem extends BaseResource {
    type: ResourceType.ITEM;
    itemType: ResourceItemType;
}

export type Resource = ResourceNumber | ResourceItem;

interface Port {
    direction: PortDirection;
}

type Ports = Partial<Record<Direction, Port>>;
type Inventory = Partial<Record<Direction, Resource>>;
type AnimationTimers = Map<Direction, [number, number]>;

interface BaseTile {
    ports: Ports;
    inv: Inventory;

    acceptedResources?: ResourceType[];

    inputAnimations?: AnimationTimers;

    bufferedDirections?: Direction[];
    nextInput?: Direction;
    nextOutput?: Direction;
    timer?: number;
}

interface TileDestination extends BaseTile {
    type: TileType.DESTINATION;
    center: boolean;
}

interface TileSource extends BaseTile {
    type: TileType.SOURCE;
    resource: Resource;
}

interface TileExtractor extends BaseTile {
    type: TileType.EXTRACTOR;
    source: TileSource;
}

interface TileConveyor extends BaseTile {
    type: TileType.CONVEYOR;
}

interface TileUnaryLogic extends BaseTile {
    type: TileType.NOT;
}

interface TileBinaryLogic extends BaseTile {
    type: TileType.AND | TileType.OR;
    // TODO internal buffers
}

export type Tile = TileDestination | TileSource | TileExtractor | TileConveyor | TileUnaryLogic | TileBinaryLogic;

const id = (point: Point) => ((Math.floor(point[0]) & 0xFFFF) << 16) | Math.floor(point[1]) & 0xFFFF;
const deid = (pid: number): Point => [(pid >> 16) & 0xFFFF, pid & 0xFFFF];

const rid = (resource: Resource): number => {
    const idPart = (resource.type & 0xFFFF) << 16;
    if (resource.type === ResourceType.NUMBER) {
        return idPart | (resource.value) & 0xFFFF;
    } else if (resource.type === ResourceType.ITEM) {
        return idPart | (resource.itemType) & 0xFFFF;
    }
    return -1;
}

export const getPortDirections = (ports: Ports) => Object.keys(ports).map(k => +k as Direction);

export const getTileInputs = (tile: Tile) => ALL_DIRECTIONS.map(d => tile.inv[d]).filter(r => r != null);
export const getTileOutput = (tile: Tile): [Resource | undefined, Direction[]] => {
    let bufferedResource = tile.inv[Direction.NONE];
    let inputDirections: Direction[] = tile.bufferedDirections ?? [];
    const availableResources = getTileInputs(tile);
    const limits = LIMITS[tile.type];
    if (availableResources.length < limits.resourcesRequired) {
        return [undefined, []];
    }

    if (bufferedResource == null) {
        if (tile.type === TileType.EXTRACTOR) {
            bufferedResource = tile.source.resource;
        } else if (limits.resourcesRequired === 1) {
            const inputDirection = ALL_DIRECTIONS.find(d => typeof tile.inv[d] !== 'undefined');
            if (inputDirection) {
                let resource = tile.inv[inputDirection];
                if (tile.type === TileType.NOT && resource?.type === ResourceType.NUMBER) {
                    resource = { ...resource, value: ~(resource.value) & 0xF };
                }
                inputDirections = [inputDirection];
                bufferedResource = resource;
            }
        } else if (limits.resourcesRequired === 2) {
            inputDirections = ALL_DIRECTIONS.filter(d => typeof tile.inv[d] !== 'undefined').slice(0, 2);
            const [x, y] = inputDirections.map(d => tile.inv[d]);
            if (x?.type === ResourceType.NUMBER && y?.type === ResourceType.NUMBER) {
                switch (tile.type) {
                    case TileType.AND:
                        bufferedResource = { type: ResourceType.NUMBER, value: (x.value & y.value) & 0xF };
                        break;
                    case TileType.OR:
                        bufferedResource = { type: ResourceType.NUMBER, value: (x.value | y.value) & 0xF };
                        break;
                }
            }
        }
        tile.inv[Direction.NONE] = bufferedResource;
        tile.bufferedDirections = inputDirections;
    }
    return [bufferedResource, [Direction.NONE, ...inputDirections]];
}

const findNextPort = (ports: Ports, portDirection: PortDirection, prevDirection: Direction | undefined): Direction => {
    const outputs = getPortDirections(ports).filter(d => d && ports[d]?.direction === portDirection);
    if (outputs.length === 0) return Direction.NONE;

    if (prevDirection) {
        let nextDirection = NEXT_DIRECTION[prevDirection];
        while (nextDirection !== prevDirection) {
            if (outputs.includes(nextDirection)) break;
            nextDirection = NEXT_DIRECTION[nextDirection]
        }
        return nextDirection;
    } else {
        return outputs[0];
    }
}

export default class World {
    private world = new Map<number, Tile>();
    private deliveredResources = new Map<number, number>();
    constructor(private seed: number = Math.random() * 2e9) {
        for (let x = 0; x < 5; x++) {
            for (let y = 0; y < 5; y++) {
                const ports: Ports = {};
                if (y == 0) ports[Direction.NORTH] = { direction: PortDirection.INPUT };
                if (y == 4) ports[Direction.SOUTH] = { direction: PortDirection.INPUT };
                if (x == 0) ports[Direction.WEST] = { direction: PortDirection.INPUT };
                if (x == 4) ports[Direction.EAST] = { direction: PortDirection.INPUT };

                this.setTile([x - 2, y - 2], {
                    type: TileType.DESTINATION,
                    ports,
                    center: x == 2 && y == 2,
                    inv: {},
                });
            }
        }
    }

    placeTile(position: Point, type: TileType, prevPosition: Point | null = null) {
        position = trunc(position);
        if (prevPosition) {
            prevPosition = trunc(prevPosition);
        }
        let tile: Tile | undefined;
        const existingTile = this.getTile(position);
        switch (type) {
            case TileType.CONVEYOR:
            case TileType.NOT:
            case TileType.AND:
            case TileType.OR:
                if (prevPosition) {
                    const prevPositionTile = this.getTile(prevPosition);
                    const ports: Ports = {};
                    const direction = getDirection(exp`${position} - ${prevPosition}`);
                    const oppositeDirection = getOppositeDirection(direction);

                    if (prevPositionTile) {
                        if (!prevPositionTile.ports[direction]) {
                            prevPositionTile.ports[direction] = { direction: PortDirection.OUTPUT };
                        }
                        ports[oppositeDirection] = { direction: PortDirection.INPUT };
                    }
                    if (existingTile) {
                        if (!existingTile.ports[oppositeDirection]) {
                            existingTile.ports[oppositeDirection] = { direction: PortDirection.INPUT };
                        }
                    } else {
                        tile = {
                            type,
                            ports,
                            inv: {},
                        }
                    }
                } else if (!existingTile) {
                    const ports = this.connectPorts(position);
                    tile = {
                        type,
                        ports,
                        inv: {},
                    };
                }
                break;
            case TileType.EXTRACTOR:
                if (existingTile?.type === TileType.SOURCE) {
                    const ports = this.connectPorts(position, PortDirection.OUTPUT);

                    tile = {
                        type: TileType.EXTRACTOR,
                        ports,
                        source: existingTile,
                        inv: {},
                    };
                }
                break;
            case TileType.DESTINATION:
            case TileType.SOURCE:
                // Naturally generated only
                break;
            default:
                if (!existingTile) {
                    tile = {
                        type,
                        ports: {},
                        inv: {},
                    }
                }
                break;
        }

        if (tile) {
            this.setTile(position, tile);
        }
    }

    private setTile(position: Point, tile: Tile) {
        this.world.set(id(position), tile);
    }

    private connectPorts(position: Point, portDirection = PortDirection.INPUT): Ports {
        const ports: Ports = {};
        for (const direction of ALL_DIRECTIONS) {
            const [neighbour, oppositeDirection] = this.getNeighbour(position, direction);
            if (neighbour && neighbour.type !== TileType.SOURCE && neighbour.type !== TileType.DESTINATION) {
                if (!neighbour.ports[oppositeDirection]) {
                    neighbour.ports[oppositeDirection] = {
                        direction: portDirection === PortDirection.OUTPUT
                            ? PortDirection.INPUT
                            : PortDirection.OUTPUT
                    };
                }
                ports[direction] = { direction: portDirection };
            }
        }
        return ports;
    }

    removeTile(position: Point) {
        position = trunc(position);
        const pid = id(position);
        const existingTile = this.world.get(pid);
        const type = existingTile?.type;
        if (type === TileType.DESTINATION) {
            return;
        }
        if (existingTile) {
            for (const direction of getPortDirections(existingTile.ports)) {
                const [neighbour, oppositeDirection] = this.getNeighbour(position, direction);
                if (neighbour) {
                    delete neighbour.ports[oppositeDirection];
                    delete neighbour.inv[oppositeDirection];
                    if (neighbour.nextInput === oppositeDirection) {
                        neighbour.nextInput = findNextPort(neighbour.ports, PortDirection.INPUT, neighbour.nextInput);
                    }
                    if (neighbour.nextOutput === oppositeDirection) {
                        neighbour.nextOutput = findNextPort(neighbour.ports, PortDirection.OUTPUT, neighbour.nextOutput);
                    }
                }
            }
            this.world.delete(pid);
        }
    }

    getTile(position: Point): Tile | null {
        const [x, y] = position;
        const pid = id(position);
        const tile = this.world.get(pid);
        if (tile) return tile;

        if (Math.abs(x) >= 3 && Math.abs(y) >= 3) {
            return this.genTile(trunc(position));
        }

        return null;
    }

    private genTile(position: Point): Tile | null {
        const hash = intHash(this.seed, ...position);

        if ([42, 69, 0x42, 0x69].includes(hash & 0xFF)) {
            let mask = 1;
            const dist = Math.log10(Math.hypot(...position));
            for (let i = 1; i < dist; i++) {
                mask = (mask << 1) | 1;
            }
            const value = (hash >> 12) & mask;
            const newTile: Tile = { type: TileType.SOURCE, resource: { type: ResourceType.NUMBER, value }, ports: {}, inv: {} };
            return newTile;
        }

        return null;
    }

    private get tiles() {
        return this.world.entries();
    }

    private getNeighbour(position: Point, direction: Direction | undefined): [Tile | null, Direction, Point] {
        if (!direction) {
            return [this.genTile(position), Direction.NONE, position];
        }
        const neighbourPosition = movePoint(position, direction);
        const neighbour = this.getTile(neighbourPosition);
        const oppositeDirection = getOppositeDirection(direction);

        return [neighbour, oppositeDirection, neighbourPosition];
    }

    update(dt: number) {
        for (const [pid, tile] of this.tiles) {
            if (!tile.nextInput) {
                tile.nextInput = findNextPort(tile.ports, PortDirection.INPUT, tile.nextInput);
            }
            if (!tile.nextOutput) {
                tile.nextOutput = findNextPort(tile.ports, PortDirection.OUTPUT, tile.nextOutput);
            }
            const position = deid(pid);
            tile.timer = Math.max(0, (tile.timer ?? 0) - dt);

            const animationTimers: AnimationTimers = tile.inputAnimations ?? new Map<Direction, [number, number]>();
            for (const [direction, [timer, timerMax]] of animationTimers) {
                animationTimers.set(direction, [Math.max(0, timer - dt), timerMax]);
            }
            tile.inputAnimations = animationTimers;

            if (!tile.timer || tile.timer <= 0) {
                switch (tile.type) {
                    case TileType.DESTINATION:
                        const isInputting = Array.from(tile.inputAnimations?.values() ?? []).some(a => a[0] > 0);
                        if (!isInputting) {
                            for (const resource of Object.values(tile.inv)) {
                                const resourceId = rid(resource);
                                const gatheredAmount = this.deliveredResources.get(resourceId) ?? 0;
                                this.deliveredResources.set(resourceId, gatheredAmount + 1);
                            }
                            tile.inv = {};
                        }
                        break;
                    case TileType.SOURCE:
                        break; // source itself does nothing
                    default:
                        const [resource, inputDirections] = getTileOutput(tile);
                        if (resource != null) {
                            tile.nextOutput = findNextPort(tile.ports, PortDirection.OUTPUT, tile.nextOutput);
                            if (tile.nextOutput) {
                                const [neighbour, inputDirection, neighbourPosition] = this.getNeighbour(position, tile.nextOutput);
                                if (neighbour) {
                                    const [priorityPusher,] = this.getNeighbour(neighbourPosition, neighbour.nextInput);
                                    const neighbourLimits = LIMITS[neighbour.type];
                                    const limits = LIMITS[tile.type];

                                    if (
                                        neighbour.ports[inputDirection]?.direction === PortDirection.INPUT
                                        && neighbour.inv[inputDirection] == null
                                        && getTileInputs(neighbour).length < neighbourLimits.capacity
                                        && (
                                            neighbour.nextInput == null
                                            || neighbour.nextInput === inputDirection
                                            || !priorityPusher
                                            || getTileOutput(priorityPusher)[0] == null
                                            || neighbourLimits.resourcesRequired > 1
                                        )
                                    ) {
                                        neighbour.inv[inputDirection] = resource;
                                        neighbour.inputAnimations =
                                            (neighbour.inputAnimations ?? new Map<Direction, [number, number]>())
                                                .set(inputDirection, [LIMITS[TileType.CONVEYOR].cooldown, LIMITS[TileType.CONVEYOR].cooldown]);
                                        if (getTileInputs(neighbour).length >= neighbourLimits.capacity) {
                                            neighbour.timer = neighbourLimits.cooldown;
                                            neighbour.nextInput = findNextPort(neighbour.ports, PortDirection.INPUT, neighbour.nextInput);
                                        }

                                        inputDirections.forEach(inputDirection => delete tile.inv[inputDirection]);
                                        tile.timer = limits.cooldown;
                                    }
                                }
                            }
                        }
                        break;
                }
            }
        }
    }
}