import { EMPTY_DOOR, MAX_NUM_TILE_PER_AXIS, MAX_WALL_HEIGHT, MIN_WALL_HEIGHT } from './constants';
import { parseTilemap } from './encoding';
import { FloorplanAction, FloorplanState, Tile } from './types';

export const initialState: FloorplanState = {
    tiles: [],
    door: { ...EMPTY_DOOR },
    thickness: { wall: 1, floor: 1 },
    wallHeight: -1,
    brush: { h: 0, action: 'SET' },
    selection: new Set<`${number},${number}`>(),
    squareSelect: false,
    pickUpBlocking: false,
    lease: { holder: null, me: false, expiresAt: null },
    seq: 0
};

const clampHeight = (h: number): number => Math.max(0, Math.min(26, h | 0));
const clamp64 = (n: number): number => Math.max(0, Math.min(MAX_NUM_TILE_PER_AXIS - 1, n | 0));

const ensureRect = (tiles: Tile[][], rows: number, cols: number): Tile[][] => {
    const tRows = Math.min(MAX_NUM_TILE_PER_AXIS, Math.max(rows, tiles.length));
    const tCols = Math.min(MAX_NUM_TILE_PER_AXIS, Math.max(cols, tiles[0]?.length ?? 0));
    if (tRows === tiles.length && (tiles[0]?.length ?? 0) === tCols) return tiles;
    const next: Tile[][] = [];
    for (let r = 0; r < tRows; r++) {
        const src = tiles[r] ?? [];
        const row: Tile[] = [];
        for (let c = 0; c < tCols; c++) {
            row.push(src[c] ?? { h: 0, blocked: true });
        }
        next.push(row);
    }
    return next;
};

const setTile = (tiles: Tile[][], row: number, col: number, tile: Tile): Tile[][] => {
    const current = tiles[row]?.[col];
    if (current && current.h === tile.h && current.blocked === tile.blocked) return tiles;
    // A tile edited while its furniture is to be picked up stays marked, so it keeps its tint and locks again when
    // picking up is switched off.
    const kept = current?.occupied ? { ...tile, occupied: true } : tile;
    const next = tiles.map((r, ri) => (ri === row ? r.map((t, ci) => (ci === col ? kept : t)) : r));
    return next;
};

/** An occupied tile can only be edited when the save is going to pick its furniture up. */
const isLocked = (state: FloorplanState, tile: Tile | undefined): boolean => !!tile?.occupied && !state.pickUpBlocking;

/**
 * Occupied flags mark tiles that hold furniture. They come from the server,
 * not from the tilemap string, so whenever a whole tilemap is swapped in
 * (import, undo/redo snapshots, live-sync snapshots) the flags are carried
 * over by position or they would silently disappear.
 */
const carryOccupied = (previous: Tile[][], next: Tile[][]): Tile[][] =>
    next.map((row, r) => row.map((tile, c) => (previous[r]?.[c]?.occupied ? { ...tile, occupied: true } : tile)));

export const reducer = (state: FloorplanState, action: FloorplanAction): FloorplanState => {
    switch (action.type) {
        case 'PAINT_TILE': {
            const row = clamp64(action.row);
            const col = clamp64(action.col);
            if (isLocked(state, state.tiles[row]?.[col])) return state;
            const tiles = ensureRect(state.tiles, row + 1, col + 1);
            const target = { h: clampHeight(action.h), blocked: false };
            const next = setTile(tiles, row, col, target);
            if (next === tiles && tiles === state.tiles) return state;
            return { ...state, tiles: next };
        }
        case 'ERASE_TILE': {
            const row = action.row | 0;
            const col = action.col | 0;
            if (row < 0 || col < 0 || row >= state.tiles.length || col >= (state.tiles[0]?.length ?? 0)) return state;
            const current = state.tiles[row][col];
            if (isLocked(state, current)) return state;
            const target = { h: current.h, blocked: true };
            const next = setTile(state.tiles, row, col, target);
            if (next === state.tiles) return state;
            return { ...state, tiles: next };
        }
        case 'ADJUST_HEIGHT': {
            const row = action.row | 0;
            const col = action.col | 0;
            if (row < 0 || col < 0 || row >= state.tiles.length || col >= (state.tiles[0]?.length ?? 0)) return state;
            const current = state.tiles[row][col];
            if (current.blocked || isLocked(state, current)) return state;
            const newH = clampHeight(current.h + action.delta);
            if (newH === current.h) return state;
            const next = setTile(state.tiles, row, col, { h: newH, blocked: false });
            return { ...state, tiles: next };
        }
        case 'SET_DOOR': {
            const x = clamp64(action.x);
            const y = clamp64(action.y);
            if (state.door.x === x && state.door.y === y) return state;
            return { ...state, door: { ...state.door, x, y } };
        }
        case 'SET_DOOR_DIR': {
            if (state.door.dir === action.dir) return state;
            return { ...state, door: { ...state.door, dir: action.dir } };
        }
        case 'SET_THICKNESS': {
            const wall = action.wall ?? state.thickness.wall;
            const floor = action.floor ?? state.thickness.floor;
            if (wall === state.thickness.wall && floor === state.thickness.floor) return state;
            return { ...state, thickness: { wall, floor } };
        }
        case 'SET_WALL_HEIGHT': {
            const value = Math.max(MIN_WALL_HEIGHT, Math.min(MAX_WALL_HEIGHT, action.value | 0));
            if (value === state.wallHeight) return state;
            return { ...state, wallHeight: value };
        }
        case 'SET_OCCUPIED_TILES': {
            // Mark tiles that currently hold furniture (server-reported). Leaves
            // height + blocked untouched so it never alters the saved tilemap.
            const map = action.map ?? [];
            let changed = false;
            const tiles = state.tiles.map((r, ri) =>
                r.map((tile, ci) => {
                    const occ = !!map[ri]?.[ci];
                    if ((tile.occupied ?? false) === occ) return tile;
                    changed = true;
                    return { ...tile, occupied: occ };
                })
            );
            if (!changed) return state;
            return { ...state, tiles };
        }
        case 'BRUSH_SET': {
            const h = action.h ?? state.brush.h;
            const act = action.action ?? state.brush.action;
            if (h === state.brush.h && act === state.brush.action) return state;
            return { ...state, brush: { h: clampHeight(h), action: act } };
        }
        case 'SELECT_ALL': {
            const sel = new Set<`${number},${number}`>();
            for (let r = 0; r < MAX_NUM_TILE_PER_AXIS; r++) {
                for (let c = 0; c < MAX_NUM_TILE_PER_AXIS; c++) {
                    sel.add(`${r},${c}`);
                }
            }
            return { ...state, selection: sel };
        }
        case 'CLEAR_SELECTION':
            return state.selection.size === 0 ? state : { ...state, selection: new Set() };
        case 'SELECT_RECT': {
            const [r0, c0] = action.from;
            const [r1, c1] = action.to;
            const rMin = Math.max(0, Math.min(r0, r1));
            const rMax = Math.min(MAX_NUM_TILE_PER_AXIS - 1, Math.max(r0, r1));
            const cMin = Math.max(0, Math.min(c0, c1));
            const cMax = Math.min(MAX_NUM_TILE_PER_AXIS - 1, Math.max(c0, c1));
            const sel = new Set<`${number},${number}`>();
            for (let r = rMin; r <= rMax; r++) {
                for (let c = cMin; c <= cMax; c++) {
                    sel.add(`${r},${c}`);
                }
            }
            return { ...state, selection: sel };
        }
        case 'APPLY_BRUSH_TO_SELECTION': {
            if (state.selection.size === 0) return state;
            if (state.brush.action === 'DOOR') return { ...state, selection: new Set() };

            let maxR = -1;
            let maxC = -1;
            for (const key of state.selection) {
                const [rStr, cStr] = key.split(',');
                const r = parseInt(rStr, 10);
                const c = parseInt(cStr, 10);
                if (r > maxR) maxR = r;
                if (c > maxC) maxC = c;
            }

            let tiles = state.tiles;
            if (state.brush.action === 'SET' && maxR >= 0 && maxC >= 0) {
                tiles = ensureRect(tiles, maxR + 1, maxC + 1);
            }

            for (const key of state.selection) {
                const [rStr, cStr] = key.split(',');
                const row = parseInt(rStr, 10);
                const col = parseInt(cStr, 10);
                const current = tiles[row]?.[col];
                if (!current) continue;
                if (isLocked(state, current)) continue;

                switch (state.brush.action) {
                    case 'SET':
                        tiles = setTile(tiles, row, col, { h: clampHeight(state.brush.h), blocked: false });
                        break;
                    case 'UNSET':
                        if (current.blocked) continue;
                        tiles = setTile(tiles, row, col, { h: current.h, blocked: true });
                        break;
                    case 'UP':
                        if (current.blocked) continue;
                        tiles = setTile(tiles, row, col, { h: clampHeight(current.h + 1), blocked: false });
                        break;
                    case 'DOWN':
                        if (current.blocked) continue;
                        tiles = setTile(tiles, row, col, { h: clampHeight(current.h - 1), blocked: false });
                        break;
                }
            }

            return { ...state, tiles, selection: new Set() };
        }
        case 'SQUARE_SELECT_TOGGLE':
            return { ...state, squareSelect: !state.squareSelect };
        case 'SET_PICK_UP_BLOCKING':
            return state.pickUpBlocking === action.value ? state : { ...state, pickUpBlocking: action.value };
        case 'IMPORT_STRING': {
            const tiles = carryOccupied(state.tiles, parseTilemap(action.raw));
            const next: FloorplanState = { ...state, tiles };
            if (action.door) next.door = action.door;
            if (action.thickness) next.thickness = action.thickness;
            if (action.wallHeight !== undefined) next.wallHeight = Math.max(MIN_WALL_HEIGHT, Math.min(MAX_WALL_HEIGHT, action.wallHeight | 0));
            return next;
        }
        case 'APPLY_REMOTE_DIFF': {
            let next: FloorplanState = { ...state, seq: action.seq };
            if (action.diff.tiles) {
                let tiles = next.tiles;
                for (const e of action.diff.tiles) {
                    tiles = ensureRect(tiles, e.row + 1, e.col + 1);
                    const occupied = tiles[e.row]?.[e.col]?.occupied;
                    tiles = setTile(tiles, e.row, e.col, occupied ? { h: clampHeight(e.h), blocked: e.blocked, occupied: true } : { h: clampHeight(e.h), blocked: e.blocked });
                }
                next.tiles = tiles;
            }
            if (action.diff.door) next.door = action.diff.door;
            if (action.diff.thickness) next.thickness = action.diff.thickness;
            if (action.diff.wallHeight !== undefined) next.wallHeight = Math.max(MIN_WALL_HEIGHT, Math.min(MAX_WALL_HEIGHT, action.diff.wallHeight | 0));
            return next;
        }
        case 'APPLY_REMOTE_SNAPSHOT': {
            return {
                ...state,
                tiles: carryOccupied(state.tiles, parseTilemap(action.raw)),
                door: action.door,
                thickness: action.thickness,
                wallHeight: Math.max(MIN_WALL_HEIGHT, Math.min(MAX_WALL_HEIGHT, action.wallHeight | 0)),
                selection: new Set(),
                seq: action.seq
            };
        }
        default:
            return state;
    }
};
