/** simple rectangle without rotation */
export interface Rectangle {
    x: number;
    y: number;
    width: number;
    height: number;
}

/** The workflow editor transform, containing the current pan and zoom values. */
export interface WorkflowTransform {
    x: number;
    y: number;
    k: number;
}

/**
 * Class compatible with rectangle interface.
 * Provides additional properties and methods specific to bounding boxes.
 * Useful to calculate the bounds of multiple rectangles,
 * using the `fitRectangle` method.
 */
export class AxisAlignedBoundingBox implements Rectangle {
    /** x coordinate of left edge */
    x = Infinity;

    /** y coordinate of upper edge */
    y = Infinity;

    /** x coordinate of right edge */
    endX = -Infinity;

    /** y coordinate of lower edge */
    endY = -Infinity;

    get width() {
        const width = this.endX - this.x;
        return width > 0 ? width : 0;
    }

    set width(value) {
        this.endX = this.x + value;
    }

    get height() {
        const height = this.endY - this.y;
        return height > 0 ? height : 0;
    }

    set height(value) {
        this.endY = this.y + value;
    }

    reset() {
        this.x = Infinity;
        this.y = Infinity;
        this.endX = -Infinity;
        this.endY = -Infinity;
    }

    /** expand bounding box to fit a rectangle */
    fitRectangle(rect: Readonly<Rectangle>) {
        if (this.x > rect.x) {
            this.x = rect.x;
        }

        if (this.y > rect.y) {
            this.y = rect.y;
        }

        if (this.endX < rect.x + rect.width) {
            this.endX = rect.x + rect.width;
        }

        if (this.endY < rect.y + rect.height) {
            this.endY = rect.y + rect.height;
        }
    }

    /** make width and height the same, maintaining the center of the bounding box */
    squareCenter() {
        if (this.width > this.height) {
            const difference = this.width - this.height;
            this.y -= difference * 0.5;
            this.endY += difference * 0.5;
        } else {
            const difference = this.height - this.width;
            this.x -= difference * 0.5;
            this.endX += difference * 0.5;
        }
    }

    /** expand bounding box in every direction */
    expand(by: number) {
        this.x -= by;
        this.y -= by;
        this.endX += by;
        this.endY += by;
    }

    /** check if a point is inside the bounding box */
    isPointInBounds(point: { x: number; y: number }) {
        if (point.x > this.x && point.y > this.y && point.x < this.endX && point.y < this.endY) {
            return true;
        } else {
            return false;
        }
    }

    /** check if other rectangle fits into this one */
    contains(other: Rectangle) {
        return (
            this.x <= other.x &&
            this.y <= other.y &&
            this.endX >= other.x + other.width &&
            this.endY >= other.y + other.height
        );
    }

    intersects(other: Rectangle) {
        return (
            this.x < other.x + other.width &&
            this.endX > other.x &&
            this.y < other.y + other.height &&
            this.endY > other.y
        );
    }

    move(position: Vector) {
        const storedWidth = this.width;
        const storedHeight = this.height;

        this.x = position[0];
        this.y = position[1];

        this.width = storedWidth;
        this.height = storedHeight;
    }

    scale(factor: number) {
        this.width *= factor;
        this.height *= factor;
    }

    transformTo(other: Rectangle): Transform {
        const scale: Vector = [this.width / other.width, this.height / other.height];
        const offset: Vector = [other.x - this.x, other.y - this.y];

        const transform = new Transform().scale(scale).translate(offset);

        return transform;
    }
}

/* Format
   [a b
    c d
    e f]
   as used by canvas: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform
*/
// prettier-ignore
export type Matrix = [
    number, number,
    number, number,
    number, number,
];

/** vector as a tuple */
export type Vector = [number, number];

/**
 * Wraps basic transform operations.
 * Each operation returns a new instance, so method calls can be chained
 * without mutating the initial transform.
 */
export class Transform {
    matrix: Matrix;

    constructor(matrix: Matrix = [1, 0, 0, 1, 0, 0]) {
        this.matrix = matrix;
    }

    /** returns a new transform with a translation vector added */
    translate(vector: Vector) {
        // prettier-ignore
        return new Transform([
            this.matrix[0], this.matrix[1],
            this.matrix[2], this.matrix[3],
            this.matrix[4] + vector[0], this.matrix[5] + vector[1]
        ]);
    }

    /** returns a new transform scaled by a given vector */
    scale(vector: Vector) {
        // prettier-ignore
        return new Transform([
            this.matrix[0] * vector[0], this.matrix[1] * vector[1],
            this.matrix[2] * vector[0], this.matrix[3] * vector[1],
            this.matrix[4], this.matrix[5]
        ]);
    }

    /** Returns the inverse vector. Can be used to un-transform things */
    inverse() {
        const m = this.matrix;
        // https://www.wolframalpha.com/input?i=Inverse+%5B%7B%7Ba%2Cc%2Ce%7D%2C%7Bb%2Cd%2Cf%7D%2C%7B0%2C0%2C1%7D%7D%5D
        const denominator = m[0] * m[3] - m[1] * m[2];

        const a = m[3] / denominator;
        const b = m[1] / -denominator;
        const c = m[2] / -denominator;
        const d = m[0] / denominator;
        const e = (m[3] * m[4] - m[2] * m[5]) / -denominator;
        const f = (m[1] * m[4] - m[0] * m[5]) / denominator;

        // prettier-ignore
        return new Transform([
            a, b,
            c, d,
            e, f
        ]);
    }

    /** applies this transform to a rendering context */
    applyToContext(ctx: CanvasRenderingContext2D): void {
        ctx.transform(...this.matrix);
    }

    /** returns a vector transformed by this transform */
    apply(vector: Vector): Vector {
        return [
            this.matrix[0] * vector[0] + this.matrix[2] * vector[1] + this.matrix[4],
            this.matrix[1] * vector[0] + this.matrix[3] * vector[1] + this.matrix[5],
        ];
    }

    /** removes the translation portion of the transform */
    resetTranslation(): Transform {
        // prettier-ignore
        return new Transform ([
            this.matrix[0], this.matrix[1],
            this.matrix[2], this.matrix[3],
            0, 0
        ]);
    }

    get scaleX() {
        return this.matrix[0];
    }

    get scaleY() {
        return this.matrix[3];
    }

    get offsetX() {
        return this.matrix[4];
    }

    get offsetY() {
        return this.matrix[5];
    }
}

/** returns a vector constructed of both vectors smaller coordinates */
export function vecMin(a: Vector, b: Vector): Vector {
    return [Math.min(a[0], b[0]), Math.min(a[1], b[1])];
}

/** returns a vector constructed of both vectors larger coordinates */
export function vecMax(a: Vector, b: Vector): Vector {
    return [Math.max(a[0], b[0]), Math.max(a[1], b[1])];
}

/** returns a vector snapped to the specified distance */
export function vecSnap(a: Vector, snap: number): Vector {
    return [Math.round(a[0] / snap) * snap, Math.round(a[1] / snap) * snap];
}

/** subtracts vector b from vector a */
export function vecSubtract(a: Vector, b: Vector): Vector {
    return [a[0] - b[0], a[1] - b[1]];
}

export function vecAdd(a: Vector, b: Vector): Vector {
    return [a[0] + b[0], a[1] + b[1]];
}

export function vecReduceFigures(a: Vector, significantFigures = 1): Vector {
    const factor = Math.pow(10, significantFigures);

    return [Math.round(a[0] * factor) / factor, Math.round(a[1] * factor) / factor];
}

export function rectCenterPoint(rect: Rectangle): Vector {
    return [rect.x + rect.width / 2, rect.y + rect.height / 2];
}

export function rectDistance(a: Rectangle, b: Rectangle): number {
    const vecA = rectCenterPoint(a);
    const vecB = rectCenterPoint(b);
    const dx = vecA[0] - vecB[0];
    const dy = vecA[1] - vecB[1];

    return Math.hypot(dx, dy);
}
