export enum Direction {
    NONE,
    NORTH,
    EAST,
    SOUTH,
    WEST,
}

export function isDirection(obj: any): obj is Direction {
    if (typeof obj !== 'number') return false;
    return Direction.NONE <= obj && obj <= Direction.WEST;
}

type Operand = Point | number;
type Operation = (a: number, b: number) => number;
function op(a: Operand, b: Operand, fn: Operation): Operand {
    const aArray = Array.isArray(a);
    const bArray = Array.isArray(b);
    if (aArray) {
        return (bArray ? a.map((x, i) => fn(x, b[i])) : a.map((x) => fn(x, b))) as Point;
    }
    return (bArray ? b.map((x) => fn(a, x)) : fn(a, b)) as Point;
}

const operations: Record<string, [Operation, number]> = {
    '+': [(x, y) => x + y, 1],
    '-': [(x, y) => x - y, 1],
    '*': [(x, y) => x * y, 2],
    '/': [(x, y) => x / y, 2],
    '%': [(x, y) => x / y, 2],
    '(': [() => 0, -1],
}

export function exp(strings: TemplateStringsArray, ...args: number[]): number;
export function exp(strings: TemplateStringsArray, ...args: Operand[]): Point;
export function exp(strings: TemplateStringsArray, ...args: Operand[]): Operand {
    const input: (string | Operand)[] = [];
    const output: (Operation | Operand)[] = [];
    const stack: string[] = [];

    for (let i = 0; i < args.length; i++) {
        input.push(strings[i].trim(), args[i]);
    }
    input.push(strings[args.length].trim());

    const pushOp = (op: string) => output.push(operations[op.trim()][0]);
    const opPriority = (op: string) => operations[op.trim()][1];

    for (const item of input) {
        if (typeof item === 'string') {
            for (const char of item) {
                if (char.match(/[ \n\r\t]/)) {
                    continue;
                } else if (char === '(') {
                    stack.push(char);
                } else if (char === ')') {
                    while (true) {
                        const item = stack.pop();
                        if (!item || item === '(') break;
                        pushOp(item);
                    }
                } else {
                    const p = opPriority(char);
                    while (true) {
                        const item = stack[stack.length - 1];
                        if (!item || opPriority(item) < p) break;
                        stack.pop();
                        pushOp(item);
                    }
                    stack.push(char);
                }
            }
        } else {
            output.push(item);
        }
    }

    while (true) {
        const item = stack.pop();
        if (!item) break;
        pushOp(item);
    }

    const calcStack: Operand[] = [];

    for (const item of output) {
        if (typeof item === 'function') {
            const y = calcStack.pop();
            const x = calcStack.pop();

            if (x == null || y == null) return NaN;

            const result = op(x, y, item);
            calcStack.push(result);
        } else {
            calcStack.push(item);
        }
    }

    return calcStack[0];
}
export function trunc(input: Point): Point {
    return op(input, 0, (x) => Math.floor(x)) as Point;
}
const EPS = 0.001;
export const pointsEquals = (a: Point, b: Point) => Math.abs(a[0] - b[0]) < EPS && Math.abs(a[1] - b[1]) < EPS;

export const getDirection = (point: Point): Direction => {
    const [x, y] = point;
    const absX = Math.abs(x);
    const absY = Math.abs(y);
    if (absX === 0 && absY === 0) return Direction.NONE;
    else if (absX > absY) return x < 0 ? Direction.WEST : Direction.EAST;
    else if (absY > absX) return y < 0 ? Direction.NORTH : Direction.SOUTH;
    else return Direction.NONE;
}
export const getOppositeDirection = (dir: Direction): Direction => {
    switch (dir) {
        case Direction.NORTH: return Direction.SOUTH;
        case Direction.SOUTH: return Direction.NORTH;
        case Direction.WEST: return Direction.EAST;
        case Direction.EAST: return Direction.WEST;
        default: return Direction.NONE;
    }
}
export const DIRECTION_VECTORS: Record<Direction, Point> = {
    [Direction.NONE]: [0, 0],
    [Direction.NORTH]: [0, -1],
    [Direction.SOUTH]: [0, 1],
    [Direction.WEST]: [-1, 0],
    [Direction.EAST]: [1, 0],
};
export const NEXT_DIRECTION: Record<Direction, Direction> = {
    [Direction.NONE]: Direction.NONE,
    [Direction.NORTH]: Direction.EAST,
    [Direction.SOUTH]: Direction.WEST,
    [Direction.WEST]: Direction.NORTH,
    [Direction.EAST]: Direction.SOUTH,
};
export const ALL_DIRECTIONS = [Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST];
export const movePoint = (point: Point, direction: Direction, scale: number = 1): Point => [
    point[0] + DIRECTION_VECTORS[direction][0] * scale,
    point[1] + DIRECTION_VECTORS[direction][1] * scale,
];