import {MusicSystem} from "./MusicSystem";
import {GraphicalStaffEntry} from "./GraphicalStaffEntry";
import {SourceMeasure} from "../VoiceData/SourceMeasure";
import {StaffLine} from "./StaffLine";
import {Staff} from "../VoiceData/Staff";
import {GraphicalObject} from "./GraphicalObject";
import {ClefInstruction} from "../VoiceData/Instructions/ClefInstruction";
import {KeyInstruction} from "../VoiceData/Instructions/KeyInstruction";
import {RhythmInstruction} from "../VoiceData/Instructions/RhythmInstruction";
import {Fraction} from "../../Common/DataObjects/Fraction";
import {Voice} from "../VoiceData/Voice";
import {VoiceEntry} from "../VoiceData/VoiceEntry";
import {SystemLinesEnum} from "./SystemLinesEnum";
import {BoundingBox} from "./BoundingBox";
import {PointF2D} from "../../Common/DataObjects/PointF2D";

/**
 * Represents a measure in the music sheet (one measure in one staff line)
 */
export abstract class GraphicalMeasure extends GraphicalObject {
    protected firstInstructionStaffEntry: GraphicalStaffEntry;
    protected lastInstructionStaffEntry: GraphicalStaffEntry;

    constructor(staff: Staff = undefined, parentSourceMeasure: SourceMeasure = undefined, staffLine: StaffLine = undefined) {
        super();
        this.parentStaff = staff;
        this.parentSourceMeasure = parentSourceMeasure;
        this.parentStaffLine = staffLine;
        if (staffLine) {
            this.parentStaff = staffLine.ParentStaff;
            this.PositionAndShape = new BoundingBox(this, staffLine.PositionAndShape);
        } else {
            this.PositionAndShape = new BoundingBox(this);
        }
        this.PositionAndShape.BorderBottom = 4;
        if (this.parentSourceMeasure) {
            this.measureNumber = this.parentSourceMeasure.MeasureNumber;
        }

        this.staffEntries = [];
    }

    public parentSourceMeasure: SourceMeasure;
    public staffEntries: GraphicalStaffEntry[];
    /** The clef of the first note of the measure (the clef the measure starts with). */
    public InitiallyActiveClef: ClefInstruction;
    /**
     * The x-width of possibly existing: repetition start line, clef, key, rhythm.
     */
    public beginInstructionsWidth: number;
    /**
     * The minimum possible x-width of all staff entries without overlapping.
     */
    public minimumStaffEntriesWidth: number;
    /**
     * Will be set by music system builder while building systems.
     */
    public staffEntriesScaleFactor: number;
    /**
     * The x-width of possibly existing: repetition end line, clef.
     */
    public endInstructionsWidth: number;
    public hasError: boolean;
    /**
     * Whether or not this measure is nothing but rest(s).
     * Also see SourceMeasure.allRests, which is not the same, because a source measure can have multiple staffs/graphicalMeasures.
     */
    public hasOnlyRests: boolean = false;

    private parentStaff: Staff;
    private parentMusicSystem: MusicSystem;
    private measureNumber: number = -1;
    private parentStaffLine: StaffLine;
    /** Used to show key, rhythm changes at the end of the system, has MeasureNumber < 0, because never set. */
    public IsExtraGraphicalMeasure: boolean;
    public ExtraGraphicalMeasurePreviousMeasure: GraphicalMeasure;
    public ShowTimeSignature: boolean = true;
    public ShowKeySignature: boolean = true;
    public isTabMeasure: boolean = false;

    public get ParentStaff(): Staff {
        return this.parentStaff;
    }

    public get ParentMusicSystem(): MusicSystem {
        return this.parentMusicSystem;
    }

    public set ParentMusicSystem(value: MusicSystem) {
        this.parentMusicSystem = value;
    }

    public get MeasureNumber(): number {
        return this.measureNumber;
    }

    public get FirstInstructionStaffEntry(): GraphicalStaffEntry {
        return this.firstInstructionStaffEntry;
    }

    public set FirstInstructionStaffEntry(value: GraphicalStaffEntry) {
        this.firstInstructionStaffEntry = value;
    }

    public get LastInstructionStaffEntry(): GraphicalStaffEntry {
        return this.lastInstructionStaffEntry;
    }

    public set LastInstructionStaffEntry(value: GraphicalStaffEntry) {
        this.lastInstructionStaffEntry = value;
    }

    public get ParentStaffLine(): StaffLine {
        return this.parentStaffLine;
    }

    public set ParentStaffLine(value: StaffLine) {
        this.parentStaffLine = value;
        if (this.parentStaffLine) {
            this.PositionAndShape.Parent = this.parentStaffLine.PositionAndShape;
        }
    }

    /**
     * Reset all the geometric values and parameters of this measure and put it in an initialized state.
     * This is needed to evaluate a measure a second time by system builder.
     */
    public resetLayout(): void {
        throw new Error("not implemented");
    }

    /**
     * Return the x-width of a given measure line.
     * @param line
     */
    public getLineWidth(line: SystemLinesEnum): number {
        throw new Error("not implemented");
    }

    /**
     * Add the given clef to the begin of the measure.
     * This has to update/increase BeginInstructionsWidth.
     * @param clef
     */
    public addClefAtBegin(clef: ClefInstruction): void {
        throw new Error("not implemented");
    }

    /**
     * Add the given key to the begin of the measure.
     * This has to update/increase BeginInstructionsWidth.
     * @param currentKey - The new valid key.
     * @param previousKey - The old cancelled key. Needed to show which accidentals are not valid any more.
     * @param currentClef - The valid clef. Needed to put the accidentals on the right y-positions.
     */
    public addKeyAtBegin(currentKey: KeyInstruction, previousKey: KeyInstruction, currentClef: ClefInstruction): void {
        throw new Error("not implemented");
    }

    /**
     * Add the given rhythm to the begin of the measure.
     * This has to update/increase BeginInstructionsWidth.
     * @param rhythm
     */
    public addRhythmAtBegin(rhythm: RhythmInstruction): void {
        throw new Error("not implemented");
    }

    /**
     * Add the given clef to the end of the measure.
     * This has to update/increase EndInstructionsWidth.
     * @param clef
     */
    public addClefAtEnd(clef: ClefInstruction, visible: boolean = true): void {
        throw new Error("not implemented");
    }

    /**
     * Set the x-position relative to the staffline (y-Position is always 0 relative to the staffline).
     * @param xPos
     */
    public setPositionInStaffline(xPos: number): void {
        this.PositionAndShape.RelativePosition = new PointF2D(xPos, 0);
    }

    /**
     * Set the overall x-width of the measure.
     * @param width
     */
    public setWidth(width: number): void {
        this.PositionAndShape.BorderRight = width;
    }

    /**
     * This method is called after the StaffEntriesScaleFactor has been set.
     * Here the final x-positions of the staff entries have to be set.
     * (multiply the minimal positions with the scaling factor, considering the BeginInstructionsWidth).
     */
    public layoutSymbols(): void {
        throw new Error("not implemented");
    }

    public findGraphicalStaffEntryFromTimestamp(relativeTimestamp: Fraction): GraphicalStaffEntry {
        for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
            const graphicalStaffEntry: GraphicalStaffEntry = this.staffEntries[idx];
            if (graphicalStaffEntry.relInMeasureTimestamp?.Equals(relativeTimestamp)) {
                return graphicalStaffEntry;
            }
        }
        return undefined;
    }

    /**
     * Iterate from start to end and find the [[GraphicalStaffEntry]] with the same absolute timestamp.
     * @param absoluteTimestamp
     * @returns {any}
     */
    public findGraphicalStaffEntryFromVerticalContainerTimestamp(absoluteTimestamp: Fraction): GraphicalStaffEntry {
        for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
            const graphicalStaffEntry: GraphicalStaffEntry = this.staffEntries[idx];
            if (graphicalStaffEntry.sourceStaffEntry.VerticalContainerParent.getAbsoluteTimestamp().Equals(absoluteTimestamp)) {
                return graphicalStaffEntry;
            }
        }
        return undefined;
    }

    /**
     * Check if the all the [[GraphicalMeasure]]'s [[StaffEntry]]s (their minimum Length) have the same duration with the [[SourceMeasure]].
     * @returns {boolean}
     */
    public hasSameDurationWithSourceMeasureParent(): boolean {
        const duration: Fraction = new Fraction(0, 1);
        for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
            const graphicalStaffEntry: GraphicalStaffEntry = this.staffEntries[idx];
            duration.Add(graphicalStaffEntry.findStaffEntryMinNoteLength());
        }
        return duration.Equals(this.parentSourceMeasure.Duration);
    }

    /**
     * Check a whole [[Measure]] for the presence of multiple Voices (used for Stem direction).
     * @returns {boolean}
     */
    public hasMultipleVoices(): boolean {
        if (this.staffEntries.length === 0) {
            return false;
        }
        const voices: Voice[] = [];
        for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
            const staffEntry: GraphicalStaffEntry = this.staffEntries[idx];
            for (let idx2: number = 0, len2: number = staffEntry.sourceStaffEntry.VoiceEntries.length; idx2 < len2; ++idx2) {
                const voiceEntry: VoiceEntry = staffEntry.sourceStaffEntry.VoiceEntries[idx2];
                if (voices.indexOf(voiceEntry.ParentVoice) < 0) {
                    voices.push(voiceEntry.ParentVoice);
                }
            }
        }
        if (voices.length > 1) {
            return true;
        }
        return false;
    }

    public isVisible(): boolean {
        return this.ParentStaff.ParentInstrument.Visible;
    }

    public getGraphicalMeasureDurationFromStaffEntries(): Fraction {
        let duration: Fraction = new Fraction(0, 1);
        const voices: Voice[] = [];
        for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
            const graphicalStaffEntry: GraphicalStaffEntry = this.staffEntries[idx];
            for (let idx2: number = 0, len2: number = graphicalStaffEntry.sourceStaffEntry.VoiceEntries.length; idx2 < len2; ++idx2) {
                const voiceEntry: VoiceEntry = graphicalStaffEntry.sourceStaffEntry.VoiceEntries[idx2];
                if (voices.indexOf(voiceEntry.ParentVoice) < 0) {
                    voices.push(voiceEntry.ParentVoice);
                }
            }
        }
        for (let idx: number = 0, len: number = voices.length; idx < len; ++idx) {
            const voice: Voice = voices[idx];
            const voiceDuration: Fraction = new Fraction(0, 1);
            for (const graphicalStaffEntry of this.staffEntries) {
                for (const gve of graphicalStaffEntry.graphicalVoiceEntries) {
                    if (gve.parentVoiceEntry.ParentVoice === voice && gve.notes.length > 0) {
                        voiceDuration.Add(gve.notes[0].graphicalNoteLength);
                    }
                }
            }
            if (duration.lt(voiceDuration)) {
                duration = Fraction.createFromFraction(voiceDuration);
            }
        }
        return duration;
    }

    public addGraphicalStaffEntry(graphicalStaffEntry: GraphicalStaffEntry): void {
        this.staffEntries.push(graphicalStaffEntry);
    }

    /**
     * Add a [[StaffEntry]] (along with its [[BoundingBox]]) to the current Measure.
     * @param staffEntry
     */
    public addGraphicalStaffEntryAtTimestamp(staffEntry: GraphicalStaffEntry): void {
        if (staffEntry) {
            if (this.staffEntries.length === 0 || this.staffEntries[this.staffEntries.length - 1].relInMeasureTimestamp.lt(staffEntry.relInMeasureTimestamp)) {
                this.staffEntries.push(staffEntry);
            } else {
                for (let i: number = this.staffEntries.length - 1; i >= 0; i--) {
                    if (this.staffEntries[i].relInMeasureTimestamp.lt(staffEntry.relInMeasureTimestamp)) {
                        this.staffEntries.splice(i + 1, 0, staffEntry);
                        break;
                    }
                    if (i === 0) {
                        this.staffEntries.splice(i, 0, staffEntry);
                    }
                }
            }
        }
    }

    public isPianoRightHand(): boolean {
        return this.isUpperStaffOfInstrument();
    }

    public isPianoLeftHand(): boolean {
        return this.isLowerStaffOfInstrument();
    }

    public isUpperStaffOfInstrument(): boolean {
        if (this.parentStaff.ParentInstrument.Staves.length === 1) {
            return true;
        }
        return this.ParentStaff === this.parentStaff.ParentInstrument.Staves[0];
    }

    public isLowerStaffOfInstrument(): boolean {
        if (this.parentStaff.ParentInstrument.Staves.length === 1) {
            return false; // technically this could be true as well, but we want this to be treated as upper and not return the same value.
            // e.g. for a violin, fingerings should go above.
        }
        return this.ParentStaff === this.ParentStaff.ParentInstrument.Staves.last();
    }

    public beginsWithLineRepetition(): boolean {
        const sourceMeasure: SourceMeasure = this.parentSourceMeasure;
        if (!sourceMeasure) {
            return false;
        }
        return sourceMeasure.beginsWithLineRepetition();
    }

    /**
     * Check if this Measure is a Repetition Ending.
     * @returns {boolean}
     */
    public endsWithLineRepetition(): boolean {
        const sourceMeasure: SourceMeasure = this.parentSourceMeasure;
        if (!sourceMeasure) {
            return false;
        }
        return sourceMeasure.endsWithLineRepetition();
    }

    /**
     * Check if a Repetition starts at the next Measure.
     * @returns {boolean}
     */
    public beginsWithWordRepetition(): boolean {
        const sourceMeasure: SourceMeasure = this.parentSourceMeasure;
        if (!sourceMeasure) {
            return false;
        }
        return sourceMeasure.beginsWithWordRepetition();
    }

    /**
     * Check if this Measure is a Repetition Ending.
     */
    public endsWithWordRepetition(): boolean {
        const sourceMeasure: SourceMeasure = this.parentSourceMeasure;
        if (!sourceMeasure) {
            return false;
        }
        return sourceMeasure.endsWithWordRepetition();
    }

    public getTransposedHalftones(): number {
        const transposeHalftones: number = this.parentStaff.ParentInstrument.GetMusicSheet.Transpose +
            this.parentStaff.ParentInstrument.Transpose;
        // if (!transposeHalftones) {
        //     transposeHalftones = this.parentStaff.ParentInstrument.Transpose;
        // }
        return transposeHalftones;
    }
}

