// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

function niceNum(value: number, round: boolean): number {
    const exp = Math.floor(Math.log10(value));
    const frac = value / Math.pow(10, exp);
    let nice: number;
    if (round) {
        if (frac < 1.5) {
            nice = 1;
        } else if (frac < 3) {
            nice = 2;
        } else if (frac < 7) {
            nice = 5;
        } else {
            nice = 10;
        }
    } else {
        if (frac <= 1) {
            nice = 1;
        } else if (frac <= 2) {
            nice = 2;
        } else if (frac <= 5) {
            nice = 5;
        } else {
            nice = 10;
        }
    }

    return nice * Math.pow(10, exp);
}

/**
 * Generate an array of "nice" tick values spanning [min, max].
 * @param min - Domain minimum
 * @param max - Domain maximum
 * @param targetCount - Desired number of ticks (approximate)
 */
export function computeNiceTicks(
    min: number,
    max: number,
    targetCount: number,
): number[] {
    if (targetCount < 1) {
        targetCount = 1;
    }

    const range = niceNum(max - min, false);
    const step = niceNum(range / targetCount, true);
    const tickMin = Math.ceil(min / step) * step;
    const tickMax = Math.floor(max / step) * step;

    const ticks: number[] = [];

    // Use epsilon to avoid floating point overshoot
    for (let t = tickMin; t <= tickMax + step * 0.001; t += step) {
        ticks.push(t);
    }

    return ticks;
}

/**
 * Format a numeric tick value for display.
 * Uses K/M/B suffixes for large numbers, fixed decimals for small.
 */
export function formatTickValue(val: number): string {
    const abs = Math.abs(val);
    if (abs === 0) {
        return "0";
    }

    if (abs >= 1e9) {
        return (val / 1e9).toFixed(1) + "B";
    }

    if (abs >= 1e6) {
        return (val / 1e6).toFixed(1) + "M";
    }

    if (abs >= 1e3) {
        return (val / 1e3).toFixed(1) + "K";
    }

    if (Number.isInteger(val)) {
        return val.toString();
    }

    if (abs >= 1) {
        return val.toFixed(1);
    }

    return val.toFixed(2);
}

/**
 * Format a timestamp (ms since epoch) as a human-readable date/time label.
 * Adapts precision based on the tick spacing.
 */
export function formatDateTickValue(val: number, stepMs?: number): string {
    const d = new Date(val);
    if (isNaN(d.getTime())) {
        return formatTickValue(val);
    }

    // If step is provided, choose precision based on tick interval
    if (stepMs !== undefined && stepMs > 0) {
        const DAY = 86_400_000;
        const HOUR = 3_600_000;
        const MINUTE = 60_000;

        if (stepMs >= DAY * 28) {
            // Monthly or longer — show year-month
            return d.toLocaleDateString(undefined, {
                year: "numeric",
                month: "short",
            });
        }

        if (stepMs >= DAY) {
            // Daily — show month and day
            return d.toLocaleDateString(undefined, {
                month: "short",
                day: "numeric",
            });
        }

        if (stepMs >= HOUR) {
            // Hourly
            return d.toLocaleString(undefined, {
                month: "short",
                day: "numeric",
                hour: "numeric",
            });
        }

        if (stepMs >= MINUTE) {
            // Minutes
            return d.toLocaleTimeString(undefined, {
                hour: "numeric",
                minute: "2-digit",
            });
        }

        // Sub-minute
        return d.toLocaleTimeString(undefined, {
            hour: "numeric",
            minute: "2-digit",
            second: "2-digit",
        });
    }

    // Default: show date only
    return d.toLocaleDateString(undefined, {
        year: "numeric",
        month: "short",
        day: "numeric",
    });
}
