/**
 * This file is part of Satie music engraver <https://github.com/jnetterf/satie>.
 * Copyright (C) Joshua Netterfield <joshua.ca> 2015 - present.
 * 
 * Satie is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * Satie is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with Satie.  If not, see <http://www.gnu.org/licenses/>.
 */

import {Time, BeamType, Beam, Count, TimeModification, Direction} from "musicxml-interfaces";
import {IAny} from "musicxml-interfaces/operations";
import {
    buildNote, patchNote, INoteBuilder,
    buildBarline, patchBarline, IBarlineBuilder,
    buildBeam,
    buildAttributes, patchAttributes, IAttributesBuilder,
    buildDirection, patchDirection, IDirectionBuilder,
    buildPrint, patchPrint, IPrintBuilder,
} from "musicxml-interfaces/builders";
import {find, forEach, last, some, times, findIndex, findLastIndex, extend, isInteger} from "lodash";
import * as invariant from "invariant";

import {Document, IMeasure, IMeasurePart, ISegment, Type, IModel} from "./document";

import {IChord, count, dots, timeModification, divisions as calcDivisions,
    rest, countToIsBeamable, beams} from "./private_chordUtil";
import {IAttributesSnapshot} from "./private_attributesSnapshot";
import {getBeamingPattern} from "./private_metre_checkBeaming";
import {simplifyRests} from "./private_metre_modifyRest";
import {cloneObject} from "./private_util";

function _prependPatch(...prefix: any[]) {
    return function __prependPatch(patch: IAny) {
        patch.p = prefix.concat(patch.p);
        return patch;
    };
}

function genUUID(): number {
    const MAX_SAFE_INTEGER = 9007199254740991;
    return Math.floor(Math.random() * MAX_SAFE_INTEGER);
}

function moreImportant(type: Type, model: IModel, doc: Document) {
    switch (type) {
        case Type.Print:
            return !doc.modelHasType(model, Type.VisualCursor);
        case Type.Grouping:
            return !doc.modelHasType(model, Type.Print, Type.VisualCursor);
        case Type.FiguredBass:
            return !doc.modelHasType(model, Type.Print, Type.Grouping, Type.VisualCursor);
        case Type.Attributes:
            return !doc.modelHasType(model, Type.Print, Type.Grouping, Type.FiguredBass, Type.VisualCursor);
        case Type.Sound:
            return !doc.modelHasType(model, Type.Print, Type.Grouping, Type.FiguredBass, Type.Attributes, Type.VisualCursor);
        case Type.Direction:
            return !doc.modelHasType(model, Type.Print, Type.Grouping, Type.FiguredBass, Type.Attributes, Type.Sound, Type.VisualCursor);
        case Type.Harmony:
            return false;
        case Type.Proxy:
            return false;
        case Type.Spacer:
            return false;
        case Type.Chord:
        case Type.VisualCursor:
            return true;
    }
}

export class StaffBuilder {
    private _segment: ISegment;
    private _patches: IAny[] = [];
    private _document: Document;
    private _idx: number;

    get patches(): IAny[] {
        return this._patches.slice();
    }

    constructor(segment: ISegment, document: Document, idx?: number) {
        this._segment = segment;
        this._document = document;
        this._idx = idx;
    }

    at(idx: number): this {
        this._idx = idx;
        return this;
    }

    next(): this {
        ++this._idx;
        return this;
    }

    atDiv(div: number, type: Type): this {
        let currDiv = 0;
        for (let i = 0; i < this._segment.length; ++i) {
            if (div < currDiv + this._segment[i].divCount ||
                    div === currDiv + this._segment[i].divCount &&
                    moreImportant(type, this._segment[i], this._document)) {
                let start = currDiv;
                let end = currDiv + this._segment[i].divCount;
                if (div === start &&moreImportant(type, this._segment[i], this._document)) {
                    return this.at(i);
                } else if (div === end) {
                    return this.at(i + 1);
                } else {
                    let s1 = div - start;
                    let s2 = end - div;
                    return this
                        .at(i)
                        .setDivCount(s1)
                        .next()
                        .insertSpacer(s2)
                        .at(i + 1);
                }
            }
            currDiv += this._segment[i].divCount;
        }
        let diff = div - currDiv;
        if (diff) {
            // Note: we should enforce this to not be possible, since the staff segment should
            // always be full.
            return this
                .at(this._segment.length)
                .insertSpacer(diff)
                .next();
        }
        return this.at(this._segment.length);
    }

    setDivCount(divCount: number): this {
        this._patches = this._patches.concat({
            oi: divCount,
            od: this._segment[this._idx].divCount,
            p: [this._idx, "divCount"],
        });

        return this;
    }

    barline(builder: (build: IBarlineBuilder) => IBarlineBuilder): this {
        let model = this._segment[this._idx] as any;
        invariant(model, "no such model");
        invariant(this._document.modelHasType(model, Type.Barline), "model is not barline");
        this._patches = this._patches.concat(
            patchBarline(model, builder).map(_prependPatch(this._idx)));
        return this;
    }

    insertBarline(builder: (build: IBarlineBuilder) => IBarlineBuilder): this {
        let li = buildBarline(builder);
        let p = [this._idx];
        this._patches = this._patches.concat({li, p});
        return this;
    }

    attributes(builder: (builder: IAttributesBuilder) => IAttributesBuilder): this {
        let model = this._segment[this._idx] as any;
        invariant(model, "no such model");
        invariant(this._document.modelHasType(model, Type.Attributes), "model is not attributes");
        this._patches = this._patches.concat(
            patchAttributes(model, builder).map(_prependPatch(this._idx)));
        return this;
    }

    insertAttributes(builder: (build: IAttributesBuilder) => IAttributesBuilder): this {
        let li = buildAttributes(builder);
        let p = [this._idx];
        this._patches = this._patches.concat({li, p});
        return this;
    }

    direction(builder: (builder: IDirectionBuilder) => IDirectionBuilder): this {
        let model = this._segment[this._idx] as any;
        invariant(model, "no such model");
        invariant(this._document.modelHasType(model, Type.Direction), "model is not direction");
        this._patches = this._patches.concat(
            patchDirection(model, builder).map(_prependPatch(this._idx)));
        return this;
    }

    insertDirection(builder: Direction | ((build: IDirectionBuilder) => IDirectionBuilder)): this {
        if (typeof builder === "function") {
            let li = buildDirection(builder);
            let p = [this._idx];
            this._patches = this._patches.concat({li, p});
            return this;
        }

        let p = [this._idx];
        let li = cloneObject(builder);
        li._class = "Direction";
        this._patches = this._patches.concat({li, p});
        return this;
    }

    print(builder: (builder: IPrintBuilder) => IPrintBuilder): this {
        let model = this._segment[this._idx] as any;
        invariant(model, "no such model");
        invariant(this._document.modelHasType(model, Type.Print), "model is not Print");
        this._patches = this._patches.concat(
        patchPrint(model, builder).map(_prependPatch(this._idx)));
        return this;
    }

    insertPrint(builder: (build: IPrintBuilder) => IPrintBuilder): this {
        let li = buildPrint(builder);
        let p = [this._idx];
        this._patches = this._patches.concat({li, p});
        return this;
    }

    insertSpacer(divs: number): this {
        this._patches = this._patches.concat({
            li: {
                _class: "Spacer",
                divCount: divs,
            },
            p: [this._idx],
        });
        return this;
    }

    remove(): this {
        this._patches = this._patches.concat(
            {
                p: [this._idx],
                ld: this._segment[this._idx]
            }
        );
        return this;
    }
}

export class VoiceBuilder {
    private _segment: ISegment;
    private _patches: IAny[] = [];
    private _document: Document;
    private _idx: number;

    get patches(): IAny[] {
        return this._patches.slice();
    }

    constructor(segment: ISegment, document: Document, idx?: number) {
        this._segment = segment;
        this._document = document;
        this._idx = idx;
    }

    at(idx: number): this {
        this._idx = idx;
        return this;
    }

    next(): this {
        ++this._idx;
        return this;
    }

    addVisualCursor(): this {
        this._patches = this._patches.concat(
            {
                li: {
                    _class: "VisualCursor",
                },
                p: [this._idx],
            }
        );
        return this;
    }

    note(noteIDX: number, builder: (build: INoteBuilder) => INoteBuilder): this {
        let model = this._segment[this._idx] as any;
        invariant(model, "no such model");
        invariant(this._document.modelHasType(model, Type.Chord), "model is not a chord");
        let note = model[noteIDX];
        invariant(note, "invalid note");
        this._patches = this._patches.concat(
            patchNote(note, builder).map(_prependPatch(this._idx, "notes", noteIDX)));
        return this;
    }

    insertChord(builders: ((build: INoteBuilder) => INoteBuilder)[]): this {
        invariant(!isNaN(this._idx), "%s must be a number", this._idx);
        let li: IChord = builders.map(builder => buildNote(builder));
        li._class = "Chord";
        invariant(li[0].noteType.duration, "Invalid note type");
        let p = [this._idx];
        this._patches = this._patches.concat({li, p});
        return this;
    }

    insertNote(position: number, builder: (builder: INoteBuilder) => INoteBuilder): this {
        let model = this._segment[this._idx] as any;
        invariant(model, "no such model");
        invariant(this._document.modelHasType(model, Type.Chord), "model is not a chord");
        let li = buildNote(builder);
        let chord = model as IChord;
        invariant(chord[position - 1] || chord[position + 1] || !chord.length, "Invalid position for note");
        invariant(li.noteType.duration, "Invalid note type");
        let p = [this._idx, "notes", position];
        this._patches = this._patches.concat({p, li});
        return this;
    }

    remove(): this {
        this._patches = this._patches.concat(
            {
                p: [this._idx],
                ld: this._segment[this._idx]
            }
        );
        return this;
    }
}

export class PartBuilder {
    private _part: IMeasurePart;
    private _patches: IAny[] = [];
    private _document: Document;

    get patches(): IAny[] {
        return this._patches.slice();
    }

    constructor(part: IMeasurePart, document: Document) {
        this._part = part;
        this._document = document;
    }

    voice(voiceID: number, builder: (build: VoiceBuilder) => VoiceBuilder, idx?: number): this {
        const voice = this._part ? this._part.voices[voiceID] : null;
        invariant(!this._part || Boolean(voice), "invalid voice");
        this._patches = this._patches.concat(
            builder(new VoiceBuilder(voice, this._document, idx))
                .patches
                .map(_prependPatch("voices", voiceID))
        );
        return this;
    }

    staff(staffID: number, builder: (build: StaffBuilder) => StaffBuilder, idx?: number): this {
        const staff = this._part ? this._part.staves[staffID] : null;
        invariant(!this._part || Boolean(staff), "invalid staff");
        this._patches = this._patches.concat(
            builder(new StaffBuilder(staff, this._document, idx))
                .patches
                .map(_prependPatch("staves", staffID))
        );
        return this;
    }
}

export class MeasureBuilder {
    private _measure: IMeasure;
    private _patches: IAny[] = [];
    private _document: Document;

    get patches(): IAny[] {
        return this._patches.slice();
    }

    constructor(measure: IMeasure, document: Document) {
        this._measure = measure;
        this._document = document;
    }

    part(partID: string, builder: (build: PartBuilder) => PartBuilder): this {
        const part = this._measure ? this._measure.parts[partID] : null;
        invariant(!this._measure || Boolean(part), "invalid part id");
        this._patches = this._patches.concat(
            builder(new PartBuilder(part, this._document))
                .patches
                .map(_prependPatch("parts", partID))
        );
        return this;
    }
}

export class DocumentBuilder {
    private _doc: Document;
    private _patches: IAny[] = [];

    get patches(): IAny[] {
        return this._patches.slice();
    }

    constructor(doc: Document) {
        this._doc = doc;
    }

    measure(measureUUID: number, builder: (build: MeasureBuilder) => MeasureBuilder): this {
        let measure = find(this._doc.measures, it => it.uuid === measureUUID);
        invariant(Boolean(measure), `invalid measure uuid ${measureUUID}`);
        this._patches = this._patches.concat(
            builder(new MeasureBuilder(measure, this._doc))
                .patches
                .map(_prependPatch(measureUUID))
        );
        return this;
    }

    insertMeasure(measureIndex: number, builder: (build: MeasureBuilder) => MeasureBuilder, uuid: number = genUUID()): this {
        this._patches = this._patches.concat({
            li: {
                uuid,
            },
            p: ["measures", measureIndex],
        });

        this._patches = this._patches.concat(builder(new MeasureBuilder(null, this._doc))
            .patches
            .map(_prependPatch(uuid))
        );
        return this;
    }

    removeMeasure(measureIndex: number): this {
        this._patches = this._patches.concat({
            ld: JSON.parse(JSON.stringify(this._doc.measures[measureIndex])),
            p: ["measures", measureIndex],
        });

        return this;
    }
}

export class ModelMetreMutationSpec {
    idx: number;
    oldIdx: number;
    start: number;
    previousDivisions: number;
    newDivisions: number;
    newCount: number;
    newDots: number;
    newTimeModification: TimeModification;
    time: Time;
    rest: boolean;
    beam: Beam[];
    touched: boolean;
    private _originalModel: IModel;
    constructor(spec: {
                idx: number;
                oldIdx: number;
                start: number;
                previousDivisions: number;
                newDivisions: number;
                newCount: number;
                newDots: number;
                newTimeModification: TimeModification;
                time: Time;
                rest: boolean;
                beam: Beam[];
                touched: boolean;
            }, originalModel?: IModel) {
        extend(this, spec);
        this._originalModel = originalModel;
    }

    toSpec(): IModel {
        if (!this._originalModel) {
            throw new Error("Only valid for mutations!");
        }
        const originalModel = cloneObject(this._originalModel) as any;
        if (originalModel._class === "Chord" || originalModel.length) {
            const chordModel: IChord = originalModel;
            forEach(chordModel, c => {
                c.noteType.duration = this.newCount;
                if (this.rest) {
                    c.rest = c.rest || {};
                    delete c.pitch;
                } else {
                    delete c.rest;
                }
                if (this.newTimeModification) {
                    c.timeModification = this.newTimeModification;
                } else {
                    delete c.timeModification;
                }
                if (!isNaN(this.newDots)) {
                    c.dots = times(this.newDots, () => ({}));
                } else {
                    delete c.dots;
                }
            });
            return chordModel as any;
        } else {
            return originalModel;
        }
    }
}

interface IMetreInfo {
    segments: {[key: string]: ISegment};
    attributes: {[key: string]: IAttributesSnapshot};
    elementInfos: {[key: string]: ModelMetreMutationSpec[]};
    elementInfoByChord: {[key: string]: ModelMetreMutationSpec};
}

function getMutationInfo(document: Document, patches: IAny[]) {
    const segments: {[key: string]: ISegment} = {};
    const attributes: {[key: string]: IAttributesSnapshot} = {};
    const elementInfos: {[key: string]: ModelMetreMutationSpec[]} = {};
    const elementInfoByChord: {[key: string]: ModelMetreMutationSpec} = {};

    patches.forEach(patch => {
        if (patch.p[0] === "measures") {
            // XXX: implement!
            return;
        }
        let measureUUID = parseInt(patch.p[0] as string, 10);
        let measure = find(document.measures, doc => doc.uuid === measureUUID);
        if (!measure) {
            // TODO: validate blank measures
            return;
        }
        if (patch.p[1] !== "parts") {
            return;
        }
        const part = measure.parts[patch.p[2]];
        invariant(part, `part ${patch.p[2]} should exist in measure ${measureUUID}`);
        if (patch.p[3] === "staves") {
            return;
        }
        invariant(patch.p[3] === "voices", "only voices are supported here");
        const voice = part.voices[patch.p[4] as number];
        invariant(voice, `expected to find voice ${patch.p[4]} in part ${patch.p[2]} in measure ${measureUUID}`);

        const segID = patch.p.slice(0, 5).join("++");
        if (!segments[segID]) {
            segments[segID] = voice;
            let currDiv = 0;
            attributes[segID] = (document.search(part.staves[1], 0, Type.Attributes)[0] as any)._snapshot as IAttributesSnapshot;
            let time = attributes[segID].time; // TODO: TS changes
            let divisions = attributes[segID].divisions;

            elementInfos[segID] = voice.reduce((elementInfo, model, idx) => {
                if (!document.modelHasType(model, Type.Chord)) {
                    return elementInfo.concat(new ModelMetreMutationSpec({
                        idx: idx,
                        oldIdx: idx,
                        start: currDiv,
                        previousDivisions: 0,
                        newDivisions: 0,
                        newCount: 0,
                        newDots: 0,
                        newTimeModification: null,
                        time: time,
                        rest: true,
                        beam: null,
                        touched: false,
                    }, model));
                }
                let divs = calcDivisions(model, {time, divisions});
                let info = new ModelMetreMutationSpec({
                    idx: idx,
                    oldIdx: idx,
                    start: currDiv,
                    previousDivisions: divs,
                    newDivisions: divs,
                    newCount: count(model),
                    newDots: dots(model),
                    newTimeModification: timeModification(model),
                    time: time,
                    rest: !!rest(model),
                    beam: beams(model),
                    touched: false,
                }, model);
                elementInfoByChord[model.key] = info;
                currDiv += divs;
                return elementInfo.concat(info);
            }, [] as ModelMetreMutationSpec[]);
        }
        let divisions = attributes[segID].divisions;

        if (patch.p.length === 6) {
            if (patch.li) {
                const isChord = patch.li._class === "Chord";
                const b = isChord ? beams(patch.li) : null;
                const c = isChord ? count(patch.li) : 0;
                const d = isChord ? dots(patch.li) : 0;
                const tm = isChord ? timeModification(patch.li) : null;
                const isRest = isChord && !!rest(patch.li);
                const divs = isChord ?
                    calcDivisions(patch.li, {time: attributes[segID].time, divisions}) :
                    0;
                let start: number;
                const spliceIdx = parseInt(patch.p[5] as string, 10);
                invariant(isInteger(spliceIdx) && !isNaN(spliceIdx), "Expected an integer");
                if (spliceIdx === 0) {
                    start = 0;
                } else {
                    start = elementInfos[segID][spliceIdx - 1].newDivisions + elementInfos[segID][spliceIdx - 1].start;
                }

                let newInfo = new ModelMetreMutationSpec({
                    idx: spliceIdx,
                    oldIdx: undefined,
                    newCount: c,
                    newDivisions: divs,
                    newDots: d,
                    previousDivisions: 0,
                    newTimeModification: tm,
                    start: start,
                    time: attributes[segID].time,
                    rest: isRest,
                    beam: b,
                    touched: true,
                });

                for (let i = spliceIdx; i < elementInfos[segID].length; ++i) {
                    elementInfos[segID][i].start += divs;
                    elementInfos[segID][i].idx += 1;
                }

                elementInfos[segID].splice(spliceIdx, 0, newInfo);
            }
            if (patch.ld) {
                let divs = patch.ld._class === "Chord" ?
                    calcDivisions(patch.ld, {time: attributes[segID].time, divisions}) :
                    0;
                let spliceIdx = parseInt(patch.p[5] as string, 10);

                elementInfos[segID].splice(spliceIdx, 1);

                for (let i = spliceIdx; i < elementInfos[segID].length; ++i) {
                    elementInfos[segID][i].start -= divs;
                    elementInfos[segID][i].idx -= 1;
                }
            }
            return;
        }

        const el = voice[patch.p[5] as number];
        invariant(el, `expected to find element $${patch.p[5]} in part ${patch.p[2]} in voice ${patch.p[4]} in measure ${measureUUID}`);

        if (!document.modelHasType(el, Type.Chord) || patch.p[6] !== "notes" || patch.p[7] !== 0) {
            return;
        }

        let info = elementInfoByChord[el.key];

        if (patch.p.length === 9 && patch.p[8] === "pitch") {
            info.touched = true;
            if (patch.oi !== undefined) {
                info.rest = !patch.oi;
            } else if (patch.od !== undefined) {
                info.rest = true;
            }
        }

        if (patch.p.length === 9 && patch.p[8] === "rest") {
            info.touched = true;
            if (patch.oi !== undefined) {
                info.rest = !!patch.oi;
            } else if (patch.od !== undefined) {
                info.rest = false;
            }
        }

        if (patch.p[8] === "noteType" && patch.p[9] === "duration") {
            if (patch.oi) {
                info.newCount = patch.oi;
            } else {
                invariant(false, "noteType is required...");
            }
        }

        if (patch.p.length === 9 && patch.p[8] === "dots") {
            if (patch.oi) {
                info.newDots = patch.oi.length;
            } else if (patch.od) {
                info.newDots = 0;
            }
        }
        info.newDivisions = calcDivisions(
            {
                count: info.newCount,
                dots: info.newDots,
                timeModification: info.newTimeModification
            },
            {
                time: info.time,
                divisions,
            }
        );
        if (info.newDivisions !== info.previousDivisions) {
            info.touched = true;
        }
    });
    return {
        segments,
        attributes,
        elementInfos,
        elementInfoByChord
    };
}
function fixMetre(document: Document, patches: IAny[]): IAny[] {
    patches = patches.slice();

    let segments: {[key: string]: ISegment};
    let attributes: {[key: string]: IAttributesSnapshot};
    let elementInfos: {[key: string]: ModelMetreMutationSpec[]};

    const mi = getMutationInfo(document, patches);
    segments = mi.segments;
    attributes = mi.attributes;
    elementInfos = mi.elementInfos;

    forEach(elementInfos, (voiceInfo, key) => {
        const anyChanged = voiceInfo.some(n => n.touched);
        if (!anyChanged) {
            return;
        }

        const restSpecs = simplifyRests(voiceInfo, document, attributes[key]);

        patches = patches.concat(restSpecs.map((spec, idx) => (extend(
            {}, spec, {
                p: (key.split("++") as (number | string)[]).concat(spec.p),
            }
        ))));
    });

    return patches;
}

function fixBarlines(doc: Document, patches: IAny[]): IAny[] {
    // XXX: FIXME
    // const measureCount = doc.measures.length;
    // const previouslyLastMeasure = doc.measures[measureCount - 1];
    // forEach(previouslyLastMeasure.parts, (part, partName) => {
    //     const segment = part.staves[1];
    //     const barlineIdx = findLastIndex(segment, el => doc.modelHasType(el, Type.Barline));

    //     patches = patches.slice();
    //     patches.forEach(patch => {
    //         if (patch.p[0] === "measures" &&
    //                 patch.p.length === 2 &&
    //                 patch.p[1] === previouslyLastMeasure.idx + 1) {

    //             const removeDoubleBarline = createPatch(false, doc,
    //                 previouslyLastMeasure.uuid, partName,
    //                 part => part.staff(1, staff => staff
    //                     .barline(barline => barline
    //                         .barStyle(barStyle => barStyle
    //                             .data(BarStyleType.Regular)
    //                         )
    //                     ),
    //                     barlineIdx
    //                 )
    //             );
    //             patches = patches.concat(removeDoubleBarline);
    //         }
    //     });
    // });
    return patches;
}

function fixCursor(doc: Document, patches: IAny[]): IAny[] {
    let {segments, attributes, elementInfos} = getMutationInfo(doc, patches);
    const newCursor = patches.filter(patch => patch.li && patch.li._class === "VisualCursor");
    if (!newCursor.length) {
        return patches;
    }
    invariant(newCursor.length === 1, "Limit 1 cursor operation per patch");
    patches = patches.slice();
    forEach(doc.measures, (measure) => {
        forEach(measure.parts, (part, partName) => {
            forEach(part.voices, (voice, voiceIDX) => {
                if (!voice) {
                    return;
                }
                const segID = [measure.uuid, "parts", partName, "voices", voiceIDX].join("++");
                const segInfo = elementInfos[segID];
                if (segInfo) {
                    let offset = 0;
                    forEach(segInfo, element => {
                        if (!isNaN(element.idx) && !isNaN(element.oldIdx) &&
                                doc.modelHasType(voice[element.oldIdx], Type.VisualCursor)) {
                            patches.push({
                                p: [measure.uuid, "parts", partName, "voices", voiceIDX, element.idx + offset],
                                ld: JSON.parse(JSON.stringify(voice[element.oldIdx])),
                            });
                            offset -= 1;
                        }
                    });
                } else {
                    let offset = 0;
                    forEach(voice, (el, idx) => {
                        if (doc.modelHasType(el, Type.VisualCursor)) {
                            patches.push({
                                p: [measure.uuid, "parts", partName, "voices", voiceIDX, idx + offset],
                                ld: JSON.parse(JSON.stringify(el)),
                            });
                            offset -= 1;
                        }
                    });
                }
            });
        });
    });
    return patches;
}

const COUNT_TO_BEAMS: {[key: number]: number} = {
    [Count.Eighth]: 1,
    [Count._16th]: 2,
    [Count._32nd]: 3,
    [Count._64th]: 4,
    [Count._128th]: 5,
    [Count._256th]: 6,
    [Count._512th]: 7
};

function addBeams(document: Document, patches: IAny[]): IAny[] {
    patches = patches.slice();

    let {segments, elementInfos} = getMutationInfo(document, patches);

    forEach(elementInfos, (voiceInfo, key) => {
        const segment = segments[key];
        const time = voiceInfo[0].time;
        const stdBP = getBeamingPattern(time); // TODO: TS changes in bar
        // const cleanBP = getBeamingPattern(time, "clean");
        // const altBP = getBeamingPattern(time, "alt");
        let prevInfo: ModelMetreMutationSpec;
        let beamGroup: number[] = [];
        let beamBeams: number[] = [];
        let inCandidate: boolean[] = [];

        let beamingPattern: IChord[] = stdBP;

        function applyCandidate() {
            // Remove all rests at the end and beginning.
            const start = findIndex(beamGroup, i => !voiceInfo[i].rest);
            const end = findLastIndex(beamGroup, i => !voiceInfo[i].rest);
            beamBeams = beamBeams.slice(start, end + 1);
            beamGroup = beamGroup.slice(start, end + 1);

            if (beamGroup.length < 2) {
                return;
            }

            // Mark elements in the candidate
            beamGroup.forEach(b => inCandidate[b] = true);
            if (!some(beamGroup, i => voiceInfo[i].touched)) {
                // We did not modify this beam group, so don't change it here.
                return;
            }

            patches = patches.concat(beamGroup.map((i, j) => {
                let type: BeamType = null;
                const beams = times(beamBeams[j], beamNumber => {
                    if (i === beamGroup[0] || beamBeams[j - 1] < beamNumber + 1) {
                        if (i === last(beamGroup) || beamBeams[j + 1] < beamNumber + 1) {
                            // HACK HACK HACK -- it's more complex than this
                            type = (j === 0 ? BeamType.ForwardHook : BeamType.BackwardHook);
                        } else {
                            type = BeamType.Begin;
                        }
                    } else if (i === last(beamGroup) || beamBeams[j + 1] < beamNumber + 1) {
                        type = BeamType.End;
                    } else {
                        type = BeamType.Continue;
                    }

                    return buildBeam(beam => beam
                        .number(beamNumber + 1)
                        .type(type));
                });

                let miniP: IAny = {
                    p: (key.split("++") as (number | string)[]).concat([i, "notes", 0, "beams"]),
                    oi: beams
                };
                if (voiceInfo[i].beam) {
                    miniP.od = voiceInfo[i].beam;
                }
                return miniP;
            }));
        }

        let bpIDX = -1;
        let divisionsInCurrentBucket = 0;
        function advanceBP(divs: number) {
            divisionsInCurrentBucket -= divs;
            if (divisionsInCurrentBucket <= 0) {
                applyCandidate();
                prevInfo = null;
                beamGroup = [];
                beamBeams = [];
                ++bpIDX;
                if (!beamingPattern[bpIDX]) {
                    // End of bar / overflowed bar
                    divisionsInCurrentBucket = Infinity;
                    return;
                }
                let next = calcDivisions(beamingPattern[bpIDX], {time, divisions: segment.divisions});
                divisionsInCurrentBucket += next;
            }
        }
        advanceBP(0);

        voiceInfo.forEach((elInfo, originalIdx) => {
            if (!elInfo.newDivisions) {
                // Skip this non-note.
                return;
            }
            let divs = calcDivisions(
                {
                    count: elInfo.newCount,
                    dots: elInfo.newDots,
                },
                {
                    time,
                    divisions: segment.divisions,
                },
                true
            );

            const isCandidate = countToIsBeamable[elInfo.newCount] &&
                (!elInfo.rest || elInfo.beam) &&
                divs <= divisionsInCurrentBucket;

            if (isCandidate) {
                prevInfo = elInfo;
                beamGroup.push(originalIdx);
                beamBeams.push(COUNT_TO_BEAMS[elInfo.newCount]);
            } else {
                applyCandidate();
                prevInfo = null;
                beamGroup = [];
                beamBeams = [];
            }
            advanceBP(divs);
        });
        applyCandidate();

        // Now remove invalid beams!
        voiceInfo.forEach((elInfo, i) => {
            if (elInfo.beam && !inCandidate[i]) {

                patches = patches.concat({
                    p: (key.split("++") as (number | string)[]).concat([i, "notes", 0, "beams"]),
                    od: elInfo.beam
                });
            }
        });
    });

    return patches;
}

function cleanupPatches(document: Document, patches: IAny[]): IAny[] {
    patches = fixMetre(document, patches);
    patches = addBeams(document, patches);
    patches = fixBarlines(document, patches);
    patches = fixCursor(document, patches);
    return patches;
}

export default function createPatch(
            isPreview: boolean,
            document: Document,
            measure: number,
            part: string,
            builder: (partBuilder: PartBuilder) => PartBuilder):
        IAny[];

export default function createPatch(
            isPreview: boolean,
            document: Document,
            builder: (build: DocumentBuilder) => DocumentBuilder):
        IAny[];

export default function createPatch(
            isPreview: boolean,
            document: Document,
            operations: IAny[]): IAny[];

export default function createPatch(isPreview: boolean,
        document: Document,
        builderOrMeasure: number | ((build: DocumentBuilder) => DocumentBuilder) | IAny[],
        part?: string,
        partBuilder?: (partBuilder: PartBuilder) => PartBuilder) {
    let patches: IAny[];
    if (builderOrMeasure instanceof Array) {
        patches = cleanupPatches(document, builderOrMeasure);
    } else if (typeof builderOrMeasure === "function") {
        invariant(part === undefined && partBuilder === undefined, "createPatch: invalid usage");
        let builder = builderOrMeasure as ((build: DocumentBuilder) => DocumentBuilder);
        patches = builder(new DocumentBuilder(document)).patches;
        if (!isPreview) {
            patches = cleanupPatches(document, patches);
        }
    } else {
        let measure = builderOrMeasure as any as number;
        let builder = partBuilder;
        patches = createPatch(isPreview, document, document => document
            .measure(measure, measure => measure
                .part(part, builder)));
    }
    return patches;
}
