import { Metric } from "./metric.mjs";

export const COLORS = Object.freeze(["blue", "blue-light", "green-light", "green", "yellow", "orange", "red", "magenta", "violet", "purple", "blue-dark", "green-dark", "ochre", "rust"]);

export function renderMetricView(viewParams) {
    let { metrics, width = 500, trackHeight = 20, subMetricMargin = 35, title = "", colors = COLORS } = viewParams;
    // Make sure subMetricMargin is set for use in renderSubMetrics.
    viewParams.subMetricMargin = subMetricMargin;
    const scatterPlotParams = { width, trackHeight, colors };

    scatterPlotParams.xAxisPositiveOnly = false;
    scatterPlotParams.xAxisShowZero = true;
    scatterPlotParams.values = prepareScatterPlotValues(metrics, true);
    scatterPlotParams.unit = "%";
    scatterPlotParams.xAxisLabel = "Spread Normalized";
    const normalizedScatterPlot = renderScatterPlot(scatterPlotParams);

    scatterPlotParams.xAxisPositiveOnly = true;
    scatterPlotParams.xAxisShowZero = false;
    scatterPlotParams.values = prepareScatterPlotValues(metrics, false);
    scatterPlotParams.unit = metrics[0].unit;
    scatterPlotParams.xAxisLabel = metrics[0].unit;
    const absoluteScatterPlot = renderScatterPlot(scatterPlotParams);

    const legend = metrics
        .map(
            (metric, i) => `
                <tr >
                    <td class="${colors[i % colors.length]} no-select" >●</td>
                    <td class="label">${metric.shortName}</td>
                    <td class="number">${metric.mean.toFixed(2)}</td>
                    <td>±</td>
                    <td>${metric.deltaString}</td>
                    <td>${metric.unit}</td>
                </tr>`
        )
        .join("");
    return `
        <dl class="metric">
            <dt><h3>${title}<h3></dt>
            <dd>
                <div class="metric-chart"">
                    <div onclick="document.body.classList.toggle('relative-charts')">
                        <div class="metric-chart-absolute">
                            ${absoluteScatterPlot}
                        </div>
                        <div class="metric-chart-relative">
                            ${normalizedScatterPlot}
                        </div>
                    </div>
                    <table class="chart chart-legend">${legend}</table>
                </div>
                ${renderSubMetrics(viewParams)}
            </dd>
        </dl>
    `;
}

function renderSubMetrics(viewParams) {
    const { metrics, width, subMetricMargin, colors = COLORS, renderChildren = true } = viewParams;
    const valuesTable = `
            <label class="details-toggle">
                <input type="checkbox" 
                        onclick="this.parentNode.nextElementSibling.classList.toggle('visible')" />
                Table
            </label>
            <div class="submetrics">
                ${renderMetricsTable(metrics)}
            </div>`;
    const hasChildMetric = metrics.length > 0 && metrics[0].children.length > 0;
    if (!hasChildMetric || !renderChildren)
        return valuesTable;

    const subMetricWidth = width - subMetricMargin;
    const childColors = [...colors];

    const subMetrics = metrics
        .map((metric) => {
            // Rotate colors to get different colors for sub-plots.
            for (let i = 0; i < metric.children.length; i++) {
                const color = childColors.pop();
                childColors.unshift(color);
            }
            const subMetricParams = {
                ...viewParams,
                parentMetric: metric,
                metrics: metric.children,
                title: metric.name,
                width: subMetricWidth,
                colors: childColors,
            };
            return renderMetricView(subMetricParams);
        })
        .join("");
    return `${valuesTable}
        <label class="details-toggle">
            <input type="checkbox" 
                    onclick="this.parentNode.nextElementSibling.classList.toggle('visible')" />
            Submetrics 
        </label>
        <div class="submetrics">
            ${subMetrics}
        </div>
    `;
}

function renderMetricsTable(metrics, min, max) {
    let numRows = 0;
    let columnHeaders = "";
    let commonPrefixes = metrics[0].name.split(Metric.separator);
    for (const metric of metrics) {
        const prefixes = metric.name.split(Metric.separator);
        for (let i = commonPrefixes.length - 1; i >= 0; i--) {
            if (commonPrefixes[i] !== prefixes[i])
                commonPrefixes.pop();
        }

    }
    const commonPrefix = commonPrefixes.join(Metric.separator);
    let commonPrefixHeader = "";
    if (commonPrefix) {
        commonPrefixHeader = `
            <tr>
                <td></td>
                <td colspan="${metrics.length}" class="prefix">${commonPrefix}</td>
            </tr>`;
    }
    for (const metric of metrics) {
        const name = metric.name.substring(commonPrefix.length);
        columnHeaders += `<th>${name} [${metric.unit}]</th>`;
        numRows = Math.max(metric.values.length, numRows);
    }

    let body = "";
    for (let row = 0; row < numRows; row++) {
        let columns = "";
        for (const metric of metrics) {
            const value = metric.values[row];
            if (value === undefined)
                continue;
            const delta = metric.max - metric.min;
            const percent = Math.max(Math.min((value - metric.min) / delta, 1), 0) * 100;
            const percentGradient = `background: linear-gradient(90deg, var(--foreground-alpha) ${percent}%, rgba(0,0,0,0) ${percent}%);`;
            columns += `<td style="${percentGradient}">${value.toFixed(2)}</td>`;
        }
        body += `<tr>
            <td>${row}</td>
            ${columns}
        </tr>`;
    }
    return `<table class="metrics-table" >
        <thead onclick="this.classList.toggle('nowrap')" >
            ${commonPrefixHeader}
            <tr>
                <th>Iteration</th>
                ${columnHeaders}
            </tr>
        </thead>
        <tbody>
        ${body}
        <tbody>
    </table>`;
}

function prepareScatterPlotValues(metrics, normalize = true) {
    let points = [];
    // Arrange child-metrics values in a single coordinate system:
    // - metric 1: x values are in range [0, 1]
    // - metric 2: y values are in range [1, 2]
    // - ...
    // This way each metric data point is on a separate track in the scatter
    // plot.
    // If normalize == true:
    //   All x values are normalized by the mean of each metric and
    //   centered on 0.
    //   Example: [90ms, 100ms, 110ms] =>  [-10%, 0%, +10%]
    const toPercent = 100;
    let unit;
    for (let metricIndex = 0; metricIndex < metrics.length; metricIndex++) {
        const metric = metrics[metricIndex];
        // If the mean is 0 we can't normalize values properly.
        const mean = metric.mean || 1;
        if (!unit)
            unit = metric.unit;
        else if (unit !== metric.unit)
            throw new Error("All metrics must have the same unit.");
        let width = metric.delta || 1;
        let center = mean;
        if (normalize) {
            width = (metric.delta / mean) * toPercent;
            center = 0;
        }
        const left = center - width / 2;
        const y = metricIndex;
        const label = `Mean: ${metric.valueString}\n` + `Min: ${metric.min.toFixed(2)}${unit}\n` + `Max: ${metric.max.toFixed(2)}${unit}`;
        const rect = [left, y, label, width];
        // Add data for individual points:
        points.push(rect);
        const values = metric.values;
        const length = values.length;
        for (let i = 0; i < length; i++) {
            const value = values[i];
            let x = value;
            let normalized = (value / mean - 1) * toPercent;
            if (normalize)
                x = normalized;
            const sign = normalized < 0 ? "-" : "+";
            normalized = Math.abs(normalized);
            // Each value is mapped to a y-coordinate in the range of [metricIndex, metricIndex + 1]
            const valueOffsetY = length === 1 ? 0.5 : i / length;
            const y = metricIndex + valueOffsetY;
            let label = `Iteration ${i}: ${value.toFixed(3)}${unit}\n` + `Normalized: ${metric.mean.toFixed(3)}${unit} ${sign} ${normalized.toFixed(2)}%`;
            const point = [x, y, label];
            points.push(point);
        }
    }
    return points;
}

function renderScatterPlot({ values, width = 500, height, trackHeight, xAxisPositiveOnly = false, xAxisShowZero = false, xAxisLabel, unit = "", colors = COLORS }) {
    if (!height && !trackHeight)
        throw new Error("Either height or trackHeight must be specified");
    let xMin = Infinity;
    let xMax = 0;
    let yMin = Infinity;
    let yMax = 0;
    for (let value of values) {
        let [x, y] = value;
        xMin = Math.min(xMin, x);
        xMax = Math.max(xMax, x);
        yMin = Math.min(yMin, y);
        yMax = Math.max(yMax, y);
    }
    if (xAxisPositiveOnly)
        xMin = Math.max(xMin, 0);
    // Max delta of values across each axis:
    const trackCount = Math.ceil(yMax - yMin) || 1;
    const spreadX = xMax - xMin;
    // Axis + labels height:
    const axisHeight = 18;
    const axisMarginY = 4;
    const trackMargin = 2;
    let markerSize = 5;
    // Auto-adjust markers to [2px, 5px] for high iteration counts:
    const iterationsLimit = 20;
    if (values.length > iterationsLimit)
        markerSize = 2 + (3 / values.length) * iterationsLimit;
    // Recalculate height:
    if (height)
        trackHeight = (height - axisHeight - axisMarginY) / trackCount;
    else
        height = trackCount * trackHeight + axisHeight + axisMarginY;

    // Horizontal axis position:
    const axisY = height - axisHeight + axisMarginY;
    const unitToPosX = width / spreadX;
    const unitToPosY = trackHeight - trackMargin - markerSize / 2;
    const points = values.map(renderValue).join("");
    let xAxisZeroLine = "";
    if (xAxisShowZero) {
        const xZeroPos = (0 - xMin) * unitToPosX;
        xAxisZeroLine = `<line x1="${xZeroPos}" x2="${xZeroPos}" y1="${0}" y2="${axisY}" class="axis"/>`;
    }
    return `
        <svg class="scatter-plot chart"
            width="${width}" height="${height}"
            viewBox="${`0 0 ${width} ${height}`}">
            <g class="horizontal-axis no-select">
                <line
                    x1="${0}" x2="${width}"
                    y1="${axisY - axisMarginY}" y2="${axisY - axisMarginY}"
                    class="axis" />
                <text y="${axisY}" x="0" text-anchor="start">${xMin.toFixed(2)}${unit}</text>
                <text y="${axisY}" x="${width / 2}" text-anchor="middle">${xAxisLabel}</text>
                <text y="${axisY}" x="${width}" text-anchor="end">${xMax.toFixed(2)}${unit}</text>
            </g>
            <defs>
                <g id="marker">
                    <circle r="${markerSize / 2}" />
                </g>
            </defs>
            <g class="values">
                ${xAxisZeroLine}
                ${points}
            </g>
        </svg>
    `;

    function renderValue(value) {
        const [rawX, rawY, label, rawWidth = 0] = value;
        const trackIndex = rawY | 0;
        const y = (rawY - yMin) * unitToPosY + markerSize * trackIndex;
        const cssClass = colors[trackIndex % colors.length];

        if (value.length <= 3) {
            // Render a simple marker:
            const x = (rawX - xMin) * unitToPosX;
            const adjustedY = y + markerSize / 2;
            return `
                <use href="#marker" x="${x}" y="${adjustedY}" class="marker ${cssClass}">
                    <title>${label}</title>
                </use>
            `;
        } else {
            // Render a rect with 4 input values:
            const x = (rawX - xMin) * unitToPosX + rawWidth / 2;
            const w = rawWidth * unitToPosX;
            const centerX = x + w / 2;
            const top = y;
            const height = trackHeight - trackMargin;
            const bottom = top + height;
            return `
            <g class="percentile ${cssClass}">
                <rect x="${x}" y="${top}" width="${w}" height="${height}">
                    <title>${label}</title>
                </rect>
                <line x1="${x}" x2="${x}" y1="${top}" y2="${bottom}" />
                <line
                    x1="${centerX}" x2="${centerX}"
                    y1="${top}"     y2="${bottom}"
                    stroke-dasharray="${height / 3}" />
                <line x1="${x + w}" x2="${x + w}" y1="${top}" y2="${bottom}" />
            </g>
        `;
        }
    }
}
