import { array } from '../../dataHelper.js';
import { Color } from '../../primitives.js';
import { instance as banPatternInstance, } from '../../rules/banPatternRule.js';
import { instance as cellCountInstance, } from '../../rules/cellCountRule.js';
import { instance as regionAreaInstance, } from '../../rules/regionAreaRule.js';
import { instance as sameShapeInstance, } from '../../rules/sameShapeRule.js';
import { instance as symbolsPerRegionInstance, } from '../../rules/symbolsPerRegionRule.js';
import { instance as undercluedInstance } from '../../rules/undercluedRule.js';
import { instance as uniqueShapeInstance, } from '../../rules/uniqueShapeRule.js';
import { Serializer } from '../../serializer/allSerializers.js';
import { instance as areaNumberInstance, } from '../../symbols/areaNumberSymbol.js';
import { instance as dartInstance, } from '../../symbols/dartSymbol.js';
import { instance as galaxyInstance, } from '../../symbols/galaxySymbol.js';
import { instance as letterInstance, } from '../../symbols/letterSymbol.js';
import { instance as lotusInstance, } from '../../symbols/lotusSymbol.js';
import { instance as minesweeperInstance, } from '../../symbols/minesweeperSymbol.js';
import { instance as myopiaInstance, } from '../../symbols/myopiaSymbol.js';
import { instance as viewpointInstance, } from '../../symbols/viewpointSymbol.js';
import { instance as connectAllInstance } from '../z3/modules/connectAllModule.js';
import { BTGridData, BTTile } from './data.js';
import BanPatternBTModule from './rules/banPattern.js';
import CellCountBTModule from './rules/cellCount.js';
import ConnectAllBTModule from './rules/connectAll.js';
import RegionAreaBTModule from './rules/regionArea.js';
import SameShapeBTModule from './rules/sameShape.js';
import SymbolsPerRegionBTModule from './rules/symbolsPerRegion.js';
import UniqueShapeBTModule from './rules/uniqueShape.js';
import AreaNumberBTModule from './symbols/areaNumber.js';
import DartBTModule from './symbols/dart.js';
import GalaxyBTModule from './symbols/galaxy.js';
import LetterBTModule from './symbols/letter.js';
import LotusBTModule from './symbols/lotus.js';
import MinesweeperBTModule from './symbols/minesweeper.js';
import MyopiaBTModule from './symbols/myopia.js';
import ViewpointBTModule from './symbols/viewpoint.js';
function translateToBTGridData(grid) {
    const tiles = array(grid.width, grid.height, (x, y) => {
        const tile = grid.getTile(x, y);
        if (!tile.exists)
            return BTTile.NonExist;
        else if (tile.color === Color.Dark)
            return BTTile.Dark;
        else if (tile.color === Color.Light)
            return BTTile.Light;
        else
            return BTTile.Empty;
    });
    const connections = array(grid.width, grid.height, (x, y) => grid.connections.getConnectedTiles({ x, y }));
    const modules = [];
    for (const [id, symbolList] of grid.symbols) {
        for (const symbol of symbolList) {
            let module;
            if (id === areaNumberInstance.id) {
                module = new AreaNumberBTModule(symbol);
            }
            else if (id === dartInstance.id) {
                module = new DartBTModule(symbol);
            }
            else if (id === viewpointInstance.id) {
                module = new ViewpointBTModule(symbol);
            }
            else if (id === galaxyInstance.id) {
                module = new GalaxyBTModule(symbol);
            }
            else if (id === lotusInstance.id) {
                module = new LotusBTModule(symbol);
            }
            else if (id === myopiaInstance.id) {
                module = new MyopiaBTModule(symbol);
            }
            else if (id === minesweeperInstance.id) {
                module = new MinesweeperBTModule(symbol);
            }
            else if (id === letterInstance.id) {
                continue;
            }
            if (!module)
                throw new Error('Symbol not supported.');
            modules.push(module);
        }
    }
    const letterSymbols = grid.symbols.get(letterInstance.id);
    if (letterSymbols) {
        modules.push(new LetterBTModule(letterSymbols, grid.width, grid.height));
    }
    for (const rule of grid.rules) {
        if (!rule.necessaryForCompletion)
            continue;
        let module;
        if (rule.id === connectAllInstance.id) {
            module = new ConnectAllBTModule(rule);
        }
        else if (rule.id === regionAreaInstance.id) {
            module = new RegionAreaBTModule(rule);
        }
        else if (rule.id === banPatternInstance.id) {
            module = new BanPatternBTModule(rule);
        }
        else if (rule.id === symbolsPerRegionInstance.id) {
            const allSymbols = [];
            grid.symbols.forEach(symbols => allSymbols.push(...symbols));
            module = new SymbolsPerRegionBTModule(rule, grid.width, grid.height, allSymbols);
        }
        else if (rule.id === cellCountInstance.id) {
            module = new CellCountBTModule(rule);
        }
        else if (rule.id === sameShapeInstance.id) {
            module = new SameShapeBTModule(rule);
        }
        else if (rule.id === uniqueShapeInstance.id) {
            module = new UniqueShapeBTModule(rule);
        }
        else if (rule.id === undercluedInstance.id) {
            continue;
        }
        if (!module)
            throw new Error('Rule not supported.');
        modules.push(module);
    }
    return new BTGridData(tiles, connections, modules, grid.width, grid.height);
}
function translateBackGridData(grid, btGrid) {
    const tiles = array(grid.width, grid.height, (x, y) => {
        const origTile = grid.getTile(x, y);
        if (!origTile.exists || origTile.fixed || origTile.color !== Color.Gray)
            return origTile;
        else
            return origTile.withColor(btGrid.getTile(x, y) === BTTile.Dark ? Color.Dark : Color.Light);
    });
    return grid.withTiles(tiles);
}
function isValid(grid, places, checkable, ratings) {
    const newCheckable = [...checkable];
    const newRatings = [...ratings];
    for (let i = 0; i < grid.modules.length; i++) {
        const module = grid.modules[i];
        // Check if skippable
        if (checkable[i] && !places.some(pos => checkable[i].get(pos.x, pos.y)))
            continue;
        const result = module.checkLocal(grid, places);
        if (!result)
            return false;
        // If returns true, it means do not change checkable and ratings
        if (result === true)
            continue;
        newCheckable[i] = result.tilesNeedCheck;
        newRatings[i] = result.ratings;
    }
    return [newCheckable, newRatings];
}
// This function chooses the next empty tile to search.
function getNextTile(grid, ratings) {
    const scores = [];
    // TODO: Sum up all the scores of connected tiles without overcounting
    for (let y = 0; y < grid.height; y++) {
        scores[y] = [];
        for (let x = 0; x < grid.width; x++) {
            scores[y][x] = 0;
        }
    }
    for (const rating of ratings) {
        if (!rating)
            continue;
        for (const score of rating) {
            scores[score.pos.y][score.pos.x] += score.score;
        }
    }
    let highest = 0;
    let pos = null;
    let fallback = null;
    for (let y = 0; y < grid.height; y++) {
        for (let x = 0; x < grid.width; x++) {
            if (grid.getTile(x, y) !== BTTile.Empty)
                continue;
            if (scores[y][x] > highest) {
                highest = scores[y][x];
                pos = { x, y };
            }
            if (!fallback)
                fallback = { x, y };
        }
    }
    return pos ?? fallback;
}
function backtrack(grid, checkable, ratings, solutionFn) {
    // Find the best empty cell to guess
    const pos = getNextTile(grid, ratings);
    // Found a solution
    if (!pos)
        return !solutionFn(grid.clone());
    for (let i = 0; i <= 1; i++) {
        // TODO: Use a better method to determine the order
        const tile = i === 0 ? BTTile.Light : BTTile.Dark;
        grid.setTileWithConnection(pos.x, pos.y, tile);
        const places = grid.connections[pos.y][pos.x];
        const result = isValid(grid, places, checkable, ratings);
        if (result && backtrack(grid, result[0], result[1], solutionFn))
            return true;
    }
    // If both fail, returns to initial state
    grid.setTileWithConnection(pos.x, pos.y, BTTile.Empty);
    return false;
}
function solveNormal(input, solutionFn) {
    // Translate to BT data types
    const grid = translateToBTGridData(input);
    const checkable = [];
    const ratings = [];
    for (const module of grid.modules) {
        const res = module.checkGlobal(grid);
        if (!res)
            return [];
        checkable.push(res.tilesNeedCheck);
        ratings.push(res.ratings);
    }
    // Call backtrack
    backtrack(grid, checkable, ratings, sol => solutionFn(translateBackGridData(input, sol)));
}
function solveUnderclued(input) {
    let grid = input;
    let count = 0;
    const possibles = array(grid.width, grid.height, () => ({
        dark: false,
        light: false,
    }));
    function search(x, y, tile, color) {
        count++;
        // console.log(`Trying (${x}, ${y}) with ${color}`);
        const newGrid = grid.setTile(x, y, tile.withColor(color));
        // Solve
        let solution;
        solveNormal(newGrid, sol => {
            solution = sol;
            return false;
        });
        if (!solution)
            return false;
        // Update the new possible states
        solution.forEach((solTile, solX, solY) => {
            if (solTile.color === Color.Dark) {
                possibles[solY][solX].dark = true;
            }
            else {
                possibles[solY][solX].light = true;
            }
        });
        return true;
    }
    for (let y = 0; y < grid.height; y++) {
        for (let x = 0; x < grid.width; x++) {
            const tile = grid.getTile(x, y);
            if (!tile.exists || tile.color !== Color.Gray)
                continue;
            // We can skip this solve if it is proved to be solvable
            const darkPossible = possibles[y][x].dark || search(x, y, tile, Color.Dark);
            const lightPossible = possibles[y][x].light || search(x, y, tile, Color.Light);
            // No solution
            if (!darkPossible && !lightPossible)
                return null;
            if (darkPossible && !lightPossible)
                grid = grid.setTile(x, y, tile.withColor(Color.Dark));
            if (!darkPossible && lightPossible)
                grid = grid.setTile(x, y, tile.withColor(Color.Light));
        }
    }
    console.log(`Solve count: ${count}`);
    return grid;
}
function solve(grid, solutionFn) {
    if (grid.findRule(rule => rule.id === undercluedInstance.id)) {
        const res = solveUnderclued(grid);
        if (res)
            solutionFn(res);
    }
    else {
        solveNormal(grid, solutionFn);
    }
}
onmessage = e => {
    const grid = Serializer.parseGrid(e.data);
    console.time('Solve time');
    let count = 0;
    solve(grid, solution => {
        if (count === 0)
            console.timeLog('Solve time', 'First solution');
        postMessage(Serializer.stringifyGrid(solution));
        count += 1;
        return count < 2;
    });
    console.timeEnd('Solve time');
    postMessage(null);
};
// make typescript happy
// eslint-disable-next-line import/no-anonymous-default-export
export default null;
