import GridData from '../grid.js';
import GridConnections from '../gridConnections.js';
import TileData from '../tile.js';
import { ConfigType } from '../config.js';
import { Color, DIRECTIONS, ORIENTATIONS, Orientation, directionToggle, orientationToggle, } from '../primitives.js';
import { array, escape, unescape } from '../dataHelper.js';
import { allRules } from '../rules/index.js';
import { allSymbols } from '../symbols/index.js';
import SerializerBase from './serializerBase.js';
import { ControlLine, Row } from '../rules/musicControlLine.js';
const OFFSETS = [
    { x: 0, y: -1 },
    { x: -1, y: 0 },
    { x: 1, y: 0 },
    { x: 0, y: 1 },
];
const orientationChars = {
    [Orientation.Up]: 'u',
    [Orientation.UpRight]: 'x',
    [Orientation.Right]: 'r',
    [Orientation.DownRight]: 'z',
    [Orientation.Down]: 'd',
    [Orientation.DownLeft]: 'y',
    [Orientation.Left]: 'l',
    [Orientation.UpLeft]: 'w',
};
export default class SerializerV0 extends SerializerBase {
    constructor() {
        super(...arguments);
        Object.defineProperty(this, "version", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: 0
        });
    }
    stringifyTile(tile) {
        if (!tile.exists)
            return '.';
        const char = tile.color === Color.Gray ? 'n' : tile.color === Color.Dark ? 'b' : 'w';
        return tile.fixed ? char.toUpperCase() : char;
    }
    parseTile(str) {
        return TileData.create(str);
    }
    stringifyControlLine(line) {
        const result = [];
        result.push(`c${line.column}`);
        if (line.bpm !== null)
            result.push(`b${line.bpm}`);
        if (line.pedal !== null)
            result.push(`p${line.pedal ? '1' : '0'}`);
        if (line.checkpoint)
            result.push('s');
        result.push(`r${line.rows
            .map(row => `v${row.velocity ?? ''}n${row.note ?? ''}`)
            .join(',')}`);
        return result.join('|');
    }
    parseControlLine(str) {
        let column = null;
        let bpm = null;
        let pedal = null;
        let checkpoint = false;
        const rows = [];
        const data = str.split('|');
        for (const entry of data) {
            const key = entry.charAt(0);
            const value = entry.slice(1);
            switch (key) {
                case 'c':
                    column = value === '' ? null : Number(value);
                    break;
                case 'b':
                    bpm = value === '' ? null : Number(value);
                    break;
                case 'p':
                    pedal = value === '1' ? true : value === '0' ? false : null;
                    break;
                case 's':
                    checkpoint = true;
                    break;
                case 'r':
                    rows.push(...value.split(',').map(row => {
                        const match = row.match(/^v([\d.]*?)n(.*)$/);
                        if (!match)
                            return new Row(null, null);
                        const [, velocity, note] = match;
                        return new Row(note === '' ? null : note, velocity === '' ? null : Number(velocity));
                    }));
                    break;
            }
        }
        return new ControlLine(column ?? 0, bpm, pedal, checkpoint, rows);
    }
    stringifyConfig(instruction, config) {
        switch (config.type) {
            case ConfigType.Boolean:
                return (config.field +
                    '=' +
                    (instruction[config.field] ? '1' : '0'));
            case ConfigType.Number:
            case ConfigType.Color:
            case ConfigType.Comparison:
            case ConfigType.Direction:
            case ConfigType.Orientation:
                return (config.field +
                    '=' +
                    String(instruction[config.field]));
            case ConfigType.NullableBoolean:
                return (config.field +
                    '=' +
                    (instruction[config.field] === null
                        ? ''
                        : instruction[config.field]
                            ? '1'
                            : '0'));
            case ConfigType.NullableNumber:
                return (config.field +
                    '=' +
                    (instruction[config.field] === null
                        ? ''
                        : String(instruction[config.field])));
            case ConfigType.DirectionToggle:
                return (config.field +
                    '=' +
                    DIRECTIONS.filter(dir => instruction[config.field][dir])
                        .map(x => orientationChars[x])
                        .join(''));
            case ConfigType.OrientationToggle:
                return (config.field +
                    '=' +
                    ORIENTATIONS.filter(dir => instruction[config.field][dir])
                        .map(x => orientationChars[x])
                        .join(''));
            case ConfigType.String:
            case ConfigType.Icon:
                return (config.field +
                    '=' +
                    escape(String(instruction[config.field])));
            case ConfigType.NullableNote:
                return (config.field +
                    '=' +
                    escape(instruction[config.field] === null
                        ? ''
                        : escape(String(instruction[config.field]))));
            case ConfigType.Tile:
            case ConfigType.Grid:
                return (config.field +
                    '=' +
                    escape(this.stringifyGrid(instruction[config.field])));
            case ConfigType.NullableGrid:
                return (config.field +
                    '=' +
                    escape(instruction[config.field] === null
                        ? ''
                        : this.stringifyGrid(instruction[config.field])));
            case ConfigType.ControlLines:
                return (config.field +
                    '=' +
                    escape(instruction[config.field]
                        .map(line => this.stringifyControlLine(line))
                        .join(':')));
        }
    }
    parseConfig(configs, entry) {
        const [key, value] = entry.split('=');
        const config = configs.find(x => x.field === key);
        if (!config)
            throw new Error(`Unknown config: ${key}`);
        switch (config.type) {
            case ConfigType.Boolean:
                return [config.field, value === '1'];
            case ConfigType.NullableBoolean:
                return [config.field, value === '' ? null : value === '1'];
            case ConfigType.Number:
                return [config.field, Number(value)];
            case ConfigType.NullableNumber:
                return [config.field, value === '' ? null : Number(value)];
            case ConfigType.Color:
                return [config.field, value];
            case ConfigType.Comparison:
                return [config.field, value];
            case ConfigType.Direction:
                return [config.field, value];
            case ConfigType.DirectionToggle: {
                const toggle = directionToggle();
                for (const dir of DIRECTIONS) {
                    toggle[dir] = value.includes(orientationChars[dir]);
                }
                return [config.field, toggle];
            }
            case ConfigType.Orientation:
                return [config.field, value];
            case ConfigType.OrientationToggle: {
                const toggle = orientationToggle();
                for (const dir of ORIENTATIONS) {
                    toggle[dir] = value.includes(orientationChars[dir]);
                }
                return [config.field, toggle];
            }
            case ConfigType.String:
            case ConfigType.Icon:
                return [config.field, unescape(value)];
            case ConfigType.Tile:
            case ConfigType.Grid:
                return [config.field, this.parseGrid(unescape(value))];
            case ConfigType.NullableGrid:
                return [
                    config.field,
                    value === '' ? null : this.parseGrid(unescape(value)),
                ];
            case ConfigType.ControlLines:
                return [
                    config.field,
                    unescape(value)
                        .split(':')
                        .map(line => this.parseControlLine(line)),
                ];
            case ConfigType.NullableNote:
                return [config.field, value === '' ? null : unescape(value)];
        }
    }
    stringifyInstruction(instruction) {
        return `${instruction.id},${instruction.configs?.map(config => this.stringifyConfig(instruction, config)).join(',') ?? ''}`;
    }
    stringifyRule(rule) {
        return this.stringifyInstruction(rule);
    }
    stringifySymbol(symbol) {
        return this.stringifyInstruction(symbol);
    }
    parseRule(str) {
        const [id, ...entries] = str.split(',');
        const instruction = allRules.get(id);
        if (!instruction)
            throw new Error(`Unknown rule: ${id}`);
        const configs = instruction.configs;
        if (configs == null)
            return instruction.copyWith({});
        return instruction.copyWith(Object.fromEntries(entries
            .filter(entry => entry !== '')
            .map(entry => this.parseConfig(configs, entry))));
    }
    parseSymbol(str) {
        const [id, ...entries] = str.split(',');
        const instruction = allSymbols.get(id);
        if (!instruction)
            throw new Error(`Unknown symbol: ${id}`);
        const configs = instruction.configs;
        if (configs == null)
            return instruction.copyWith({});
        return instruction.copyWith(Object.fromEntries(entries.map(entry => this.parseConfig(configs, entry))));
    }
    stringifyConnections(connections) {
        const maxX = connections.edges.reduce((max, edge) => Math.max(max, edge.x1, edge.x2), 0);
        const maxY = connections.edges.reduce((max, edge) => Math.max(max, edge.y1, edge.y2), 0);
        const result = array(maxX + 1, maxY + 1, () => '.');
        for (let y = 0; y <= maxY; y++) {
            for (let x = 0; x <= maxX; x++) {
                if (result[y][x] !== '.') {
                    continue;
                }
                const tiles = connections.getConnectedTiles({ x, y });
                if (tiles.length < 2) {
                    continue;
                }
                const existingChars = [];
                for (const { x: tx, y: ty } of tiles) {
                    for (const { x: dx, y: dy } of OFFSETS) {
                        if (tx + dx > maxX ||
                            ty + dy > maxY ||
                            tx + dx < 0 ||
                            ty + dy < 0) {
                            continue;
                        }
                        if (result[ty + dy][tx + dx] !== '.')
                            existingChars.push(result[ty + dy][tx + dx]);
                    }
                }
                let char = 'A';
                while (existingChars.includes(char)) {
                    char = String.fromCharCode(char.charCodeAt(0) + 1);
                }
                for (const connection of tiles) {
                    result[connection.y][connection.x] = char;
                }
            }
        }
        return `C${maxX + 1}:${result.map(row => row.join('')).join('')}`.replace(/\.+$/, '');
    }
    parseConnections(input) {
        if (!input.startsWith('C')) {
            throw new Error('Invalid grid connections\n' + input);
        }
        const [size, data] = input.slice(1).split(':');
        const width = Number(size);
        const tiles = array(width, Math.ceil(data.length / width), (x, y) => data[y * width + x]);
        return GridConnections.create(tiles.map(row => row.join('')));
    }
    stringifyTiles(tiles) {
        return `T${tiles[0]?.length ?? 0}:${tiles.map(row => row.map(tile => this.stringifyTile(tile)).join('')).join('')}`;
    }
    parseTiles(input) {
        if (!input.startsWith('T')) {
            throw new Error('Invalid grid data\n' + input);
        }
        const [size, data] = input.slice(1).split(':');
        const width = Number(size);
        return array(width, Math.ceil(data.length / width), (x, y) => this.parseTile(data.charAt(y * width + x)));
    }
    stringifyRules(rules) {
        return `R${rules.map(rule => this.stringifyRule(rule)).join(':')}`;
    }
    parseRules(input) {
        if (!input.startsWith('R')) {
            throw new Error('Invalid rules\n' + input);
        }
        return input
            .slice(1)
            .split(':')
            .filter(rule => rule !== '')
            .map(rule => this.parseRule(rule));
    }
    stringifySymbols(symbols) {
        return `S${Array.from(symbols.values())
            .flat()
            .map(symbol => this.stringifySymbol(symbol))
            .join(':')}`;
    }
    parseSymbols(input) {
        if (!input.startsWith('S')) {
            throw new Error('Invalid symbols\n' + input);
        }
        const symbols = new Map();
        input
            .slice(1)
            .split(':')
            .filter(symbol => symbol !== '')
            .forEach(symbol => {
            const parsed = this.parseSymbol(symbol);
            if (symbols.has(parsed.id)) {
                symbols.set(parsed.id, [...symbols.get(parsed.id), parsed]);
            }
            else {
                symbols.set(parsed.id, [parsed]);
            }
        });
        return symbols;
    }
    stringifyGrid(grid) {
        const data = [
            this.stringifyTiles(grid.tiles),
            this.stringifyConnections(grid.connections),
            this.stringifySymbols(grid.symbols),
            this.stringifyRules(grid.rules),
        ];
        return `${grid.width}x${grid.height}|${data.join('|')}`;
    }
    parseGrid(input) {
        const data = input.split('|');
        let width;
        let height;
        let tiles;
        let connections;
        let symbols;
        let rules;
        for (const d of data) {
            if (/^\d+x\d+$/.test(d)) {
                [width, height] = d.split('x').map(Number);
            }
            else if (d.startsWith('T')) {
                tiles = this.parseTiles(d);
            }
            else if (d.startsWith('C')) {
                connections = this.parseConnections(d);
            }
            else if (d.startsWith('S')) {
                symbols = this.parseSymbols(d);
            }
            else if (d.startsWith('R')) {
                rules = this.parseRules(d);
            }
            else {
                throw new Error(`Invalid data: ${d}`);
            }
        }
        return new GridData(width ?? tiles?.[0].length ?? 0, height ?? tiles?.length ?? 0, tiles, connections, symbols, rules);
    }
    stringifyPuzzle(puzzle) {
        let grid = puzzle.grid;
        if (puzzle.solution !== null) {
            const tiles = array(puzzle.grid.width, puzzle.grid.height, (x, y) => {
                const tile = puzzle.grid.getTile(x, y);
                const solutionTile = puzzle.solution.getTile(x, y);
                return tile.exists &&
                    !tile.fixed &&
                    solutionTile.exists &&
                    solutionTile.color !== Color.Gray
                    ? tile.copyWith({
                        color: puzzle.solution.tiles[y][x].color,
                    })
                    : tile;
            });
            grid = puzzle.grid.copyWith({ tiles });
        }
        return JSON.stringify({
            title: puzzle.title,
            grid: this.stringifyGrid(grid),
            difficulty: puzzle.difficulty,
            link: puzzle.link,
            author: puzzle.author,
            description: puzzle.description,
        });
    }
    parsePuzzle(input) {
        const { grid: gridString, ...metadata } = JSON.parse(input);
        const grid = this.parseGrid(gridString);
        const reset = grid.resetTiles();
        return {
            ...metadata,
            grid: reset,
            solution: grid.colorEquals(reset) ? null : grid,
        };
    }
}
