import { ConfigType } from '../config.js';
import GridData from '../grid.js';
import { resize } from '../dataHelper.js';
import { Color, State } from '../primitives.js';
import CustomIconSymbol from '../symbols/customIconSymbol.js';
import { ControlLine, Row } from './musicControlLine.js';
import Rule from './rule.js';
const DEFAULT_SCALLE = [
    new Row('C5', 0.6),
    new Row('B4', 0.6),
    new Row('A4', 0.6),
    new Row('G4', 0.6),
    new Row('F4', 0.6),
    new Row('E4', 0.6),
    new Row('D4', 0.6),
    new Row('C4', 0.6),
];
class MusicGridRule extends Rule {
    /**
     * **Music Grid: Listen to the solution**
     * @param controlLines Denote changes in the playback settings. At least one control line at column 0 should be present to enable playback.
     * @param track The grid to be played when "listen" is clicked. Set as null to play the solution.
     */
    constructor(controlLines, track) {
        super();
        Object.defineProperty(this, "controlLines", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: controlLines
        });
        Object.defineProperty(this, "track", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: track
        });
        this.controlLines = MusicGridRule.deduplicateControlLines(controlLines);
        this.track = track;
    }
    get id() {
        return `music`;
    }
    get explanation() {
        return `*Music Grid:* Listen to the solution`;
    }
    get configs() {
        return MusicGridRule.CONFIGS;
    }
    createExampleGrid() {
        return MusicGridRule.EXAMPLE_GRID;
    }
    get searchVariants() {
        return MusicGridRule.SEARCH_VARIANTS;
    }
    validateGrid(_grid) {
        return { state: State.Incomplete };
    }
    onSetGrid(_oldGrid, newGrid) {
        if (newGrid.getTileCount(true, undefined, Color.Gray) === 0)
            return newGrid;
        const tiles = newGrid.tiles.map(row => row.map(tile => tile.color === Color.Gray ? tile.withColor(Color.Light) : tile));
        return newGrid.copyWith({ tiles });
    }
    onGridChange(newGrid) {
        if (this.controlLines.length === 0)
            return this;
        if (newGrid.height === this.controlLines[0].rows.length &&
            !this.controlLines.some(line => line.column >= newGrid.width))
            return this;
        const controlLines = this.controlLines
            .filter(line => line.column < newGrid.width)
            .map(line => line.withRows(resize(line.rows, newGrid.height, () => new Row(null, null))));
        return this.copyWith({ controlLines });
    }
    onGridResize(_grid, mode, direction, index) {
        if (mode === 'insert') {
            if (direction === 'row') {
                return this.copyWith({
                    controlLines: this.controlLines.map(line => {
                        const rows = line.rows.slice();
                        rows.splice(index, 0, new Row(null, null));
                        return line.withRows(rows);
                    }),
                });
            }
            else if (direction === 'column') {
                return this.copyWith({
                    controlLines: this.controlLines.map(line => line.column >= index ? line.withColumn(line.column + 1) : line),
                });
            }
        }
        else if (mode === 'remove') {
            if (direction === 'row') {
                return this.copyWith({
                    controlLines: this.controlLines.map(line => line.withRows(line.rows.filter((_, idx) => idx !== index))),
                });
            }
            else if (direction === 'column') {
                const lines = [];
                for (const line of this.controlLines) {
                    if (line.column === index) {
                        const nextLine = this.controlLines.find(l => l.column === index + 1);
                        if (nextLine) {
                            lines.push(MusicGridRule.mergeControlLines(line, nextLine));
                        }
                        else {
                            lines.push(line.withColumn(index));
                        }
                    }
                    else if (line.column > index) {
                        lines.push(line.withColumn(line.column - 1));
                    }
                    else {
                        lines.push(line);
                    }
                }
                return this.copyWith({ controlLines: lines });
            }
        }
        return this;
    }
    /**
     * Add or replace a control line.
     * @param controlLine The control line to set.
     * @returns A new rule with the control line set.
     */
    setControlLine(controlLine) {
        const controlLines = this.controlLines.filter(line => line.column !== controlLine.column);
        return this.copyWith({
            controlLines: [...controlLines, controlLine].sort((a, b) => a.column - b.column),
        });
    }
    withTrack(track) {
        return this.copyWith({ track });
    }
    copyWith({ controlLines, track, }) {
        return new MusicGridRule(controlLines ?? this.controlLines, track !== undefined ? track : this.track);
    }
    get validateWithSolution() {
        return true;
    }
    get isSingleton() {
        return true;
    }
    static mergeControlLines(...lines) {
        const rows = Array.from({ length: Math.max(...lines.map(l => l.rows.length)) }, (_, idx) => {
            const note = lines
                .map(l => l.rows[idx]?.note)
                .reduce((a, b) => b ?? a, null);
            const velocity = lines
                .map(l => l.rows[idx]?.velocity)
                .reduce((a, b) => b ?? a, null);
            return new Row(note, velocity);
        });
        const bpm = lines.map(l => l.bpm).reduce((a, b) => b ?? a, null);
        const pedal = lines.map(l => l.pedal).reduce((a, b) => b ?? a, null);
        const checkpoint = lines.some(l => l.checkpoint);
        return new ControlLine(lines[0].column, bpm, pedal, checkpoint, rows);
    }
    static deduplicateControlLines(lines) {
        const columns = new Map();
        for (const line of lines) {
            if (!columns.has(line.column)) {
                columns.set(line.column, [line]);
            }
            else {
                columns.get(line.column).push(line);
            }
        }
        return Array.from(columns.values()).map(lines => lines.length > 1 ? MusicGridRule.mergeControlLines(...lines) : lines[0]);
    }
}
Object.defineProperty(MusicGridRule, "EXAMPLE_GRID", {
    enumerable: true,
    configurable: true,
    writable: true,
    value: Object.freeze(GridData.create(['.']).addSymbol(new CustomIconSymbol('', GridData.create([]), 0, 0, 'MdMusicNote')))
});
Object.defineProperty(MusicGridRule, "CONFIGS", {
    enumerable: true,
    configurable: true,
    writable: true,
    value: Object.freeze([
        {
            type: ConfigType.ControlLines,
            default: [new ControlLine(0, 120, false, false, DEFAULT_SCALLE)],
            field: 'controlLines',
            description: 'Control Lines',
            configurable: false,
        },
        {
            type: ConfigType.NullableGrid,
            default: null,
            nonNullDefault: new GridData(5, 4).addRule(new MusicGridRule([new ControlLine(0, 120, false, false, DEFAULT_SCALLE)], null)),
            field: 'track',
            description: 'Track',
            configurable: true,
        },
    ])
});
Object.defineProperty(MusicGridRule, "SEARCH_VARIANTS", {
    enumerable: true,
    configurable: true,
    writable: true,
    value: [
        new MusicGridRule([new ControlLine(0, 120, false, false, DEFAULT_SCALLE)], null).searchVariant(),
    ]
});
export default MusicGridRule;
export const instance = new MusicGridRule([new ControlLine(0, 120, false, false, DEFAULT_SCALLE)], null);
