import {VoiceEntry, StemDirectionType} from "./VoiceEntry";
import {SourceStaffEntry} from "./SourceStaffEntry";
import {Fraction} from "../../Common/DataObjects/Fraction";
import {NoteEnum, Pitch} from "../../Common/DataObjects/Pitch";
import {Beam} from "./Beam";
import {Tuplet} from "./Tuplet";
import {Tie} from "./Tie";
import {Staff} from "./Staff";
import {Slur} from "./Expressions/ContinuousExpressions/Slur";
import {NoteState} from "../Graphical/DrawingEnums";
import {Notehead} from "./Notehead";
import {Arpeggio} from "./Arpeggio";
import {NoteType} from "./NoteType";
import { SourceMeasure } from "./SourceMeasure";
import { TechnicalInstruction } from "./Instructions";
import { Glissando } from "../../MusicalScore/VoiceData/Glissando";

import { PlaybackNote } from "../Playback/PlaybackNote";

/**
 * Represents a single pitch with a duration (length)
 */
export class Note {

    constructor(voiceEntry: VoiceEntry, parentStaffEntry: SourceStaffEntry, length: Fraction, pitch: Pitch, sourceMeasure: SourceMeasure, isRest?: boolean) {
        this.voiceEntry = voiceEntry;
        this.parentStaffEntry = parentStaffEntry;
        this.length = length;
        this.pitch = pitch;
        this.sourceMeasure = sourceMeasure;
        this.isRestFlag = isRest ?? false;
        if (pitch) {
            this.halfTone = pitch.getHalfTone();
        } else {
          this.halfTone = 0;
        }
    }

    /**
     * The transposed (!!!) HalfTone of this note.
     */
    public halfTone: number;
    public state: NoteState;
    private voiceEntry: VoiceEntry;
    private parentStaffEntry: SourceStaffEntry;
    private length: Fraction;
    private playbackNote: PlaybackNote;
    private sourceMeasure: SourceMeasure;
    /** The length/duration given in the <type> tag. different from length for tuplets/tremolos. */
    private typeLength: Fraction;
    /** The NoteType given in the XML, e.g. quarter, which can be a normal quarter or tuplet quarter -> can have different length/fraction */
    private noteTypeXml: NoteType;
    public DotsXml: number;
    /** The amount of notes the tuplet of this note (if there is one) replaces. */
    private normalNotes: number;
    private isRestFlag: boolean;
    public IsWholeMeasureRest: boolean;
    /**
     * The untransposed (!!!) source data.
     */
    private pitch: Pitch;
    /** The transposed pitch, if the score is transposed, otherwise undefined. */
    public TransposedPitch: Pitch;
    public displayStepUnpitched: NoteEnum;
    public displayOctaveUnpitched: number;
    public get NoteAsString(): string {
        return this.pitch.toString();
    }
    private beam: Beam;
    private tuplet: Tuplet;
    private tie: Tie;
    private glissando: Glissando;
    private slurs: Slur[] = [];
    private playbackInstrumentId: string = undefined;
    private notehead: Notehead = undefined;
    /** States whether the note should be displayed. False if xmlNode.attribute("print-object").value = "no". */
    private printObject: boolean = true;
    /** The Arpeggio this note is part of. */
    private arpeggio: Arpeggio;
    /** States whether this is a cue note (Stichnote) (smaller size). */
    private isCueNote: boolean;
    public IsGraceNote: boolean;
    /** The stem direction asked for in XML. Not necessarily final or wanted stem direction. */
    private stemDirectionXml: StemDirectionType;
    /** The number of tremolo strokes this note has (16th tremolo = 2 strokes).
     * Could be a Tremolo object in future when there is more data like tremolo between two notes.
     */
    public TremoloInfo: TremoloInfo;
    /** Color of the stem given in the XML Stem tag. RGB Hexadecimal, like #00FF00.
     * This is not used for rendering, which takes VoiceEntry.StemColor.
     * It is merely given in the note's stem element in XML and stored here for reference.
     * So, to read or change the stem color of a note, modify note.ParentVoiceEntry.StemColor.
     */
    private stemColorXml: string;
    /** Color of the notehead given in the XML Notehead tag. RGB Hexadecimal, like #00FF00.
     * This should not be changed, instead noteheadColor is used and modifiable for Rendering.
     * Needs to be stored here and not in Note.Notehead,
     * because Note.Notehead is undefined for normal Noteheads to save space and time.
     */
    private noteheadColorXml: string;
    /** Color of the notehead currently set/desired for next render. RGB Hexadecimal, like #00FF00.
     * Needs to be stored here and not in Note.Notehead,
     * because Note.Notehead is undefined for normal Noteheads to save space and time.
     */
    private noteheadColor: string;
    private noteheadColorCurrentlyRendered: string;
    public Fingering: TechnicalInstruction; // this is also stored in VoiceEntry.TechnicalInstructions
    public StringInstruction: TechnicalInstruction; // this is also stored in VoiceEntry.TechnicalInstructions
    // note that there is also TabNote.StringNumber, so we can't use that identifier here
    /** Used by GraphicalNote.FromNote(note) and osmd.rules.GNote(note) to get a GraphicalNote from a Note.
     *  Note that we don't want the data model (Note) to be dependent on the graphical implementation (GraphicalNote),
     *    and have (potentially circular) import dependencies of graphical parts, which also applies to other non-graphical classes.
     *    That's why we don't save a GraphicalNote reference directly in Note.
     */
    public NoteToGraphicalNoteObjectId: number; // used with EngravingRules.NoteToGraphicalNoteMap

    public ToStringShort(octaveOffset: number = 0): string {
        if (!this.Pitch || this.isRest()) {
            return "rest"; // Pitch is undefined for rest notes
        }
        return this.Pitch?.ToStringShort(octaveOffset);
    }
    public get ToStringShortGet(): string {
        return this.ToStringShort(0);
    }
    public get ParentVoiceEntry(): VoiceEntry {
        return this.voiceEntry;
    }
    public set ParentVoiceEntry(value: VoiceEntry) {
        this.voiceEntry = value;
    }
    public get ParentStaffEntry(): SourceStaffEntry {
        return this.parentStaffEntry;
    }
    public get ParentStaff(): Staff {
        return this.parentStaffEntry.ParentStaff;
    }
    public get Length(): Fraction {
        return this.length;
    }
    public set PlaybackNote(value: PlaybackNote) {
        this.playbackNote = value;
    }
    public get PlaybackNote(): PlaybackNote {
        return this.playbackNote;
    }
    public set Length(value: Fraction) {
        this.length = value;
    }
    public get SourceMeasure(): SourceMeasure {
        return this.sourceMeasure;
    }
    public get TypeLength(): Fraction {
        return this.typeLength;
    }
    public set TypeLength(value: Fraction) {
        this.typeLength = value;
    }
    public get NoteTypeXml(): NoteType {
        return this.noteTypeXml;
    }
    public set NoteTypeXml(value: NoteType) {
        this.noteTypeXml = value;
    }
    public get NormalNotes(): number {
        return this.normalNotes;
    }
    public set NormalNotes(value: number) {
        this.normalNotes = value;
    }
    public get Pitch(): Pitch {
        return this.pitch;
    }
    public get NoteBeam(): Beam {
        return this.beam;
    }
    public set NoteBeam(value: Beam) {
        this.beam = value;
    }
    public set Notehead(value: Notehead) {
        this.notehead = value;
    }
    public get Notehead(): Notehead {
        return this.notehead;
    }
    public get NoteTuplet(): Tuplet {
        return this.tuplet;
    }
    public set NoteTuplet(value: Tuplet) {
        this.tuplet = value;
    }
    public get NoteGlissando(): Glissando {
        return this.glissando;
    }
    public set NoteGlissando(value: Glissando) {
        this.glissando = value;
    }
    public get NoteTie(): Tie {
        return this.tie;
    }
    public set NoteTie(value: Tie) {
        this.tie = value;
    }
    public get NoteSlurs(): Slur[] {
        return this.slurs;
    }
    public set NoteSlurs(value: Slur[]) {
        this.slurs = value;
    }
    public get PlaybackInstrumentId(): string {
        return this.playbackInstrumentId;
    }
    public set PlaybackInstrumentId(value: string) {
        this.playbackInstrumentId = value;
    }
    public get PrintObject(): boolean {
        return this.printObject;
    }
    public set PrintObject(value: boolean) {
        this.printObject = value;
    }
    public get Arpeggio(): Arpeggio {
        return this.arpeggio;
    }
    public set Arpeggio(value: Arpeggio) {
        this.arpeggio = value;
    }
    public get IsCueNote(): boolean {
        return this.isCueNote;
    }
    public set IsCueNote(value: boolean) {
        this.isCueNote = value;
    }
    public get StemDirectionXml(): StemDirectionType {
        return this.stemDirectionXml;
    }
    public set StemDirectionXml(value: StemDirectionType) {
        this.stemDirectionXml = value;
    }
    public get TremoloStrokes(): number {
        return this.TremoloInfo?.tremoloStrokes;
    }
    public get StemColorXml(): string {
        return this.stemColorXml;
    }
    public set StemColorXml(value: string) {
        this.stemColorXml = value;
    }
    public get NoteheadColorXml(): string {
        return this.noteheadColorXml;
    }
    public set NoteheadColorXml(value: string) {
        this.noteheadColorXml = value;
    }
    /** The desired notehead color for the next render. */
    public get NoteheadColor(): string {
        return this.noteheadColor;
    }
    public set NoteheadColor(value: string) {
        this.noteheadColor = value;
    }
    public get NoteheadColorCurrentlyRendered(): string {
        return this.noteheadColorCurrentlyRendered;
    }
    public set NoteheadColorCurrentlyRendered(value: string) {
        this.noteheadColorCurrentlyRendered = value;
    }

    public isRest(): boolean {
        return this.isRestFlag;
    }

    /** Note: May be dangerous to use if ParentStaffEntry.VerticalContainerParent etc is not set.
     * better calculate this directly when you have access to the note's measure.
     * whole rest: length = measure length. (4/4 in a 4/4 time signature, 3/4 in a 3/4 time signature, 1/4 in a 1/4 time signature, etc.)
     */
    public isWholeRest(): boolean {
        return this.isRest() && this.Length.RealValue === this.sourceMeasure.ActiveTimeSignature.RealValue;
    }

    /** Whether the note fills the whole measure. */
    public isWholeMeasureNote(): boolean {
        return this.Length.RealValue === this.sourceMeasure.ActiveTimeSignature.RealValue;
    }

    public ToString(): string {
        if (this.pitch) {
            return this.Pitch.ToString() + ", length: " + this.length.toString();
        } else {
          return "rest note, length: " + this.length.toString();
        }
    }
    public getAbsoluteTimestamp(): Fraction {
        return Fraction.plus(
            this.voiceEntry.Timestamp,
            this.sourceMeasure.AbsoluteTimestamp
        );
    }
    public isDuplicateSlur(slur: Slur): boolean {
        for (let idx: number = 0, len: number = this.slurs.length; idx < len; ++idx) {
            const noteSlur: Slur = this.slurs[idx];
            if (
              noteSlur.StartNote !== undefined &&
              noteSlur.EndNote !== undefined &&
              slur.StartNote !== undefined &&
              slur.StartNote === noteSlur.StartNote &&
              noteSlur.EndNote === this &&
              slur.PlacementXml === noteSlur.PlacementXml
            ) { return true; }
        }
        return false;
    }
    public hasTabEffects(): boolean {
        return false; // override in TabNote
    }
}

export enum Appearance {
    Normal,
    Grace,
    Cue
}

export interface TremoloInfo {
    tremoloStrokes: number;
    /** Buzz roll (type="unmeasured" in XML) */
    tremoloUnmeasured: boolean;
    // could in future be extended e.g. for tremolo between notes
}
