import { globalConfig } from "./config";

const tileSize = globalConfig.tileSize;
const halfTileSize = globalConfig.halfTileSize;

/**
 * @enum {string}
 */
export const enumDirection = {
    top: "top",
    right: "right",
    bottom: "bottom",
    left: "left",
};

/**
 * @enum {string}
 */
export const enumInvertedDirections = {
    [enumDirection.top]: enumDirection.bottom,
    [enumDirection.right]: enumDirection.left,
    [enumDirection.bottom]: enumDirection.top,
    [enumDirection.left]: enumDirection.right,
};

/**
 * @enum {number}
 */
export const enumDirectionToAngle = {
    [enumDirection.top]: 0,
    [enumDirection.right]: 90,
    [enumDirection.bottom]: 180,
    [enumDirection.left]: 270,
};

/**
 * @enum {enumDirection}
 */
export const enumAngleToDirection = {
    0: enumDirection.top,
    90: enumDirection.right,
    180: enumDirection.bottom,
    270: enumDirection.left,
};

export class Vector {
    /**
     *
     * @param {number=} x
     * @param {number=} y
     */
    constructor(x, y) {
        this.x = x || 0;
        this.y = y || 0;
    }

    /**
     * return a copy of the vector
     * @returns {Vector}
     */
    copy() {
        return new Vector(this.x, this.y);
    }

    /**
     * Adds a vector and return a new vector
     * @param {Vector} other
     * @returns {Vector}
     */
    add(other) {
        return new Vector(this.x + other.x, this.y + other.y);
    }

    /**
     * Adds a vector
     * @param {Vector} other
     * @returns {Vector}
     */
    addInplace(other) {
        this.x += other.x;
        this.y += other.y;
        return this;
    }

    /**
     * Substracts a vector and return a new vector
     * @param {Vector} other
     * @returns {Vector}
     */
    sub(other) {
        return new Vector(this.x - other.x, this.y - other.y);
    }

    /**
     * Subs a vector
     * @param {Vector} other
     * @returns {Vector}
     */
    subInplace(other) {
        this.x -= other.x;
        this.y -= other.y;
        return this;
    }

    /**
     * Multiplies with a vector and return a new vector
     * @param {Vector} other
     * @returns {Vector}
     */
    mul(other) {
        return new Vector(this.x * other.x, this.y * other.y);
    }

    /**
     * Adds two scalars and return a new vector
     * @param {number} x
     * @param {number} y
     * @returns {Vector}
     */
    addScalars(x, y) {
        return new Vector(this.x + x, this.y + y);
    }

    /**
     * Substracts a scalar and return a new vector
     * @param {number} f
     * @returns {Vector}
     */
    subScalar(f) {
        return new Vector(this.x - f, this.y - f);
    }

    /**
     * Substracts two scalars and return a new vector
     * @param {number} x
     * @param {number} y
     * @returns {Vector}
     */
    subScalars(x, y) {
        return new Vector(this.x - x, this.y - y);
    }

    /**
     * Returns the euclidian length
     * @returns {number}
     */
    length() {
        return Math.hypot(this.x, this.y);
    }

    /**
     * Returns the square length
     * @returns {number}
     */
    lengthSquare() {
        return this.x * this.x + this.y * this.y;
    }

    /**
     * Divides both components by a scalar and return a new vector
     * @param {number} f
     * @returns {Vector}
     */
    divideScalar(f) {
        return new Vector(this.x / f, this.y / f);
    }

    /**
     * Divides both components by the given scalars and return a new vector
     * @param {number} a
     * @param {number} b
     * @returns {Vector}
     */
    divideScalars(a, b) {
        return new Vector(this.x / a, this.y / b);
    }

    /**
     * Divides both components by a scalar
     * @param {number} f
     * @returns {Vector}
     */
    divideScalarInplace(f) {
        this.x /= f;
        this.y /= f;
        return this;
    }

    /**
     * Multiplies both components with a scalar and return a new vector
     * @param {number} f
     * @returns {Vector}
     */
    multiplyScalar(f) {
        return new Vector(this.x * f, this.y * f);
    }

    /**
     * Multiplies both components with two scalars and returns a new vector
     * @param {number} a
     * @param {number} b
     * @returns {Vector}
     */
    multiplyScalars(a, b) {
        return new Vector(this.x * a, this.y * b);
    }

    /**
     * For both components, compute the maximum of each component and the given scalar, and return a new vector.
     * For example:
     *   - new Vector(-1, 5).maxScalar(0) -> Vector(0, 5)
     * @param {number} f
     * @returns {Vector}
     */
    maxScalar(f) {
        return new Vector(Math.max(f, this.x), Math.max(f, this.y));
    }

    /**
     * Adds a scalar to both components and return a new vector
     * @param {number} f
     * @returns {Vector}
     */
    addScalar(f) {
        return new Vector(this.x + f, this.y + f);
    }

    /**
     * Computes the component wise minimum and return a new vector
     * @param {Vector} v
     * @returns {Vector}
     */
    min(v) {
        return new Vector(Math.min(v.x, this.x), Math.min(v.y, this.y));
    }

    /**
     * Computes the component wise maximum and return a new vector
     * @param {Vector} v
     * @returns {Vector}
     */
    max(v) {
        return new Vector(Math.max(v.x, this.x), Math.max(v.y, this.y));
    }
    /**
     * Computes the component wise absolute
     * @returns {Vector}
     */
    abs() {
        return new Vector(Math.abs(this.x), Math.abs(this.y));
    }

    /**
     * Computes the scalar product
     * @param {Vector} v
     * @returns {number}
     */
    dot(v) {
        return this.x * v.x + this.y * v.y;
    }

    /**
     * Computes the distance to a given vector
     * @param {Vector} v
     * @returns {number}
     */
    distance(v) {
        return Math.hypot(this.x - v.x, this.y - v.y);
    }

    /**
     * Computes the square distance to a given vectort
     * @param {Vector} v
     * @returns {number}
     */
    distanceSquare(v) {
        const dx = this.x - v.x;
        const dy = this.y - v.y;
        return dx * dx + dy * dy;
    }

    /**
     * Computes and returns the center between both points
     * @param {Vector} v
     * @returns {Vector}
     */
    centerPoint(v) {
        const cx = this.x + v.x;
        const cy = this.y + v.y;
        return new Vector(cx / 2, cy / 2);
    }

    /**
     * Computes componentwise floor and returns a new vector
     * @returns {Vector}
     */
    floor() {
        return new Vector(Math.floor(this.x), Math.floor(this.y));
    }

    /**
     * Computes componentwise ceil and returns a new vector
     * @returns {Vector}
     */
    ceil() {
        return new Vector(Math.ceil(this.x), Math.ceil(this.y));
    }

    /**
     * Computes componentwise round and return a new vector
     * @returns {Vector}
     */
    round() {
        return new Vector(Math.round(this.x), Math.round(this.y));
    }

    /**
     * Converts this vector from world to tile space and return a new vector
     * @returns {Vector}
     */
    toTileSpace() {
        return new Vector(Math.floor(this.x / tileSize), Math.floor(this.y / tileSize));
    }

    /**
     * Converts this vector from world to street space and return a new vector
     * @returns {Vector}
     */
    toStreetSpace() {
        return new Vector(Math.floor(this.x / halfTileSize + 0.25), Math.floor(this.y / halfTileSize + 0.25));
    }

    /**
     * Converts this vector to world space and return a new vector
     * @returns {Vector}
     */
    toWorldSpace() {
        return new Vector(this.x * tileSize, this.y * tileSize);
    }

    /**
     * Converts this vector to world space and return a new vector
     * @returns {Vector}
     */
    toWorldSpaceCenterOfTile() {
        return new Vector(this.x * tileSize + halfTileSize, this.y * tileSize + halfTileSize);
    }

    /**
     * Converts the top left tile position of this vector
     * @returns {Vector}
     */
    snapWorldToTile() {
        return new Vector(Math.floor(this.x / tileSize) * tileSize, Math.floor(this.y / tileSize) * tileSize);
    }

    /**
     * Normalizes the vector, dividing by the length(), and return a new vector
     * @returns {Vector}
     */
    normalize() {
        const len = Math.max(1e-5, Math.hypot(this.x, this.y));
        return new Vector(this.x / len, this.y / len);
    }

    /**
     * Normalizes the vector, dividing by the length(), and return a new vector
     * @returns {Vector}
     */
    normalizeIfGreaterOne() {
        const len = Math.max(1, Math.hypot(this.x, this.y));
        return new Vector(this.x / len, this.y / len);
    }

    /**
     * Returns the normalized vector to the other point
     * @param {Vector} v
     * @returns {Vector}
     */
    normalizedDirection(v) {
        const dx = v.x - this.x;
        const dy = v.y - this.y;
        const len = Math.max(1e-5, Math.hypot(dx, dy));
        return new Vector(dx / len, dy / len);
    }

    /**
     * Returns a perpendicular vector
     * @returns {Vector}
     */
    findPerpendicular() {
        return new Vector(-this.y, this.x);
    }

    /**
     * Returns the unnormalized direction to the other point
     * @param {Vector} v
     * @returns {Vector}
     */
    direction(v) {
        return new Vector(v.x - this.x, v.y - this.y);
    }

    /**
     * Returns a string representation of the vector
     * @returns {string}
     */
    toString() {
        return this.x + "," + this.y;
    }

    /**
     * Compares both vectors for exact equality. Does not do an epsilon compare
     * @param {Vector} v
     * @returns {Boolean}
     */
    equals(v) {
        return this.x === v.x && this.y === v.y;
    }

    /**
     * Rotates this vector
     * @param {number} angle
     * @returns {Vector} new vector
     */
    rotated(angle) {
        const sin = Math.sin(angle);
        const cos = Math.cos(angle);
        return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
    }

    /**
     * Rotates this vector
     * @param {number} angle
     * @returns {Vector} this vector
     */
    rotateInplaceFastMultipleOf90(angle) {
        // const sin = Math.sin(angle);
        // const cos = Math.cos(angle);
        // let sin = 0, cos = 1;
        assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);

        switch (angle) {
            case 0:
            case 360: {
                return this;
            }
            case 90: {
                // sin = 1;
                // cos = 0;

                const x = this.x;
                this.x = -this.y;
                this.y = x;
                return this;
            }
            case 180: {
                // sin = 0
                // cos = -1
                this.x = -this.x;
                this.y = -this.y;
                return this;
            }
            case 270: {
                // sin = -1
                // cos = 0
                const x = this.x;
                this.x = this.y;
                this.y = -x;
                return this;
            }
            default: {
                assertAlways(false, "Invalid fast inplace rotation: " + angle);
                return this;
            }
        }
        // return new Vector(this.x * cos - this.y * sin, this.x * sin + this.y * cos);
    }

    /**
     * Rotates this vector
     * @param {number} angle
     * @returns {Vector} new vector
     */
    rotateFastMultipleOf90(angle) {
        assert(angle >= 0 && angle <= 360, "Invalid angle, please clamp first: " + angle);

        switch (angle) {
            case 360:
            case 0: {
                return new Vector(this.x, this.y);
            }
            case 90: {
                return new Vector(-this.y, this.x);
            }
            case 180: {
                return new Vector(-this.x, -this.y);
            }
            case 270: {
                return new Vector(this.y, -this.x);
            }
            default: {
                assertAlways(false, "Invalid fast inplace rotation: " + angle);
                return new Vector();
            }
        }
    }

    /**
     * Helper method to rotate a direction
     * @param {enumDirection} direction
     * @param {number} angle
     * @returns {enumDirection}
     */
    static transformDirectionFromMultipleOf90(direction, angle) {
        if (angle === 0 || angle === 360) {
            return direction;
        }
        assert(angle >= 0 && angle <= 360, "Invalid angle: " + angle);
        switch (direction) {
            case enumDirection.top: {
                switch (angle) {
                    case 90:
                        return enumDirection.right;
                    case 180:
                        return enumDirection.bottom;
                    case 270:
                        return enumDirection.left;
                    default:
                        assertAlways(false, "Invalid angle: " + angle);
                        return;
                }
            }

            case enumDirection.right: {
                switch (angle) {
                    case 90:
                        return enumDirection.bottom;
                    case 180:
                        return enumDirection.left;
                    case 270:
                        return enumDirection.top;
                    default:
                        assertAlways(false, "Invalid angle: " + angle);
                        return;
                }
            }

            case enumDirection.bottom: {
                switch (angle) {
                    case 90:
                        return enumDirection.left;
                    case 180:
                        return enumDirection.top;
                    case 270:
                        return enumDirection.right;
                    default:
                        assertAlways(false, "Invalid angle: " + angle);
                        return;
                }
            }

            case enumDirection.left: {
                switch (angle) {
                    case 90:
                        return enumDirection.top;
                    case 180:
                        return enumDirection.right;
                    case 270:
                        return enumDirection.bottom;
                    default:
                        assertAlways(false, "Invalid angle: " + angle);
                        return;
                }
            }
            default:
                assertAlways(false, "Invalid angle: " + angle);
                return;
        }
    }

    /**
     * Compares both vectors for epsilon equality
     * @param {Vector} v
     * @returns {Boolean}
     */
    equalsEpsilon(v, epsilon = 1e-5) {
        return Math.abs(this.x - v.x) < 1e-5 && Math.abs(this.y - v.y) < epsilon;
    }

    /**
     * Returns the angle
     * @returns {number} 0 .. 2 PI
     */
    angle() {
        return Math.atan2(this.y, this.x) + Math.PI / 2;
    }

    /**
     * Serializes the vector to a string
     * @returns {string}
     */
    serializeTile() {
        return String.fromCharCode(33 + this.x) + String.fromCharCode(33 + this.y);
    }

    /**
     * Creates a simple representation of the vector
     */
    serializeSimple() {
        return { x: this.x, y: this.y };
    }

    /**
     * @returns {number}
     */
    serializeTileToInt() {
        return this.x + this.y * 256;
    }

    /**
     *
     * @param {number} i
     * @returns {Vector}
     */
    static deserializeTileFromInt(i) {
        const x = i % 256;
        const y = Math.floor(i / 256);
        return new Vector(x, y);
    }

    /**
     * Deserializes a vector from a string
     * @param {string} s
     * @returns {Vector}
     */
    static deserializeTile(s) {
        return new Vector(s.charCodeAt(0) - 33, s.charCodeAt(1) - 33);
    }

    /**
     * Deserializes a vector from a serialized json object
     * @param {object} obj
     * @returns {Vector}
     */
    static fromSerializedObject(obj) {
        if (obj) {
            return new Vector(obj.x || 0, obj.y || 0);
        }
    }
}

/**
 * Interpolates two vectors, for a = 0, returns v1 and for a = 1 return v2, otherwise interpolate
 * @param {Vector} v1
 * @param {Vector} v2
 * @param {number} a
 */
export function mixVector(v1, v2, a) {
    return new Vector(v1.x * (1 - a) + v2.x * a, v1.y * (1 - a) + v2.y * a);
}

/**
 * Mapping from string direction to actual vector
 * @enum {Vector}
 */
export const enumDirectionToVector = {
    top: new Vector(0, -1),
    right: new Vector(1, 0),
    bottom: new Vector(0, 1),
    left: new Vector(-1, 0),
};
