// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { WritingDirection } from './text.mojom-webui.js';
// Percentage of image size to add to the word bounding boxes hit box.
const BOUNDING_BOX_HIT_MARGIN_PERCENT = 0.02;
/**
 * Helper function that implements hit testing for words. A hit is used to
 * determine the users intent when their mouse is not on a specific word.
 *
 * @param words The list of words to check for a hit with
 * @param target The point normalized to image coordinates to check for a hit.
 * @return The best hit, null if no matches are found.
 */
export function bestHit(words, target) {
    if (words.length === 0) {
        return null;
    }
    return bestHorizontallyExtendedHit(words, target) ||
        bestVerticalHit(words, target);
}
// Finds the closes hit when extending alal word hit boxes horizontally.
function bestHorizontallyExtendedHit(words, target) {
    const hits = [];
    for (const word of words) {
        const hit = horizontallyExtendedHit(word, target);
        if (hit) {
            hits.push(hit);
        }
    }
    return getBestHit(hits, target);
}
// Finds the best hit that is vertically close.
function bestVerticalHit(words, target) {
    const closestProjectedDistances = getShortedProjectedDistanceHits(words, target);
    return getBestHit(closestProjectedDistances, target);
}
function horizontallyExtendedHit(word, target) {
    const position = rotateCoordinate(word, target);
    const boundingBox = boundingBoxWithMargin(word);
    if (boxContainsTarget(boundingBox, position)) {
        return word;
    }
    if (isInExtension(position, boundingBox, word.writingDirection)) {
        return word;
    }
    return null;
}
// Finds the hit closest the the target.
function getBestHit(hits, target) {
    let closestHit = null;
    let closestDistance = Number.MAX_SAFE_INTEGER;
    for (const hit of hits) {
        const wordBoundingBox = hit.geometry?.boundingBox.box;
        if (!wordBoundingBox) {
            continue;
        }
        const distX = target.x - wordBoundingBox.x;
        const distY = target.y - wordBoundingBox.y;
        const totalDistance = distX * distX + distY * distY;
        if (totalDistance < closestDistance) {
            closestHit = hit;
            closestDistance = totalDistance;
        }
    }
    return closestHit;
}
// Rotates the target coordinates to be in relation to the word rotation.
function rotateCoordinate(word, target) {
    const wordBoundingBox = word.geometry?.boundingBox;
    if (!wordBoundingBox) {
        return { x: 0, y: 0 };
    }
    // Translate to origin 0,0.
    const translatedX = target.x - wordBoundingBox.box.x;
    const translatedY = target.y - wordBoundingBox.box.y;
    // Rotate clockwise around origin.
    const rotatedX = translatedX * Math.cos(wordBoundingBox.rotation) +
        translatedY * Math.sin(wordBoundingBox.rotation);
    const rotatedY = -translatedX * Math.sin(wordBoundingBox.rotation) +
        translatedY * Math.cos(wordBoundingBox.rotation);
    // Translate the coordinate back.
    const finalX = rotatedX + wordBoundingBox.box.x;
    const finalY = rotatedY + wordBoundingBox.box.y;
    return { x: finalX, y: finalY };
}
// Creates a bounding box for the word, with additional BOUNDING_BOX_HIT_MARGIN.
function boundingBoxWithMargin(word) {
    const wordBoundingBox = word.geometry?.boundingBox.box;
    if (!wordBoundingBox) {
        return { x: 0, y: 0, width: 0, height: 0 };
    }
    return {
        x: wordBoundingBox.x,
        y: wordBoundingBox.y,
        width: wordBoundingBox.width + BOUNDING_BOX_HIT_MARGIN_PERCENT,
        height: wordBoundingBox.height + BOUNDING_BOX_HIT_MARGIN_PERCENT,
    };
}
/** @return True if target is within the box. */
function boxContainsTarget(box, target) {
    const boxMinX = box.x - (box.width / 2);
    const boxMaxX = box.x + (box.width / 2);
    const boxMinY = box.y - (box.height / 2);
    const boxMaxY = box.y + (box.height / 2);
    return target.x >= boxMinX && target.x <= boxMaxX && target.y >= boxMinY &&
        target.y <= boxMaxY;
}
/**
 * Determines if the position is within the giving bounding box, if the bounding
 * box expanded horizontally infinetely.
 * @returns True if the position is within the horizontal expansion of the word.
 */
function isInExtension(position, boundingBox, writingDirection) {
    const boxLeft = boundingBox.x - (boundingBox.width / 2);
    const boxRight = boundingBox.x + (boundingBox.width / 2);
    const boxTop = boundingBox.y - (boundingBox.height / 2);
    const boxBottom = boundingBox.y + (boundingBox.height / 2);
    if (writingDirection === WritingDirection.kTopToBottom) {
        const isAboveBox = position.x > boxLeft && position.x < boxRight &&
            position.y <= boundingBox.y;
        const isBelowBox = position.x > boxLeft && position.x < boxRight &&
            position.y >= boundingBox.y;
        return isAboveBox || isBelowBox;
    }
    // If writingDirection is null, assume left to right.
    const isLeftOfBox = position.y > boxTop && position.y < boxBottom &&
        position.x <= boundingBox.x;
    const isRightOfBox = position.y > boxTop && position.y < boxBottom &&
        position.x >= boundingBox.x;
    return isLeftOfBox || isRightOfBox;
}
/**
 * Gets the hits with the shortest projected distance as defined by
 * getProjectedDistance().
 * @returns The list of hits with the shortest distance to the target.
 */
function getShortedProjectedDistanceHits(words, target) {
    let closestHits = [];
    let closestProjectedDistance = Number.MAX_SAFE_INTEGER;
    for (const word of words) {
        const projectedDistance = getProjectedDistance(word, target);
        if (!projectedDistance) {
            continue;
        }
        if (Math.abs(closestProjectedDistance - projectedDistance) <=
            BOUNDING_BOX_HIT_MARGIN_PERCENT) {
            // This porjected distance is close enough within the hit margin, so add
            // to closest hits.
            closestHits.push(word);
        }
        else if (projectedDistance <= closestProjectedDistance) {
            // Sizable new closest projected distance, so reset closest hits.
            closestHits = [word];
            closestProjectedDistance = projectedDistance;
        }
    }
    return closestHits;
}
/**
 * Calculates the projected distance from the target to the word. The projected
 * distance is the "vertical" distance from the target to the top/bottom of the
 * word. For top to bottom reading languages, the projected distance is the
 * distance from the right/left of the word.
 * @returns The projected distance from the target to the word. Null if the word
 *          does not have a bounding box.
 */
function getProjectedDistance(word, target) {
    const rotatedTarget = rotateCoordinate(word, target);
    const boundingBox = word.geometry?.boundingBox.box;
    if (!boundingBox) {
        return null;
    }
    const deltaX = Math.max(Math.abs(boundingBox.x - rotatedTarget.x) - boundingBox.width / 2, 0);
    const deltaY = Math.max(Math.abs(boundingBox.y - rotatedTarget.y) - boundingBox.height / 2, 0);
    return word.writingDirection === WritingDirection.kTopToBottom ? deltaX :
        deltaY;
}
