import { Chart, ChartEvent, Plugin, Scale } from "chart.js";
import { CursorOptions, CursorPosition } from "./types";

const defaultOptions: CursorOptions = {
    enabled: false,
    visible: true,
    positions: [],
    line: {
        color: "blue",
        width: 1,
        dashPattern: [],
    },
};

const getXScale = (chart: Chart): Scale | null => {
    const meta = chart.getDatasetMeta(0);
    return chart.data.datasets.length && meta.xAxisID
        ? chart.scales[meta.xAxisID]
        : null;
};

const getYScale = (chart: Chart): Scale | null => {
    const meta = chart.getDatasetMeta(0);
    return chart.data.datasets.length && meta.yAxisID
        ? chart.scales[meta.yAxisID]
        : null;
};

const CursorsPlugin: Plugin<"line"> = {
    id: "cursors",
    afterInit(chart: Chart) {
        if (!chart.options?.scales?.x) {
            return;
        }
        const xScaleType = chart.options.scales.x.type;
        if (
            ![
                "linear",
                "time",
                "timeseries",
                "category",
                "logarithmic",
                "realtime",
            ].includes(xScaleType as string)
        ) {
            return;
        }

        if (!chart.options?.plugins) {          
            chart.options.plugins = {};
        }

        if (chart.options.plugins?.cursors === undefined) {          
            chart.options.plugins.cursors = defaultOptions;
        }

        if (!chart.options.plugins.cursors.enabled) {
            return;
        }

        let positions: CursorPosition[] = [];
        if (chart.options.plugins.cursors.positions) {
            positions = chart.options.plugins.cursors.positions.map(
                (position) => ({
                    previousX: undefined,
                    previousY: undefined,
                    x: position.x ?? 0,
                    y: position.y ?? 0,
                })
            );
        }

        chart.cursors = {
            enabled: true,
            visible: chart.options.plugins.cursors.visible ?? true,
            suppressUpdate: false,
            ignoreNextEvents: 0,
            selected: -1,
            positions,
            startTime: undefined,
            centreCursors: this.centreCursors,
            hitTest: (x: number, y: number, threshold: number) =>
                this.hitTestCursor(chart, x, y, threshold),
            setStartTime: (time: number) => {
                chart.cursors.startTime = time;
            },
        };
    },

    afterEvent(
        chart: Chart,
        args: {
            event: ChartEvent;
            replay: boolean;
            changed?: boolean;
            cancelable: false;
            inChartArea: boolean;
        }
    ) {
        if (
            !chart.cursors?.enabled ||
            chart.cursors.suppressUpdate ||
            !chart.options?.scales?.x
        ) {
            return;
        }
        // do nothing if xScale is not linear, time, timeseries, category, logarithmic or realtime
        const xScaleType = chart.options.scales.x.type as string;
        if (
            ![
                "linear",
                "time",
                "timeseries",
                "category",
                "logarithmic",
                "realtime",
            ].includes(xScaleType)
        ) {
            return;
        }
        // if startTime is not set, check options else do nothing
        if (
            chart.cursors.startTime === undefined &&
            xScaleType === "realtime"
        ) {
            chart.cursors.startTime = chart.options.plugins?.cursors?.startTime;
            if (chart.cursors.startTime === undefined) {
                return;
            }
        }

        const xScale = getXScale(chart);
        const yScale = getYScale(chart);
        if (!xScale || !yScale) {
            return;
        }

        if (chart.cursors.ignoreNextEvents > 0) {
            chart.cursors.ignoreNextEvents -= 1;
            return;
        }

        const e = args.event;

        if (!e.x || !e.y) {
            return false;
        }
        // used to do a check for cursor outside chart here

        switch (e.type) {
            case "mousedown":
                const selected = chart.cursors.hitTest(e.x, e.y, 10);
                chart.cursors.selected = selected;
                break;
            case "mouseup":
                chart.cursors.selected = -1;
                break;
        }

        if (
            chart.cursors.positions &&
            chart.cursors.selected >= chart.cursors.positions.length
        ) {
            chart.cursors.selected = -1;
        }

        if (chart.cursors.selected >= 0) {
            chart.cursors.positions[chart.cursors.selected].x =
                xScale.getValueForPixel(e.x) ?? 0;
            chart.cursors.positions[chart.cursors.selected].y =
                yScale.getValueForPixel(e.y) ?? 0;
        }

        chart.draw();
    },

    afterDraw(chart: Chart) {
        if (!chart.cursors?.enabled) {
            return false;
        }

        const newVisible = chart.options.plugins?.cursors?.visible;
        
        if (!newVisible) {
            chart.cursors.visible = false;
            return false;
        }

        // if visible has changed, centre cursors
        if (chart.cursors.visible !== newVisible) {
            chart.cursors.visible = newVisible;
            this.centreCursors(chart);
        }

        const xScaleType = chart.options.scales?.x?.type;
        if (
            (xScaleType as string) === "realtime" &&
            chart.cursors.startTime === undefined
        ) {
            chart.cursors.startTime = chart.options.plugins?.cursors?.startTime;
            if (chart.cursors.startTime === undefined) {
                return false;
            }
        }

        this.drawCursors(chart);
        return true;
    },

    drawCursors(chart: Chart) {
        if (!chart.cursors?.enabled || !chart.cursors.visible) {
            return;
        }

        const xScale = getXScale(chart);
        const yScale = getYScale(chart);

        if (!xScale || !yScale) {
            return;
        }

        const xScaleType = chart.options.scales?.x?.type as string;

        if (
            chart.cursors.positions?.some(
                (p) => p.x === undefined || p.y === undefined
            )
        ) {
            this.centreCursors(chart);
            chart.draw();
        }

        const xMinPixel = xScale.getPixelForValue(xScale.min);
        const xMaxPixel = xScale.getPixelForValue(xScale.max);
        const yMinPixel = yScale.getPixelForValue(yScale.min);
        const yMaxPixel = yScale.getPixelForValue(yScale.max);

        const lineWidth = chart.options.plugins?.cursors?.line?.width ?? 1;
        const color = chart.options.plugins?.cursors?.line?.color ?? "blue";
        const dashPattern = (chart.options.plugins?.cursors?.line
            ?.dashPattern ?? []) as number[];

        chart.cursors.positions?.forEach((cursor, index) => {
            if (cursor.x === undefined || cursor.y === undefined) {
                return;
            }

            const cursorX = xScale.getPixelForValue(cursor.x);
            const cursorY = yScale.getPixelForValue(cursor.y);

            let xString = cursor.x.toFixed(3);

            if (
                xScaleType === "realtime" &&
                chart.cursors.startTime !== undefined
            ) {
                const secondsSinceStart = (
                    (cursor.x - chart.cursors.startTime) /
                    1000
                ).toFixed(2);
                xString = `(${secondsSinceStart}s`;
            }

            const ctx = chart.ctx;
            ctx.save();
            
            ctx.fillStyle = color;
            ctx.font = "12px Arial";

            if (chart.cursors.selected === index) {
                ctx.setLineDash([]);
                ctx.fillText(
                    `${xString}, ${cursor.y.toFixed(3)})`,
                    cursorX + 5,
                    cursorY - 5
                );
            } else {
                ctx.beginPath();
                ctx.arc(cursorX, cursorY, 3, 0, 2 * Math.PI);
                ctx.fill();
                ctx.fillText(
                    `${index + 1}`,
                    cursorX - 12,
                    cursorY + 12
                );
            }

            // Draw vertical and horizontal lines
            ctx.beginPath();
            ctx.setLineDash(dashPattern);
            ctx.lineWidth = lineWidth;
            ctx.strokeStyle = color;

            ctx.moveTo(cursorX, yMaxPixel);
            ctx.lineTo(cursorX, yMinPixel);
            ctx.moveTo(xMinPixel, cursorY);
            ctx.lineTo(xMaxPixel, cursorY);

            ctx.stroke();
            ctx.restore();
        });

        const afterMoveCallback =
            chart.options.plugins?.cursors?.callbacks?.afterMove;
        if (afterMoveCallback && chart.cursors.positions) {
            const changed = chart.cursors.positions.some(
                (cursor) =>
                    cursor.previousX !== cursor.x ||
                    cursor.previousY !== cursor.y
            );

            if (changed) {
                const positions = chart.cursors.positions.map((p, i) => ({
                    index: i,
                    x: parseFloat(p.x?.toFixed(2) ?? "0"),
                    y: parseFloat(p.y?.toFixed(2) ?? "0"),
                }));

                afterMoveCallback(positions);

                chart.cursors.positions.forEach((cursor) => {
                    cursor.previousX = cursor.x;
                    cursor.previousY = cursor.y;
                });
            }
        }
    },

    hitTestCursor(
        chart: Chart,
        x: number,
        y: number,
        threshold: number
    ): number {
        const xScale = getXScale(chart);
        const yScale = getYScale(chart);
        if (!xScale || !yScale || !chart.cursors.positions) return -1;

        return chart.cursors.positions.findIndex((cursor) => {
            if (cursor.x === undefined || cursor.y === undefined) return false;
            const cursorX = xScale.getPixelForValue(cursor.x);
            const cursorY = yScale.getPixelForValue(cursor.y);
            const distance = Math.sqrt((x - cursorX) ** 2 + (y - cursorY) ** 2);
            return distance < threshold;
        });
    },

    centreCursors(chart: Chart) {
        const xScale = getXScale(chart);
        const yScale = getYScale(chart);

        if (!xScale || !yScale) {
            return;
        }

        const xDistance = xScale.max - xScale.min;
        const yDistance = yScale.max - yScale.min;

        const xPrecision = xDistance <= 5 ? 1 : 0;
        const yPrecision = yDistance <= 5 ? 1 : 0;

        chart.cursors.positions?.forEach(function (cursor) {
            cursor.x = parseFloat(
                ((xScale.min + xScale.max) / 2).toFixed(xPrecision)
            );
            cursor.y = parseFloat(
                ((yScale.min + yScale.max) / 2).toFixed(yPrecision)
            );
        });
    },
};

export default CursorsPlugin;
