import * as TSU from "@panyam/tsutils";
import { AtomType, Group, Space } from "./";
import { WindowIterator } from "./iterators";
import { GridModel, GridCell, ColAlign, GridLayoutGroup } from "./grids";
import { isLine, isBlock } from "./notation";
const ZERO = TSU.Num.Fraction.ZERO;
const ONE = TSU.Num.Fraction.ONE;
export class Beat {
    constructor(index, role, offset, duration, barIndex, beatIndex, instance, prevBeat, nextBeat) {
        this.index = index;
        this.role = role;
        this.offset = offset;
        this.duration = duration;
        this.barIndex = barIndex;
        this.beatIndex = beatIndex;
        this.instance = instance;
        this.prevBeat = prevBeat;
        this.nextBeat = nextBeat;
        this.uuid = Beat.idCounter++;
        this.atomIsPlaceholder = false;
    }
    debugValue() {
        return {
            index: this.index,
            role: this.role.name,
            offset: this.offset.toString(),
            duration: this.duration.toString(),
            barIndex: this.barIndex,
            beatIndex: this.beatIndex,
            instance: this.instance,
            atom: this.atom.debugValue(),
        };
    }
    get endOffset() {
        return this.offset.plus(this.duration);
    }
    get filled() {
        return this.remaining.isZero;
    }
    get remaining() {
        return this.atom ? this.duration.minus(this.atom.duration, true) : this.duration;
    }
    add(atom) {
        if (this.remaining.cmp(atom.duration) < 0) {
            return false;
        }
        if (!this.atom) {
            this.atom = atom;
        }
        else {
            if (!this.atomIsPlaceholder) {
                this.atomIsPlaceholder = true;
                this.atom = new Group(this.atom).setDuration(ONE, true);
            }
            this.atom.addAtoms(true, atom);
        }
        return true;
    }
    get preMarkers() {
        const out = [];
        let curr = this.atom;
        while (curr != null) {
            for (const marker of curr.markersBefore || []) {
                out.push(marker);
            }
            if (curr.TYPE == AtomType.GROUP) {
                curr = curr.atoms.first;
            }
            else {
                curr = null;
            }
        }
        return out;
    }
    get postMarkers() {
        const out = [];
        let curr = this.atom;
        while (curr != null) {
            out.splice(0, 0, ...(curr.markersAfter || []));
            if (curr.TYPE == AtomType.GROUP) {
                curr = curr.atoms.last;
            }
            else {
                curr = null;
            }
        }
        return out;
    }
}
Beat.idCounter = 0;
export class BeatsBuilder {
    constructor(role, layoutParams, startOffset = ZERO, ...atoms) {
        this.role = role;
        this.layoutParams = layoutParams;
        this.startOffset = startOffset;
        this.beats = [];
        const [, [bar, beat, instance], beatOffset, index] = layoutParams.cycle.getPosition(startOffset);
        this.cycleIter = layoutParams.cycle.iterateBeats(bar, beat, instance);
        this.windowIter = new WindowIterator();
        this.beatOffset = beatOffset;
        this.startIndex = index;
        this.addAtoms(...atoms);
    }
    addAtoms(...atoms) {
        this.windowIter.push(...atoms);
        while (this.windowIter.hasMore) {
            let currBeat = this.beats[this.beats.length - 1];
            if (this.beats.length == 0 || currBeat.filled) {
                currBeat = this.addBeat();
            }
            const [remAtoms, filled] = this.windowIter.get(currBeat.remaining);
            TSU.assert(remAtoms.length > 0, "Atleast one element should have been available here");
            for (const atom of remAtoms) {
                TSU.assert(currBeat.add(atom), "Should return true as we are already using a duration iterator here");
                if (this.onAtomAdded)
                    this.onAtomAdded(atom, currBeat);
            }
            if (currBeat.filled) {
                if (this.onBeatFilled)
                    this.onBeatFilled(currBeat);
            }
        }
    }
    addBeat() {
        const numBeats = this.beats.length;
        const lastBeat = numBeats == 0 ? null : this.beats[numBeats - 1];
        const nextCP = this.cycleIter.next().value;
        const apb = this.layoutParams.beatDuration;
        const newBeat = new Beat(lastBeat == null ? this.startIndex : lastBeat.index + 1, this.role, lastBeat == null ? this.startOffset.minus(this.beatOffset).timesNum(apb, true) : lastBeat.endOffset, nextCP[1].timesNum(apb), nextCP[0][0], nextCP[0][1], nextCP[0][2], lastBeat, null);
        if (lastBeat == null && this.beatOffset.isGT(ZERO)) {
            newBeat.add(new Space(this.beatOffset.timesNum(apb)));
        }
        if (lastBeat)
            lastBeat.nextBeat = newBeat;
        this.beats.push(newBeat);
        if (this.onBeatAdded)
            this.onBeatAdded(newBeat);
        return newBeat;
    }
}
export class BeatColumn extends ColAlign {
    constructor(offset, endOffset, markerType) {
        super();
        this.offset = offset;
        this.endOffset = endOffset;
        this.markerType = markerType;
        this.atomSpacing = 5;
        offset = offset.factorized;
        endOffset = endOffset.factorized;
        this.key = BeatColumn.keyFor(offset, endOffset, markerType);
    }
    static keyFor(offset, endOffset, markerType = 0) {
        offset = offset.factorized;
        endOffset = endOffset.factorized;
        if (markerType < 0) {
            return ":" + offset.toString();
        }
        else if (markerType > 0) {
            return endOffset.toString() + ":";
        }
        else {
            return offset.toString() + ":" + endOffset.toString();
        }
    }
}
export class BeatColDAG {
    constructor(layoutGroup) {
        this.layoutGroup = layoutGroup;
        this.beatColumns = new Map();
    }
    getBeatColumn(offset, endOffset, markerType = 0) {
        const [bcol, newcreated] = this.ensureBeatColumn(offset, endOffset, markerType);
        if (newcreated) {
            if (markerType == 0) {
                const [prevcol] = this.ensureBeatColumn(offset, endOffset, -1);
                const [nextcol] = this.ensureBeatColumn(offset, endOffset, 1);
                prevcol.addSuccessor(bcol);
                bcol.addSuccessor(nextcol);
                for (const other of this.beatColumns.values()) {
                    if (other.markerType == -1 && endOffset.equals(other.offset)) {
                        nextcol.addSuccessor(other);
                    }
                    else if (other.markerType == 1 && other.endOffset.equals(offset)) {
                        other.addSuccessor(prevcol);
                    }
                }
            }
        }
        return bcol;
    }
    ensureBeatColumn(offset, endOffset, markerType = 0) {
        const key = BeatColumn.keyFor(offset, endOffset, markerType);
        let bcol = this.beatColumns.get(key) || null;
        const newcreated = bcol == null;
        if (!bcol) {
            bcol = new BeatColumn(offset, endOffset, markerType);
            this.beatColumns.set(key, bcol);
        }
        return [bcol, newcreated];
    }
}
export class GlobalBeatLayout {
    constructor(sharedGridLayoutGroup) {
        this.gridModelsForLine = new Map();
        this.roleBeatsForLine = new Map();
        this.beatColDAGsByLP = new Map();
        this.gridLayoutGroup = sharedGridLayoutGroup !== null && sharedGridLayoutGroup !== void 0 ? sharedGridLayoutGroup : new GridLayoutGroup();
    }
    getGridModelForLine(lineid) {
        let out = this.gridModelsForLine.get(lineid) || null;
        if (!out) {
            out = new GridModel();
            this.gridLayoutGroup.addGridModel(out);
            this.gridModelsForLine.set(lineid, out);
        }
        return out;
    }
    beatColDAGForLP(lpid) {
        let out = this.beatColDAGsByLP.get(lpid) || null;
        if (!out) {
            out = new BeatColDAG(this.gridLayoutGroup);
            this.beatColDAGsByLP.set(lpid, out);
        }
        return out;
    }
    addLine(line) {
        var _a, _b;
        const gridModel = this.getGridModelForLine(line.uuid);
        (_a = gridModel.eventHub) === null || _a === void 0 ? void 0 : _a.startBatchMode();
        this.lineToRoleBeats(line, gridModel);
        (_b = gridModel.eventHub) === null || _b === void 0 ? void 0 : _b.commitBatch();
    }
    processBlock(block) {
        for (const child of block.children()) {
            this.processBlockItem(child);
        }
    }
    processBlockItem(item) {
        if (isLine(item)) {
            const line = item;
            if (!line.isEmpty && line.layoutParams != null) {
                this.addLine(line);
            }
        }
        else if (isBlock(item)) {
            this.processBlock(item);
        }
    }
    lineToRoleBeats(line, gridModel) {
        const lp = line.layoutParams;
        const roleBeats = [];
        this.roleBeatsForLine.set(line.uuid, roleBeats);
        const lineOffset = line.offset.divbyNum(lp.beatDuration);
        for (const role of line.roles) {
            const bb = new BeatsBuilder(role, lp, lineOffset, ...role.atoms);
            roleBeats.push(bb.beats);
            for (const beat of bb.beats) {
                this.addBeat(beat, gridModel);
            }
        }
        return roleBeats;
    }
    addBeat(beat, gridModel) {
        const line = beat.role.line;
        const lp = line.layoutParams;
        const beatColDAG = this.beatColDAGForLP(lp.uuid);
        const [layoutLine, layoutColumn, rowOffset] = lp.getBeatLocation(beat);
        const colEnd = rowOffset.plus(beat.duration, true);
        const bcol = beatColDAG.getBeatColumn(rowOffset, colEnd, 0);
        const roleIndex = beat.role.line.indexOfRole(beat.role.name);
        const nthLine = Math.floor(beat.index / lp.totalBeats);
        const realLine = lp.lineBreaks.length * nthLine + layoutLine;
        const realRow = line.roles.length * realLine + roleIndex;
        const realCol = 1 + layoutColumn * 3;
        const preMarkers = beat.preMarkers;
        if (preMarkers.length > 0) {
            const val = {
                beat: beat,
                markers: preMarkers,
            };
            const precol = beatColDAG.getBeatColumn(rowOffset, colEnd, -1);
            gridModel.setValue(realRow, realCol - 1, val, (gridRow, col) => {
                const cell = new GridCell(gridRow, col);
                cell.colAlign = precol;
                return cell;
            });
        }
        const postMarkers = beat.postMarkers;
        if (postMarkers.length > 0) {
            const val = {
                beat: beat,
                markers: postMarkers,
            };
            const postcol = beatColDAG.getBeatColumn(rowOffset, colEnd, 1);
            gridModel.setValue(realRow, realCol + 1, val, (gridRow, col) => {
                const cell = new GridCell(gridRow, col);
                cell.colAlign = postcol;
                return cell;
            });
        }
        return gridModel.setValue(realRow, realCol, beat, (gridRow, col) => {
            const cell = new GridCell(gridRow, col);
            cell.colAlign = bcol;
            return cell;
        });
    }
}
//# sourceMappingURL=beats.js.map