// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import type { TreemapChart } from "./treemap";
import { NULL_NODE } from "../common/node-store";
import { PADDING_LABEL } from "./treemap-layout";
import {
    renderTreemapFrame,
    renderTreemapChromeOverlay,
} from "./treemap-render";
import {
    buildTreeTooltipLines,
    dismissTreePinnedTooltip,
    emitTreeNodeEvent,
    showTreePinnedTooltip,
    treeDrillTo,
} from "../common/tree-interact";

interface HitResult {
    leafId: number;
    branchId: number;
    inHeader: boolean;
}

/**
 * Find the smallest leaf AND deepest branch at `(mx, my)`. Walks the
 * (already LOD-filtered) `_visibleNodeIds` — at 2M total nodes this is
 * still a small linear scan because LOD keeps visible count bounded.
 *
 * In faceted mode `chart._visibleRootIds[i]` names the drill root that
 * owns node `i`, so the "skip the root itself" check works regardless
 * of which facet the node belongs to.
 */
function hitTest(chart: TreemapChart, mx: number, my: number): HitResult {
    const store = chart._nodeStore;
    const x0 = store.x0;
    const y0 = store.y0;
    const x1 = store.x1;
    const y1 = store.y1;
    const depth = store.depth;
    const firstChild = store.firstChild;
    const ids = chart._visibleNodeIds;
    const n = chart._visibleNodeCount;
    const baseArr = chart._visibleBaseDepths;
    const rootArr = chart._visibleRootIds;

    let bestLeafId = NULL_NODE;
    let bestLeafArea = Infinity;
    let bestBranchId = NULL_NODE;
    let bestBranchArea = Infinity;
    let labelBranchId = NULL_NODE;

    if (!ids) {
        return { leafId: NULL_NODE, branchId: NULL_NODE, inHeader: false };
    }

    for (let i = 0; i < n; i++) {
        const id = ids[i];
        const rootId = rootArr ? rootArr[i] : chart._currentRootId;
        if (id === rootId) {
            continue;
        }

        if (!(mx >= x0[id] && mx <= x1[id] && my >= y0[id] && my <= y1[id])) {
            continue;
        }

        const area = (x1[id] - x0[id]) * (y1[id] - y0[id]);
        if (firstChild[id] !== NULL_NODE) {
            if (area < bestBranchArea) {
                bestBranchArea = area;
                bestBranchId = id;
            }

            const baseDepth = baseArr
                ? baseArr[i]
                : depth[chart._currentRootId];
            const relDepth = depth[id] - baseDepth;
            if (relDepth === 1 && my <= y0[id] + PADDING_LABEL) {
                labelBranchId = id;
            }

            if (relDepth === 2) {
                const nw = x1[id] - x0[id];
                const nh = y1[id] - y0[id];
                if (nw >= 60 && nh >= 30) {
                    const cy = y0[id] + nh / 2;
                    const cx = x0[id] + nw / 2;
                    if (
                        Math.abs(my - cy) < 10 &&
                        Math.abs(mx - cx) < nw * 0.4
                    ) {
                        labelBranchId = id;
                    }
                }
            }
        } else {
            if (area < bestLeafArea) {
                bestLeafArea = area;
                bestLeafId = id;
            }
        }
    }

    if (labelBranchId !== NULL_NODE) {
        return { leafId: NULL_NODE, branchId: labelBranchId, inHeader: true };
    }

    return {
        leafId: bestLeafId,
        branchId: bestBranchId,
        inHeader: false,
    };
}

export function handleTreemapHover(
    chart: TreemapChart,
    mx: number,
    my: number,
): void {
    if (chart._pinnedNodeId !== NULL_NODE) {
        return;
    }

    for (const region of chart._breadcrumbRegions) {
        if (
            mx >= region.x0 &&
            mx <= region.x1 &&
            my >= region.y0 &&
            my <= region.y1
        ) {
            chart._tooltip.setCursor("pointer");
            chart._hoveredNodeId = NULL_NODE;
            renderTreemapChromeOverlay(chart);
            return;
        }
    }

    const { leafId, branchId, inHeader } = hitTest(chart, mx, my);
    const best = inHeader ? branchId : leafId !== NULL_NODE ? leafId : branchId;

    if (best !== chart._hoveredNodeId) {
        chart._hoveredNodeId = best;
        chart._tooltip.setCursor(
            branchId !== NULL_NODE ? "pointer" : "default",
        );
        if (best !== NULL_NODE) {
            // Kick off the lazy tooltip build for hover; re-render
            // the chrome overlay once lines resolve. Stale results
            // (mouse moved elsewhere, new view) are dropped by the
            // controller's serial gate.
            const serial = chart._lazyTooltip.beginHover(best);
            buildTreeTooltipLines(chart, best).then((lines) => {
                if (chart._lazyTooltip.commitHover(serial, lines)) {
                    renderTreemapChromeOverlay(chart);
                }
            });
        } else {
            chart._lazyTooltip.clearHover();
        }

        renderTreemapChromeOverlay(chart);
    }
}

export function handleTreemapClick(
    chart: TreemapChart,
    mx: number,
    my: number,
): void {
    if (chart._pinnedNodeId !== NULL_NODE) {
        dismissTreemapPinnedTooltip(chart);
        chart.emitUnselect();
        return;
    }

    for (const region of chart._breadcrumbRegions) {
        if (
            mx >= region.x0 &&
            mx <= region.x1 &&
            my >= region.y0 &&
            my <= region.y1
        ) {
            if (region.nodeId !== chart._currentRootId) {
                drillTo(chart, region.nodeId);
                // Breadcrumb is chrome — no `perspective-click`. The
                // drill-up pops one or more levels off the host's
                // cached filter stack via `selected: false`.
                chart.emitUnselect();
            }

            return;
        }
    }

    const { leafId, branchId, inHeader } = hitTest(chart, mx, my);

    if (branchId !== NULL_NODE && inHeader) {
        drillTo(chart, branchId);
        void emitTreeNodeEvent(chart, branchId, "branch");
    } else if (leafId !== NULL_NODE) {
        showTreemapPinnedTooltip(chart, leafId);
        void emitTreeNodeEvent(chart, leafId, "leaf");
    } else if (branchId !== NULL_NODE) {
        drillTo(chart, branchId);
        void emitTreeNodeEvent(chart, branchId, "branch");
    }
}

export function handleTreemapDblClick(
    chart: TreemapChart,
    mx: number,
    my: number,
): void {
    const wasPinned = chart._pinnedNodeId !== NULL_NODE;
    dismissTreemapPinnedTooltip(chart);
    if (wasPinned) {
        chart.emitUnselect();
    }

    const { leafId, branchId } = hitTest(chart, mx, my);
    const store = chart._nodeStore;
    let target = branchId;
    if (target === NULL_NODE && leafId !== NULL_NODE) {
        const parent = store.parent[leafId];
        if (parent !== chart._currentRootId && parent !== NULL_NODE) {
            target = parent;
        }
    }

    if (
        target !== NULL_NODE &&
        target !== chart._currentRootId &&
        store.firstChild[target] !== NULL_NODE
    ) {
        drillTo(chart, target);
        void emitTreeNodeEvent(chart, target, "branch");
        if (leafId !== NULL_NODE && store.firstChild[leafId] === NULL_NODE) {
            showTreemapPinnedTooltip(chart, leafId);
            void emitTreeNodeEvent(chart, leafId, "leaf");
        }
    }
}

function drillTo(chart: TreemapChart, nodeId: number): void {
    treeDrillTo(chart, nodeId, () => {
        if (chart._glManager) {
            renderTreemapFrame(chart, chart._glManager);
        }
    });
}

export function showTreemapPinnedTooltip(
    chart: TreemapChart,
    nodeId: number,
): void {
    const store = chart._nodeStore;
    const cx = (store.x0[nodeId] + store.x1[nodeId]) / 2;
    const cy = (store.y0[nodeId] + store.y1[nodeId]) / 2;
    showTreePinnedTooltip(chart, nodeId, { cx, cy }, () =>
        renderTreemapChromeOverlay(chart),
    );
}

export function dismissTreemapPinnedTooltip(chart: TreemapChart): void {
    dismissTreePinnedTooltip(chart);
}

export { buildTreeTooltipLines as buildTreemapTooltipLines } from "../common/tree-interact";
