import Vex from "vexflow";
import { LabelRenderSpecs, MusicSheetDrawer } from "../MusicSheetDrawer";
import VF = Vex.Flow;
import { RectangleF2D } from "../../../Common/DataObjects/RectangleF2D";
import { VexFlowMeasure } from "./VexFlowMeasure";
import { PointF2D } from "../../../Common/DataObjects/PointF2D";
import { GraphicalLabel } from "../GraphicalLabel";
import { VexFlowTextMeasurer } from "./VexFlowTextMeasurer";
import { MusicSystem } from "../MusicSystem";
import { GraphicalObject } from "../GraphicalObject";
import { GraphicalLayers } from "../DrawingEnums";
import { GraphicalStaffEntry } from "../GraphicalStaffEntry";
import { VexFlowBackend } from "./VexFlowBackend";
import { VexFlowOctaveShift } from "./VexFlowOctaveShift";
import { VexFlowInstantaneousDynamicExpression } from "./VexFlowInstantaneousDynamicExpression";
import { VexFlowInstrumentBracket } from "./VexFlowInstrumentBracket";
import { VexFlowInstrumentBrace } from "./VexFlowInstrumentBrace";
import { GraphicalLyricEntry } from "../GraphicalLyricEntry";
import { VexFlowStaffLine } from "./VexFlowStaffLine";
import { StaffLine } from "../StaffLine";
import { GraphicalSlur } from "../GraphicalSlur";
import { PlacementEnum } from "../../VoiceData/Expressions/AbstractExpression";
import { GraphicalInstantaneousTempoExpression } from "../GraphicalInstantaneousTempoExpression";
import { GraphicalInstantaneousDynamicExpression } from "../GraphicalInstantaneousDynamicExpression";
import log from "loglevel";
import { GraphicalContinuousDynamicExpression } from "../GraphicalContinuousDynamicExpression";
import { VexFlowContinuousDynamicExpression } from "./VexFlowContinuousDynamicExpression";
import { DrawingParameters } from "../DrawingParameters";
import { GraphicalMusicPage } from "../GraphicalMusicPage";
import { GraphicalMusicSheet } from "../GraphicalMusicSheet";
import { GraphicalUnknownExpression } from "../GraphicalUnknownExpression";
import { VexFlowPedal } from "./VexFlowPedal";
import { VexflowVibratoBracket } from "./VexflowVibratoBracket";
// import { Fraction } from "../../../Common";

/**
 * This is a global constant which denotes the height in pixels of the space between two lines of the stave
 * (when zoom = 1.0)
 * @type number
 */
export const unitInPixels: number = 10;

export class VexFlowMusicSheetDrawer extends MusicSheetDrawer {
    private backend: VexFlowBackend;
    private backends: VexFlowBackend[] = [];
    private zoom: number = 1.0;
    public get Zoom(): number {
        return this.zoom;
    }
    private pageIdx: number = 0; // this is a bad solution, should use MusicPage.PageNumber instead.

    constructor(drawingParameters: DrawingParameters = new DrawingParameters()) {
        super(new VexFlowTextMeasurer(drawingParameters.Rules), drawingParameters);
    }

    public get Backends(): VexFlowBackend[] {
        return this.backends;
    }

    protected initializeBackendForPage(page: GraphicalMusicPage): void {
        this.backend = this.backends[page.PageNumber - 1];
    }

    public drawSheet(graphicalMusicSheet: GraphicalMusicSheet): void {
        // vexflow 3.x: change default font
        if (this.rules.DefaultVexFlowNoteFont === "gonville") {
            (Vex.Flow as any).DEFAULT_FONT_STACK = [(Vex.Flow as any).Fonts?.Gonville, (Vex.Flow as any).Fonts?.Bravura, (Vex.Flow as any).Fonts?.Custom];
        } // else keep new vexflow default Bravura (more cursive, bold).

        // sizing defaults in Vexflow
        (Vex.Flow as any).STAVE_LINE_THICKNESS = this.rules.StaffLineWidth * unitInPixels;
        (Vex.Flow as any).STEM_WIDTH = this.rules.StemWidth * unitInPixels;
        // sets scale/size of notes/rest notes:
        (Vex.Flow as any).DEFAULT_NOTATION_FONT_SCALE = this.rules.VexFlowDefaultNotationFontScale; // default 39
        (Vex.Flow as any).DEFAULT_TAB_FONT_SCALE = this.rules.VexFlowDefaultTabFontScale; // default 39 // TODO doesn't seem to do anything

        this.pageIdx = 0;
        for (const graphicalMusicPage of graphicalMusicSheet.MusicPages) {
            if (graphicalMusicPage.PageNumber > this.rules.MaxPageToDrawNumber) {
                break;
            }
            const backend: VexFlowBackend = this.backends[this.pageIdx];
            backend.graphicalMusicPage = graphicalMusicPage;
            backend.scale(this.zoom);
            //backend.resize(graphicalMusicSheet.ParentMusicSheet.pageWidth * unitInPixels * this.zoom,
            //               EngravingRules.Rules.PageHeight * unitInPixels * this.zoom);
            this.pageIdx += 1;
        }

        this.pageIdx = 0;
        this.backend = this.backends[0];
        super.drawSheet(graphicalMusicSheet);
    }

    protected drawPage(page: GraphicalMusicPage): void {
        if (!page) {
            return;
        }
        this.backend = this.backends[page.PageNumber - 1]; // TODO we may need to set this in a couple of other places. this.pageIdx is a bad solution
        super.drawPage(page);
        this.pageIdx += 1;
    }

    public clear(): void {
        for (const backend of this.backends) {
            backend.clear();
        }
    }

    public setZoom(zoom: number): void {
        this.zoom = zoom;
    }

    /**
     * Converts a distance from unit to pixel space.
     * @param unitDistance the distance in units
     * @returns {number} the distance in pixels
     */
    public calculatePixelDistance(unitDistance: number): number {
        return unitDistance * unitInPixels;
    }

    protected drawStaffLine(staffLine: StaffLine): void {
        const stafflineNode: Node = this.backend.getContext().openGroup();
        if (stafflineNode) {
            (stafflineNode as SVGGElement).classList.add("staffline");
        }
        super.drawStaffLine(staffLine);
        const absolutePos: PointF2D = staffLine.PositionAndShape.AbsolutePosition;
        if (this.rules.RenderSlurs) {
            this.drawSlurs(staffLine as VexFlowStaffLine, absolutePos);
        }
        this.backend.getContext().closeGroup();
    }

    private drawSlurs(vfstaffLine: VexFlowStaffLine, absolutePos: PointF2D): void {
        const vfcurveItem: any[] = (window as any).GYM?.vfcurveItem
        for(let i = 0; i < vfstaffLine.GraphicalSlurs.length; i++){
            const graphicalSlur = vfstaffLine.GraphicalSlurs[i]
            if (graphicalSlur.slur.isCrossed()) {
                continue;
            }
            if (vfcurveItem && vfcurveItem.length) {
                try {
                    const measure = vfcurveItem.find((n: any, _i: number) => (n.index ? n.index === i : true) && n.MeasureNumberXML == graphicalSlur.staffEntries[graphicalSlur.staffEntries.length - 1].parentMeasure.MeasureNumber)
                    if (measure){
                        graphicalSlur.bezierEndControlPt.y += measure?.bezierEndControlPt?.y ? measure.bezierEndControlPt.y : 0
                        graphicalSlur.bezierEndPt.y += measure?.bezierEndPt?.y ? measure.bezierEndPt.y : 0
                        graphicalSlur.bezierStartControlPt.y += measure?.bezierStartControlPt?.y ? measure.bezierStartControlPt.y : 0
                    }
                } catch (error) {
                    console.log(error)
                }
            }
            this.drawSlur(graphicalSlur, absolutePos);
        }
        // for (const graphicalSlur of vfstaffLine.GraphicalSlurs) {
        //     // don't draw crossed slurs, as their curve calculation is not implemented yet:
        //     if (graphicalSlur.slur.isCrossed()) {
        //         continue;
        //     }
        //     this.drawSlur(graphicalSlur, absolutePos);
        // }
    }

    private drawSlur(graphicalSlur: GraphicalSlur, abs: PointF2D): void {
        
        const curvePointsInPixels: PointF2D[] = [];
        let p1: PointF2D;
        let p2: PointF2D;
        let p3: PointF2D;
        let p4: PointF2D;

        // 1) create inner or original curve:
        // 简谱 模式,将三次贝塞尔曲线放在谱子上方
        if (this.rules.DYMusicScoreType === "jianpu") {
            if (graphicalSlur.placement > 0 || graphicalSlur.bezierStartPt.y > 0){
                graphicalSlur.bezierStartPt.y = -0.8;
                graphicalSlur.bezierStartControlPt.y = -2.5;
            }

            p1 = new PointF2D(graphicalSlur.bezierStartPt.x + abs.x, graphicalSlur.bezierStartPt.y + abs.y);
            p2 = new PointF2D(graphicalSlur.bezierStartControlPt.x + abs.x, graphicalSlur.bezierStartControlPt.y + abs.y);
            p3 = new PointF2D(graphicalSlur.bezierEndControlPt.x + abs.x, graphicalSlur.bezierStartControlPt.y + abs.y);
            p4 = new PointF2D(graphicalSlur.bezierEndPt.x + abs.x, graphicalSlur.bezierStartPt.y + abs.y);
        } else {
            p1 = new PointF2D(graphicalSlur.bezierStartPt.x + abs.x, graphicalSlur.bezierStartPt.y + abs.y);
            p2 = new PointF2D(graphicalSlur.bezierStartControlPt.x + abs.x, graphicalSlur.bezierStartControlPt.y + abs.y);
            p3 = new PointF2D(graphicalSlur.bezierEndControlPt.x + abs.x, graphicalSlur.bezierEndControlPt.y + abs.y);
            p4 = new PointF2D(graphicalSlur.bezierEndPt.x + abs.x, graphicalSlur.bezierEndPt.y + abs.y);
        }

        // put screen transformed points into array
        curvePointsInPixels.push(this.applyScreenTransformation(p1));
        curvePointsInPixels.push(this.applyScreenTransformation(p2));
        curvePointsInPixels.push(this.applyScreenTransformation(p3));
        curvePointsInPixels.push(this.applyScreenTransformation(p4));
        //DEBUG: Render control points
        // for (const point of curvePointsInPixels) {
        //     const pointRect: RectangleF2D = new RectangleF2D(point.x - 2, point.y - 2, 4, 4);
        //     this.backend.renderRectangle(pointRect, 3, "red", 1);
        // }

        // 2) create second outer curve to create a thickness for the curve:
        if (graphicalSlur.placement === PlacementEnum.Above) {
            p1.y -= 0.05;
            p2.y -= 0.3;
            p3.y -= 0.3;
            p4.y -= 0.05;
        } else {
            p1.y += 0.05;
            p2.y += 0.3;
            p3.y += 0.3;
            p4.y += 0.05;
        }

        // put screen transformed points into array
        curvePointsInPixels.push(this.applyScreenTransformation(p1));
        curvePointsInPixels.push(this.applyScreenTransformation(p2));
        curvePointsInPixels.push(this.applyScreenTransformation(p3));
        curvePointsInPixels.push(this.applyScreenTransformation(p4));
        graphicalSlur.SVGElement = this.backend.renderCurve(curvePointsInPixels);
    }

    protected drawMeasure(measure: VexFlowMeasure): void {
        measure.setAbsoluteCoordinates(
            measure.PositionAndShape.AbsolutePosition.x * unitInPixels,
            measure.PositionAndShape.AbsolutePosition.y * unitInPixels
        );
        const context: Vex.IRenderContext = this.backend.getContext();
        try {
            measure.draw(context);
            // Vexflow errors can happen here. If we don't catch errors, rendering will stop after this measure.
        } catch (ex) {
            log.warn("VexFlowMusicSheetDrawer.drawMeasure", ex);
        }
        // Draw the StaffEntries
        for (const staffEntry of measure.staffEntries) {
            this.drawStaffEntry(staffEntry);
        }
    }

    // private drawPixel(coord: PointF2D): void {
    //     coord = this.applyScreenTransformation(coord);
    //     const ctx: any = this.backend.getContext();
    //     const oldStyle: string = ctx.fillStyle;
    //     ctx.fillStyle = "#00FF00FF";
    //     ctx.fillRect( coord.x, coord.y, 2, 2 );
    //     ctx.fillStyle = oldStyle;
    // }

    /** Draws a line in the current backend. Only usable while pages are drawn sequentially, because backend reference is updated in that process.
     *  To add your own lines after rendering, use DrawOverlayLine.
     */
    protected drawLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number = 0.2): Node {
        // TODO maybe the backend should be given as an argument here as well, otherwise this can't be used after rendering of multiple pages is done.
        start = this.applyScreenTransformation(start);
        stop = this.applyScreenTransformation(stop);
        /*if (!this.backend) {
            this.backend = this.backends[0];
        }*/
        return this.backend.renderLine(start, stop, color, lineWidth * unitInPixels);
    }

    /** Lets a user/developer draw an overlay line on the score. Use this instead of drawLine, which is for OSMD internally only.
     *  The MusicPage has to be specified, because each page and Vexflow backend has its own relative coordinates.
     *  (the AbsolutePosition of a GraphicalNote is relative to its backend)
     *  To get a MusicPage, use GraphicalNote.ParentMusicPage.
     */
    public DrawOverlayLine(start: PointF2D, stop: PointF2D, musicPage: GraphicalMusicPage,
                           color: string = "#FF0000FF", lineWidth: number = 0.2): Node {
        if (!musicPage.PageNumber || musicPage.PageNumber > this.backends.length || musicPage.PageNumber < 1) {
            console.log("VexFlowMusicSheetDrawer.drawOverlayLine: invalid page number / music page number doesn't correspond to an existing backend.");
            return;
        }
        const musicPageIndex: number = musicPage.PageNumber - 1;
        const backendToUse: VexFlowBackend = this.backends[musicPageIndex];

        start = this.applyScreenTransformation(start);
        stop = this.applyScreenTransformation(stop);
        return backendToUse.renderLine(start, stop, color, lineWidth * unitInPixels);
    }

    protected drawSkyLine(staffline: StaffLine): void {
        const startPosition: PointF2D = staffline.PositionAndShape.AbsolutePosition;
        const width: number = staffline.PositionAndShape.Size.width;
        this.drawSampledLine(staffline.SkyLine, startPosition, width);
    }

    protected drawBottomLine(staffline: StaffLine): void {
        const startPosition: PointF2D = new PointF2D(staffline.PositionAndShape.AbsolutePosition.x,
                                                     staffline.PositionAndShape.AbsolutePosition.y);
        const width: number = staffline.PositionAndShape.Size.width;
        this.drawSampledLine(staffline.BottomLine, startPosition, width, "#0000FFFF");
    }

    /**
     * Draw a line with a width and start point in a chosen color (used for skyline/bottom line debugging) from
     * a simple array
     * @param line numeric array. 0 marks the base line. Direction given by sign. Dimensions in units
     * @param startPosition Start position in units
     * @param width Max line width in units
     * @param color Color to paint in. Default is red
     */
    private drawSampledLine(line: number[], startPosition: PointF2D, width: number, color: string = "#FF0000FF"): void {
        const indices: number[] = [];
        let currentValue: number = 0;
        //Loops through bottom line, grabs all indices that don't equal the previously grabbed index
        //Starting with 0 (gets index of all line changes)
        for (let i: number = 0; i < line.length; i++) {
            if (line[i] !== currentValue) {
                indices.push(i);
                currentValue = line[i];
            }
        }

        const absolute: PointF2D = startPosition;
        if (indices.length > 0) {
            const samplingUnit: number = this.rules.SamplingUnit;

            let horizontalStart: PointF2D = new PointF2D(absolute.x, absolute.y);
            let horizontalEnd: PointF2D = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y);
            this.drawLine(horizontalStart, horizontalEnd, color);

            let verticalStart: PointF2D;
            let verticalEnd: PointF2D;

            if (line[0] >= 0) {
                verticalStart = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y);
                verticalEnd = new PointF2D(indices[0] / samplingUnit + absolute.x, absolute.y + line[indices[0]]);
                this.drawLine(verticalStart, verticalEnd, color);
            }

            for (let i: number = 1; i < indices.length; i++) {
                horizontalStart = new PointF2D(indices[i - 1] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
                horizontalEnd = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
                this.drawLine(horizontalStart, horizontalEnd, color);

                verticalStart = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i - 1]]);
                verticalEnd = new PointF2D(indices[i] / samplingUnit + absolute.x, absolute.y + line[indices[i]]);
                this.drawLine(verticalStart, verticalEnd, color);
            }

            if (indices[indices.length - 1] < line.length) {
                horizontalStart = new PointF2D(indices[indices.length - 1] / samplingUnit + absolute.x, absolute.y + line[indices[indices.length - 1]]);
                horizontalEnd = new PointF2D(absolute.x + width, absolute.y + line[indices[indices.length - 1]]);
                this.drawLine(horizontalStart, horizontalEnd, color);
            } else {
                horizontalStart = new PointF2D(indices[indices.length - 1] / samplingUnit + absolute.x, absolute.y);
                horizontalEnd = new PointF2D(absolute.x + width, absolute.y);
                this.drawLine(horizontalStart, horizontalEnd, color);
            }
        } else {
            // Flat line
            const start: PointF2D = new PointF2D(absolute.x, absolute.y);
            const end: PointF2D = new PointF2D(absolute.x + width, absolute.y);
            this.drawLine(start, end, color);
        }
    }

    private drawStaffEntry(staffEntry: GraphicalStaffEntry): void {
        if (staffEntry.FingeringEntries.length > 0) {
            for (const fingeringEntry of staffEntry.FingeringEntries) {
                fingeringEntry.SVGNode = this.drawLabel(fingeringEntry, GraphicalLayers.Notes);
            }
        }
        // Draw ChordSymbols
        if (staffEntry.graphicalChordContainers !== undefined && staffEntry.graphicalChordContainers.length > 0) {
            for (const graphicalChordContainer of staffEntry.graphicalChordContainers) {
                const label: GraphicalLabel = graphicalChordContainer.GraphicalLabel;
                label.SVGNode = this.drawLabel(label, <number>GraphicalLayers.Notes);
            }
        }
        if (this.rules.RenderLyrics) {
            if (staffEntry.LyricsEntries.length > 0) {
                this.drawLyrics(staffEntry.LyricsEntries, <number>GraphicalLayers.Notes);
            }
        }
    }

    /**
     * Draw all lyrics to the canvas
     * @param lyricEntries Array of lyric entries to be drawn
     * @param layer Number of the layer that the lyrics should be drawn in
     */
    private drawLyrics(lyricEntries: GraphicalLyricEntry[], layer: number): void {
        lyricEntries.forEach(lyricsEntry => {
            const label: GraphicalLabel = lyricsEntry.GraphicalLabel;
            label.SVGNode = this.drawLabel(label, layer);
        });
    }

    protected drawInstrumentBrace(brace: GraphicalObject, system: MusicSystem): void {
        // Draw InstrumentBrackets at beginning of line
        const vexBrace: VexFlowInstrumentBrace = (brace as VexFlowInstrumentBrace);
        vexBrace.draw(this.backend.getContext());
    }

    protected drawGroupBracket(bracket: GraphicalObject, system: MusicSystem): void {
        // Draw InstrumentBrackets at beginning of line
        const vexBrace: VexFlowInstrumentBracket = (bracket as VexFlowInstrumentBracket);
        vexBrace.draw(this.backend.getContext());
    }

    protected drawOctaveShifts(staffLine: StaffLine): void {
        for (const graphicalOctaveShift of staffLine.OctaveShifts) {
            if (graphicalOctaveShift) {
                const vexFlowOctaveShift: VexFlowOctaveShift = graphicalOctaveShift as VexFlowOctaveShift;
                const ctx: Vex.IRenderContext = this.backend.getContext();
                const textBracket: VF.TextBracket = vexFlowOctaveShift.getTextBracket();
                textBracket.setContext(ctx);
                try {
                    textBracket.draw();
                } catch (ex) {
                    log.warn(ex);
                }
            }
        }
    }

    protected drawPedals(staffLine: StaffLine): void {
        for (const graphicalPedal of staffLine.Pedals) {
            if (graphicalPedal) {
                const vexFlowPedal: VexFlowPedal = graphicalPedal as VexFlowPedal;
                const ctx: Vex.IRenderContext = this.backend.getContext();
                const pedalMarking: Vex.Flow.PedalMarking = vexFlowPedal.getPedalMarking();
                pedalMarking.setContext(ctx);
                pedalMarking.draw();
            }
        }
    }

    protected drawWavyLines(staffLine: StaffLine): void {
        for (const graphicalWavyLine of staffLine.WavyLines) {
            if (graphicalWavyLine) {
                const vexFlowVibratoBracket: VexflowVibratoBracket = graphicalWavyLine as VexflowVibratoBracket;
                const ctx: Vex.IRenderContext = this.backend.getContext();
                const vfVibratoBracket: Vex.Flow.VibratoBracket = vexFlowVibratoBracket.getVibratoBracket();
                (vfVibratoBracket as any).setContext(ctx);
                vfVibratoBracket.draw();
            }
        }
    }

    protected getDotId = (abstractGraphicalExpression, only?: "x" | "y"): string => {
        if (only) {
            return String(abstractGraphicalExpression.Label.PositionAndShape.RelativePosition[only]);
        }

          return (
            String(abstractGraphicalExpression.Label.PositionAndShape.RelativePosition.x) +
            String(abstractGraphicalExpression.Label.    PositionAndShape.RelativePosition.y)
          );
    };

    protected hasDot = (point: PointF2D, points?: PointF2D[], abs?: boolean, dots?: {[key: string]: PointF2D}): boolean => {
        const dotsValues: PointF2D[] = points || Object.values(dots);
        // console.log(dotsValues)
        for (const dot of dotsValues) {
            // console.log(point, dot)
            if (abs) {
                return point.x === dot.x && point.y === dot.y;
            }
            if (point.x === dot.x) {
                return true;
            }
        }
        return false;
    };

    protected getMaxExpressionsYAndDots(staffline: StaffLine): {
        maxY: number;
        dots: {[key: string]: PointF2D};
        dotsPoint: {[key: string]: PointF2D[]};
        dotsPointByX: {[key: string]: PointF2D[]};
    } {
        const measureNumbers: number[] = [];
        let y: number = 0;
        for (const measure of (staffline as any).measures) {
            measureNumbers.push(measure.measureNumber);
        }
        const dots: {[key: string]: PointF2D} = {};
        const dotsPoint: {[key: string]: PointF2D[]} = {};
        const dotsPointByX: {[key: string]: PointF2D[]} = {};

        // 先循环一次获取每个相对位置的文字数
        for (const abstractGraphicalExpression of staffline.AbstractExpressions) {
            if ((
                abstractGraphicalExpression instanceof GraphicalUnknownExpression &&
                measureNumbers.includes((abstractGraphicalExpression as any).parentMeasure.MeasureNumber)
            )) {
                const id: string = this.getDotId(abstractGraphicalExpression);
                const idx: string = this.getDotId(abstractGraphicalExpression, "x");
                if (!dotsPoint[id]) {
                    dotsPoint[id] = [];
                }
                if (!dotsPointByX[idx]) {
                    dotsPointByX[idx] = [];
                }
                if (!this.hasDot(abstractGraphicalExpression.Label.PositionAndShape.RelativePosition, dotsPoint[id], true)) {
                    dotsPoint[id].push(abstractGraphicalExpression.Label.PositionAndShape.RelativePosition);
                    dotsPointByX[idx].push(abstractGraphicalExpression.Label.PositionAndShape.RelativePosition);
                }
                // if (!hasDot(abstractGraphicalExpression.Label.PositionAndShape.RelativePosition, dotsPoint[id])) {
                    // dotsPointByX[idx].push(abstractGraphicalExpression.Label.PositionAndShape.RelativePosition)
                // }
                // if (!hasDot(abstractGraphicalExpression.Label.PositionAndShape.RelativePosition)) {
                    dots[id] = abstractGraphicalExpression.Label.PositionAndShape.RelativePosition;
                // }
            }
        }
        let needIgnoreMeasureNumber: number = 0;
        for (const abstractGraphicalExpression of staffline.AbstractExpressions) {
            const measureNumber: number = (abstractGraphicalExpression as any).parentMeasure?.MeasureNumber;
            if (abstractGraphicalExpression instanceof GraphicalUnknownExpression && measureNumbers.includes(measureNumber)) {
                const stave: any = (abstractGraphicalExpression as any).parentMeasure.verticalMeasureList[0]?.stave;
                const width: number = stave?.end_x - stave?.x;
                const idx: string = this.getDotId(abstractGraphicalExpression, "x");
                if ((dotsPointByX[idx] || []).length === 1 && needIgnoreMeasureNumber !== measureNumber) {
                    // console.log((abstractGraphicalExpression as any).parentMeasure.MeasureNumber)
                    const lableY: number = abstractGraphicalExpression.Label.PositionAndShape.AbsolutePosition.y;
                    y = y === 0 ? lableY : Math.min(y, lableY);
                    // console.log(y)
                }
                if ((dotsPointByX[idx] || []).length !== 1) {
                    if (width / 10 < abstractGraphicalExpression.Label.PositionAndShape.MarginSize.width) {
                        needIgnoreMeasureNumber = measureNumber + 1;
                    }
                }
                // getDotId(abstractGraphicalExpression)
                // if (!hasDot(abstractGraphicalExpression.Label.PositionAndShape.RelativePosition)) {

                // }
                // console.log(dots)
                // dots[getDotId(abstractGraphicalExpression)] = abstractGraphicalExpression.Label.PositionAndShape.RelativePosition
                // console.log(abstractGraphicalExpression)
                // if ((abstractGraphicalExpression as any).parentMeasure.staffLinkedExpressions[0].length === 1) {

                // }
                // if ((abstractGraphicalExpression as any).parentMeasure.staffLinkedExpressions[0].length > 1) {
                //     const lastExpression = [...(abstractGraphicalExpression as any).parentMeasure.staffLinkedExpressions[0]].pop()
                //     console.log(abstractGraphicalExpression, lastExpression)
                //     const lableY = lastExpression.Label.PositionAndShape.AbsolutePosition.y
                //     y = y === 0 ? lableY : Math.min(y, lableY)
                // }
            }
        }
        return {
            maxY: y,
            dots,
            dotsPoint,
            dotsPointByX
        };
    }

    protected drawExpressions(staffline: StaffLine): void {
        /**
         *
         * 修改 添加统一的高度
         */
        const numByIdx: any = {};
        const used: string[] = [];
        const {maxY, dotsPointByX} = this.getMaxExpressionsYAndDots(staffline);
        // Draw all Expressions
        for (const abstractGraphicalExpression of staffline.AbstractExpressions) {
            // Draw InstantaniousDynamics
            if (abstractGraphicalExpression instanceof GraphicalInstantaneousDynamicExpression) {
                this.drawInstantaneousDynamic((abstractGraphicalExpression as VexFlowInstantaneousDynamicExpression));
                // Draw InstantaniousTempo
            } else if (abstractGraphicalExpression instanceof GraphicalInstantaneousTempoExpression) {
                const label: GraphicalLabel = (abstractGraphicalExpression as GraphicalInstantaneousTempoExpression).GraphicalLabel;
                label.SVGNode = this.drawLabel(label, GraphicalLayers.Notes);
                // const oldy: number = abstractGraphicalExpression.Label.PositionAndShape.AbsolutePosition.y;
                // console.log(label, GraphicalLayers.Notes);
                // abstractGraphicalExpression.Label.PositionAndShape.AbsolutePosition.y = oldy + 10;
                // Draw ContinuousDynamics
            } else if (abstractGraphicalExpression instanceof GraphicalContinuousDynamicExpression) {
                this.drawContinuousDynamic((abstractGraphicalExpression as VexFlowContinuousDynamicExpression));
                // Draw ContinuousTempo
                // } else if (abstractGraphicalExpression instanceof GraphicalContinuousTempoExpression) {
                //     this.drawLabel((abstractGraphicalExpression as GraphicalContinuousTempoExpression).GraphicalLabel, GraphicalLayers.Notes);
                // // Draw Mood
                // } else if (abstractGraphicalExpression instanceof GraphicalMoodExpression) {
                //     GraphicalMoodExpression; graphicalMood = (GraphicalMoodExpression); abstractGraphicalExpression;
                //     drawLabel(graphicalMood.GetGraphicalLabel, <number>GraphicalLayers.Notes);
            // Draw Unknown
            } else if (abstractGraphicalExpression instanceof GraphicalUnknownExpression) {
                const id: string = this.getDotId(abstractGraphicalExpression);
                const idx: string = this.getDotId(abstractGraphicalExpression, "x");
                if ((dotsPointByX[idx] || []).length === 1) {
                    // abstractGraphicalExpression.Label.PositionAndShape.AbsolutePosition.y = maxY
                } else {
                    const oldy: number = abstractGraphicalExpression.Label.PositionAndShape.AbsolutePosition.y;
                    if (!numByIdx[idx]) {
                        numByIdx[idx] = -Math.abs(maxY - oldy);
                        // console.log(maxY, oldy)
                        abstractGraphicalExpression.Label.PositionAndShape.AbsolutePosition.y = oldy + numByIdx[idx];
                    } else {
                        if (!used.includes(id) && numByIdx[idx] !== 0) {
                            abstractGraphicalExpression.Label.PositionAndShape.AbsolutePosition.y = oldy - 1;
                        }
                    }

                    used.push(id);
                    // abstractGraphicalExpression.Label.PositionAndShape.AbsolutePosition.y = oldy - maxY + oldy
                    // console.log(-oldy - maxY + oldy)
                    // abstractGraphicalExpression.Label.PositionAndShape.AbsolutePosition.y = oldy + numByIdx[idx]
                    // console.log(abstractGraphicalExpression.Label)
                }
                // this.drawLabel(abstractGraphicalExpression.Label, <number>GraphicalLayers.Notes);
                const label: GraphicalLabel = abstractGraphicalExpression.Label;
                label.SVGNode = this.drawLabel(label, <number>GraphicalLayers.Notes);
            } else {
                log.warn("Unkown type of expression!");
            }
        }
    }

    protected drawInstantaneousDynamic(instantaneousDynamic: GraphicalInstantaneousDynamicExpression): void {
        const label: GraphicalLabel = (instantaneousDynamic as VexFlowInstantaneousDynamicExpression).Label;
        label.SVGNode = this.drawLabel(label, <number>GraphicalLayers.Notes);
    }

    protected drawContinuousDynamic(graphicalExpression: VexFlowContinuousDynamicExpression): void {
        if (graphicalExpression.IsVerbal) {
            const label: GraphicalLabel = graphicalExpression.Label;
            label.SVGNode = this.drawLabel(label, <number>GraphicalLayers.Notes);
        } else {
            // 修改 换行的渐强线错误
            try {
                if (graphicalExpression.Lines.length === 2 && Math.abs(graphicalExpression.Lines[0].Start.x - graphicalExpression.Lines[1].Start.x) > Number.EPSILON){
                    let minX = Math.min(...graphicalExpression.Lines.map(n => n.Start.x));
                    let startYMin = Math.min(...graphicalExpression.Lines.map(n => n.Start.y));
                    graphicalExpression.Lines[0].Start.x = minX;
                    graphicalExpression.Lines[1].Start.x = minX;
                    graphicalExpression.Lines[0].Start.y = startYMin + this.rules.WedgeOpeningLength / 2;
                    graphicalExpression.Lines[1].Start.y = startYMin + this.rules.WedgeOpeningLength * 2;
                    graphicalExpression.Lines[0].End.y += this.rules.WedgeOpeningLength / 2;
                    graphicalExpression.Lines[1].End.y += this.rules.WedgeOpeningLength;
                }
            } catch (error) {

            }

            const ctx = this.backend.getContext()
            ctx.openGroup('lineGroup')
            for (const line of graphicalExpression.Lines) {
                const start: PointF2D = new PointF2D(graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.x + line.Start.x,
                                                     graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.y + line.Start.y);
                const end: PointF2D = new PointF2D(graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.x + line.End.x,
                                                   graphicalExpression.ParentStaffLine.PositionAndShape.AbsolutePosition.y + line.End.y);
                this.drawLine(start, end, "black", line.Width);
            }
            ctx.closeGroup()
        }
    }

    /**
     * Renders a Label to the screen (e.g. Title, composer..)
     * @param graphicalLabel holds the label string, the text height in units and the font parameters
     * @param layer is the current rendering layer. There are many layers on top of each other to which can be rendered. Not needed for now.
     * @param bitmapWidth Not needed for now.
     * @param bitmapHeight Not needed for now.
     * @param heightInPixel the height of the text in screen coordinates
     * @param screenPosition the position of the lower left corner of the text in screen coordinates
     */
    protected renderLabel(graphicalLabel: GraphicalLabel, layer: GraphicalLayers, specs: LabelRenderSpecs): Node {
        return this._renderLabel(graphicalLabel, specs);
    }

    private _renderLabel(graphicalLabel: GraphicalLabel, specs: LabelRenderSpecs): Node {
        if (!graphicalLabel.Label.print) {
            return undefined;
        }
        const height: number = graphicalLabel.Label.fontHeight * unitInPixels;
        const { font } = graphicalLabel.Label;
        let color: string;
        if (this.rules.ColoringEnabled) {
            color = graphicalLabel.Label.colorDefault;
            if (graphicalLabel.Label.color) {
                color = graphicalLabel.Label.color.toString();
            }
            if (!color) {
                color = this.rules.DefaultColorLabel;
            }
        }
        let { fontStyle, fontFamily } = graphicalLabel.Label;
        if (!fontStyle) {
            fontStyle = this.rules.DefaultFontStyle;
        }
        if (!fontFamily) {
            fontFamily = this.rules.DefaultFontFamily;
        }

        let node: Node;
        for (let i: number = 0; i < graphicalLabel.TextLines?.length; i++) {
            const currLine: {text: string, xOffset: number, width: number} = graphicalLabel.TextLines[i];
            const xOffsetInPixel: number = this.calculatePixelDistance(currLine.xOffset);
            const linePosition: PointF2D = new PointF2D(specs.ScreenPosition.x + xOffsetInPixel, specs.ScreenPosition.y);
            const newNode: Node =
                this.backend.renderText(height, fontStyle, font, currLine.text, specs.FontHeightInPixel, linePosition, color, graphicalLabel.Label.fontFamily);
            if (!node) {
                node = newNode;
            } else {
                node.appendChild(newNode);
            }
            specs.ScreenPosition.y = specs.ScreenPosition.y + specs.FontHeightInPixel;
            if (graphicalLabel.TextLines.length > 1) {
                specs.ScreenPosition.y += this.rules.SpacingBetweenTextLines;
            }
        }
        // font currently unused, replaced by fontFamily
        return node; // this will be a merge conflict with annotations, refactor there to handle node array instead of single node
    }

    /**
     * Renders a rectangle with the given style to the screen.
     * It is given in screen coordinates.
     * @param rectangle the rect in screen coordinates
     * @param layer is the current rendering layer. There are many layers on top of each other to which can be rendered. Not needed for now.
     * @param styleId the style id
     * @param alpha alpha value between 0 and 1
     */
    protected renderRectangle(rectangle: RectangleF2D, layer: number, styleId: number, colorHex: string, alpha: number): Node {
        return this.backend.renderRectangle(rectangle, styleId, colorHex, alpha);
    }

    /**
     * Converts a point from unit to pixel space.
     * @param point
     * @returns {PointF2D}
     */
    protected applyScreenTransformation(point: PointF2D): PointF2D {
        return new PointF2D(point.x * unitInPixels, point.y * unitInPixels);
    }

    /**
     * Converts a rectangle from unit to pixel space.
     * @param rectangle
     * @returns {RectangleF2D}
     */
    protected applyScreenTransformationForRect(rectangle: RectangleF2D): RectangleF2D {
        return new RectangleF2D(rectangle.x * unitInPixels, rectangle.y * unitInPixels, rectangle.width * unitInPixels, rectangle.height * unitInPixels);
    }
}
