export interface Point {
    x: number;
    y: number;
}

export interface Size {
    width: number;
    height: number;
}

export interface Rect extends Point, Size {}

export interface Frame {
    top: number;
    left: number;
    width: number;
    height: number;
}

export interface Placement {
    top?: number;
    left?: number;
    right?: number;
    bottom?: number;
    width?: number;
    height?: number;
    minWidth?: number;
    maxWidth?: number;
    minHeight?: number;
    maxHeight?: number;
}

export interface SizeConstraints {
    maxWidth?: number;
    maxHeight?: number;
}

export function zeroSize(): Size {
    return { width: 0, height: 0 };
}
export function zeroPoint(): Point {
    return { x: 0, y: 0 };
}

export function zeroFrame(): Frame {
    return {
        top: 0,
        left: 0,
        width: 0,
        height: 0,
    };
}

export function zeroRect(): Rect {
    return {
        ...zeroPoint(),
        ...zeroSize(),
    };
}

export function constrainedSize(size: Size, cons: SizeConstraints): Size {
    let width = size.width;
    let height = size.height;

    if (cons.maxHeight !== undefined && height > cons.maxHeight) {
        let factor = cons.maxHeight / height;
        height = cons.maxHeight;
        width = Math.floor(width * factor);
        // console.log({width, height});
    }
    if (cons.maxWidth !== undefined && width > cons.maxWidth) {
        let factor = cons.maxWidth / width;
        width = cons.maxWidth;
        height = Math.floor(height * factor);
        // console.log({width, height});
    }
    return { width, height };
}

export function proportionalZoomDelta(size: Size, zoomDelta: Point): Point {
    const aspectRatio = size.width / size.height;
    let result = structuredClone(zoomDelta)
    if (zoomDelta.x >= 0 && zoomDelta.y >= 0) {
        if (zoomDelta.x > zoomDelta.y) {
            result.y = result.x * aspectRatio
        } else {
            result.x = result.y * aspectRatio
        }
    } else {
        if (zoomDelta.y > zoomDelta.x) {
            result.y = result.x * aspectRatio
        } else {
            result.x = result.y * aspectRatio
        }
    }
    return result
}

export function getWindowSize(): Size {
    return {
        width: document.body.clientWidth,
        height: document.body.clientHeight,
    };
}

export function constrainedFrame(position: Point, size: Size, windowSize: Size): Frame {
    if (position.x + size.width > windowSize.width) {
        position.x = windowSize.width - size.width;
        if (position.x < 0) {
            position.x = 0;
        }
    }
    if (position.y + size.height > windowSize.height) {
        position.y = windowSize.height - size.height;
        if (position.y < 0) {
            position.y = 0;
        }
    }
    let frame: Frame = {
        top: position.y,
        left: position.x,
        width: size.width,
        height: size.height,
    };
    return frame;
}

export function pointMinus(p0: Point, p1: Point): Point {
    return {
        x: p0.x - p1.x,
        y: p0.y - p1.y,
    };
}

export function pointPlus(p0: Point, p1: Point): Point {
    return {
        x: p1.x + p0.x,
        y: p1.y + p0.y,
    };
}

export type Line = [Point, Point];

export function vectorLength(vec: Point): number {
    return Math.sqrt(vec.x * vec.x + vec.y * vec.y);
}

export function pointDist(pt0: Point, pt1: Point): number {
    return vectorLength(pointMinus(pt1, pt0))
}

export function vectorResize(vec: Point, newLen: number): Point {
    let len = Math.sqrt(vec.x * vec.x + vec.y * vec.y);
    let factor = newLen / len
    return {
        x: vec.x * factor,
        y: vec.y * factor,
    }
}

export function pointLerp(a: Point, b: Point, t: number): Point {
    const x = a.x + (b.x - a.x) * t;
    const y = a.y + (b.y - a.y) * t;
    return { x, y };
}

export function pointMiddle(a: Point, b: Point): Point {
    return {
        x: (a.x + b.x) / 2,
        y: (a.y + b.y) / 2,
    }
}

export function lineLength(line: Line): number {
    return pointDist(...line)
}

export function normalVector(pt0: Point, pt1: Point): Point {
    let vec = pointMinus(pt1, pt0);
    let normal: Point = { x: -vec.y, y: vec.x }; // surprisingly simple but correct!
    let length = vectorLength(normal);
    if (length === 0) {
        return zeroPoint();
    }
    normal.x /= length;
    normal.y /= length;
    return normal;
}

export function parallelTransition(line: Line, distance: number): Line {
    let [pt0, pt1] = line;
    let normal = normalVector(pt0, pt1);
    normal.x *= distance;
    normal.y *= distance;
    return [pointPlus(pt0, normal), pointPlus(pt1, normal)];
}

// generated by ChatGPT
export function distanceFromPointToLine(p: Point, line: Line): number {
    const [a, b] = line;
    const lineLength = Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2);
    if (lineLength === 0) return Math.sqrt((p.x - a.x) ** 2 + (p.y - a.y) ** 2);
    const t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / (lineLength ** 2);
    const closestPoint = t < 0 ? a : t > 1 ? b : { x: a.x + t * (b.x - a.x), y: a.y + t * (b.y - a.y) };
    return Math.sqrt((p.x - closestPoint.x) ** 2 + (p.y - closestPoint.y) ** 2);
}

// Interpolates or extrapolates a point on a line defined by a segment, based on factor t.
// Generated by ChatGPT
export function interpolateLine(line: Line, t: number): Point {
    const [pt0, pt1] = line;

    // Linearly interpolate or extrapolate the x and y coordinates
    const x = pt0.x + (pt1.x - pt0.x) * t;
    const y = pt0.y + (pt1.y - pt0.y) * t;

    return { x, y };
}

export function pointInRect(point: Point, rect: Rect) {
    return (
        point.x >= rect.x &&
        point.x <= rect.x + rect.width &&
        point.y >= rect.y &&
        point.y <= rect.y + rect.height
    );
}

// Provided by ChatGPT (including the comments!)
//
/**
 * Checks if two rectangles intersect using the Separating Axis Theorem (SAT).
 * According to SAT, two convex shapes are separated if and only if there exists an axis
 * along which the shapes do not overlap. For axis-aligned rectangles, this simplifies to
 * checking overlap along the x and y axes.
 *
 * @param rect1 - The first rectangle.
 * @param rect2 - The second rectangle.
 * @returns true if the rectangles intersect, false otherwise.
 */
export function rectsIntersect(rect1: Rect, rect2: Rect): boolean {
    return (
        rect1.x < rect2.x + rect2.width &&
        rect1.x + rect1.width > rect2.x &&
        rect1.y < rect2.y + rect2.height &&
        rect1.y + rect1.height > rect2.y
    );
}

// some math
export function pointScale(p: Point, f: number): Point {
    return {
        x: p.x * f,
        y: p.y * f,
    };
}

export function sizeScale(s: Size, f: number): Size {
    return {
        width: s.width * f,
        height: s.height * f,
    }
}

export function frameScale(r: Frame, factor: number): Frame {
    return {
        top: Math.round(r.top * factor),
        left: Math.round(r.left * factor),
        width: Math.round(r.width * factor),
        height: Math.round(r.height * factor),
    };
}

// returns negative number if point is inside rect, and positive if outside
// this is basically (almost) a signed distance function that does not respect the corner radius
export function distanceToRect(point: Point, rect: Rect): number {
    let half_width_x = rect.width / 2;
    let half_width_y = rect.height / 2;

    let center_x = rect.x + half_width_x;
    let center_y = rect.y + half_width_y;

    let dist_x = Math.abs(point.x - center_x) - half_width_x;
    let dist_y = Math.abs(point.y - center_y) - half_width_y;

    return Math.max(dist_x, dist_y);
}


// -------------------------------------------------------------------------------------------------
// Matrix 2D Transformations
// -------------------------------------------------------------------------------------------------

// Generated by Claude
export type M3 = Float32Array & { length: 9 }

export function identityM3(): M3 {
    return new Float32Array([
        1, 0, 0,
        0, 1, 0,
        0, 0, 1
    ]) as M3
}

export function asContextTransform(m: M3): number[] {
    return [
        m[0], m[1], m[3],
        m[4], m[2], m[5]
    ]
}

export type Vec2 = Float32Array & { length: 2 }

export function vec2(x: number, y: number): Vec2 {
    return new Float32Array([x, y]) as Vec2
}


export function translateM3(m: M3, vec: Vec2) {
    // Generated by Copilot
    // modify the matrix `m` so it's translated by the offset indicated by vec
    const tx = vec[0];
    const ty = vec[1];

    m[6] = m[0] * tx + m[3] * ty + m[6];
    m[7] = m[1] * tx + m[4] * ty + m[7];
    m[8] = m[2] * tx + m[5] * ty + m[8];
}

export function scaleM3(m: M3, f: number) {
    // Generated by Copilot
    // modify the matrix `m` so it's scaled by factor f
    m[0] *= f;
    m[1] *= f;
    m[3] *= f;
    m[4] *= f;
}

export function rotationM3(turns: number): M3 {
    // Generated by Copilot
    // matrix representing a rotation in turns
    const angle = turns * 2 * Math.PI; // convert turns to radians
    const cos = Math.cos(angle);
    const sin = Math.sin(angle);

    return new Float32Array([
        cos, -sin, 0,
        sin,  cos, 0,
          0,    0, 1
    ]) as M3;
}

export function translationM3(v: Vec2): M3 {
    // translation matrix from the input vector
    return new Float32Array([
        1, 0, v[0],
        0, 1, v[1],
        0, 0, 1
    ]) as M3;
}


export function matmul(m1: M3, m2: M3): M3 {
    // Generated by Copilot
    // matrix multiplication
    const result = new Float32Array(9) as M3;

    result[0] = m1[0] * m2[0] + m1[1] * m2[3] + m1[2] * m2[6];
    result[1] = m1[0] * m2[1] + m1[1] * m2[4] + m1[2] * m2[7];
    result[2] = m1[0] * m2[2] + m1[1] * m2[5] + m1[2] * m2[8];

    result[3] = m1[3] * m2[0] + m1[4] * m2[3] + m1[5] * m2[6];
    result[4] = m1[3] * m2[1] + m1[4] * m2[4] + m1[5] * m2[7];
    result[5] = m1[3] * m2[2] + m1[4] * m2[5] + m1[5] * m2[8];

    result[6] = m1[6] * m2[0] + m1[7] * m2[3] + m1[8] * m2[6];
    result[7] = m1[6] * m2[1] + m1[7] * m2[4] + m1[8] * m2[7];
    result[8] = m1[6] * m2[2] + m1[7] * m2[5] + m1[8] * m2[8];

    return result;
}

export function rotationAroundPoint(pivot: Vec2, turns: number): M3 {
    let m = translationM3(vec2(-pivot[0], -pivot[1]))
    m = matmul(m, rotationM3(turns))
    m = matmul(m, translationM3(pivot))
    return m
}

export function rotateM3(m: M3, turns: number) {
    m.set(matmul(m, rotationM3(turns)))
}

export function vec2Plus(v1: Vec2, v2: Vec2): Vec2 {
    return vec2(v1[0] + v2[0], v1[1] + v2[1]);
}

export function vec2mul(v: Vec2, f: number): Vec2 {
    return vec2(v[0] * f, v[1] * f);
}

export function vec2Transform(v: Vec2, m: M3): Vec2 {
    const x = m[0] * v[0] + m[3] * v[1] + m[6];
    const y = m[1] * v[0] + m[4] * v[1] + m[7];
    return vec2(x, y);
}
