import { Heap } from 'ts-heap'
import { HalfEdge, Face, Vertex } from './dcel';

interface WithPriority {
    get_priority(): number;
}

export class Point {
    x: number;
    y: number;
    face?: Face;

    constructor(x: number, y: number, face? :Face) {
        this.x = x;
        this.y = y;
        this.face = face;
    }

    equals(other: Point): boolean {
        return Math.abs(this.x - other.x) + Math.abs(this.y - other.y) < 0.00001;
    }

    toString(): string {
        return `{x: ${this.x}, y: ${this.y}}`;
    }
}

class CircleEvent implements WithPriority {
    y: number;
    alive: boolean = true;
    center: Vertex;
    leaf: Leaf;

    from: Point[];

    static
    from_sites(s1: Point, s2: Point, s3: Point, leaf: Leaf): CircleEvent {
        const a = s1.x * (s2.y - s3.y) - s1.y*(s2.x - s3.x) + s2.x*s3.y - s3.x * s2.y;
        const b = (s1.x ** 2 + s1.y ** 2) * (s3.y - s2.y) + (s2.x ** 2 + s2.y ** 2)*(s1.y - s3.y) + (s3.x ** 2 + s3.y ** 2) * (s2.y - s1.y);
        const c = (s1.x ** 2 + s1.y ** 2) * (s2.x - s3.x) + (s2.x ** 2 + s2.y ** 2)*(s3.x - s1.x) + (s3.x ** 2 + s3.y ** 2) * (s1.x - s2.x);
        const d = (s1.x ** 2 + s1.y ** 2) * (s3.x*s2.y - s2.x*s3.y) + (s2.x ** 2 + s2.y ** 2)*(s1.x*s3.y - s3.x*s1.y) + (s3.x ** 2 + s3.y ** 2) * (s2.x*s1.y - s1.x*s2.y);

        const center = new Vertex(-b / (2. * a), -c / (2. * a));
        const r = Math.sqrt((b ** 2 + c ** 2 - 4. * a * d) / (4. * a ** 2));
        const y = center.coords[1] - r;

        const out = new CircleEvent();
        out.y = y;
        out.center = center;
        out.leaf = leaf;

        out.from = [s1, s2, s3];

        return out;
    }

    get_priority(): number {
        return this.y;
    }

    print() {
        console.log(`Circle event at ${this.y} ${JSON.stringify(this.center)}, ${this.leaf.point.toString()} from ${JSON.stringify(this.from.map((e) => e.toString()))}`);
    }
}

class SiteEvent implements WithPriority{
    face: Face;
    point: Point;

    constructor(point: Point) {
        this.face = new Face(point);
        this.point = point;
        this.point.face = this.face;
    }

    get_priority(): number {
        return this.point.y;
    }

    print() {
        console.log(`Site event ${this.point.toString()}`);
    }
}

function calc_x(left: Point, right: Point, y: number): number {
    const [a1, b1, c1] = from_focus_vertex(left, y);
    const [a2, b2, c2] = from_focus_vertex(right, y);

    if (Math.abs(a1 - a2) < 0.0001) {
        return (left.x + right.x) / 2.;
    }

    const da = a1 - a2;
    const db = b1 - b2;
    const dc = c1 - c2;

    const d = db * db - 4. * da * dc;

    if (d <= 0.) {
        throw new Error(`D is less then 0 ${d}`);
    }

    const dd = Math.sqrt(d);

    const x = (-db + dd) / (2. * da);

    return x;
}

function from_focus_vertex(focus: Point, y: number): number[] {
    const a = (focus.y - y) / 2;
    const h = focus.x;
    const k = focus.y - a;

    return [1 / (4. * a), -h / (2 * a), (h ** 2 / (4 * a)) + k]
}

function cmp_event(e1: WithPriority, e2: WithPriority) {
    return e2.get_priority() - e1.get_priority();
}

type Queue = Heap<WithPriority>;

type Node = Leaf | Breakpoint;

type Parent = Breakpoint | State;

class Leaf {
    point: Point;
    event: CircleEvent | undefined;

    left: Leaf | undefined;
    right: Leaf | undefined;
    parent: Parent;

    half_edge: HalfEdge;

    constructor(point: Point, parent: Parent) {
        this.point = point;
        this.parent = parent;
        this.half_edge = new HalfEdge(undefined, undefined);
    }

    false_alarm() {
        if(this.event) {
            console.log(`False alarm ${JSON.stringify(this.event.center)} ${this.event.y}`);
            this.event.alive = false;
        }
    }

    update_left(leaf: Leaf) {
        if (this.left) {
            this.left.right = leaf;
        }
        leaf.left = this.left;
    }

    update_right(leaf: Leaf) {
        if (this.right) {
            this.right.left = leaf;
        }
        leaf.right = this.right;
    }

    split(point: Point, events: Queue) {
        this.false_alarm();

        if (this.point.y == point.y) {
            const middle = new Leaf(point, undefined);
            const parent = this.parent;

            if (this.point.x > point.x) {
                const br = new Breakpoint([point, middle], [this.point, this], this.parent);

                if (this.left) this.left.right = middle;
                this.left = middle;
                middle.right = this;

                if (parent instanceof Breakpoint) {
                    parent.set_me(this, br);
                } else {
                    parent.root = br;
                }

                const maybe_left = middle.check_circles(point.y, events);

                if (maybe_left && maybe_left.center.coords[0] < middle.point.x) {
                    console.log(`Adding circle`);
                    maybe_left.print();
                    middle.event = maybe_left;
                    events.add(maybe_left);
                }

                const maybe_right = this.check_circles(point.y, events);
                if (maybe_right && maybe_right.center.coords[0] >= middle.point.x) {
                    console.log(`Adding circle`);
                    maybe_right.print();
                    this.event = maybe_right;
                    events.add(maybe_right);
                }

            } else {
                const br = new Breakpoint([this.point, this], [point, middle], this.parent);

                if (this.right) this.right.left = middle;
                this.right = middle;
                middle.left = this;

                if (parent instanceof Breakpoint) {
                    parent.set_me(this, br);
                } else {
                    parent.root = br;
                }

                const maybe_left = this.check_circles(point.y, events);

                if (maybe_left && maybe_left.center.coords[0] < middle.point.x) {
                    console.log(`Adding circle`);
                    maybe_left.print();
                    this.event = maybe_left;
                    events.add(maybe_left);
                }

                const maybe_right = middle.check_circles(point.y, events);
                if (maybe_right && maybe_right.center.coords[0] >= middle.point.x) {
                    console.log(`Adding circle`);
                    maybe_right.print();
                    middle.event = maybe_right;
                    events.add(maybe_right);
                }
            }

            return;
        }

        const left = new Leaf(this.point, undefined);
        left.left = this.left;
        if (this.left) this.left.right = left;

        const right = new Leaf(this.point, undefined);
        right.right = this.right;
        if (this.right) this.right.left = right;

        const middle = new Leaf(point, undefined);
        middle.left = left;
        middle.right = right;

        right.left = middle;
        left.right = middle;

        const br1 = new Breakpoint([this.point, left], [point, middle], undefined);
        const br2 = new Breakpoint([point, br1], [this.point, right], this.parent);
        br1.parent = br2;

        if (this.parent instanceof Breakpoint) {
            this.parent.set_me(this, br2);
        } else {
            this.parent.root = br2;
        }

        const maybe_left = left.check_circles(point.y, events);
        if (maybe_left && maybe_left.center.coords[0] < middle.point.x) {
            console.log(`Adding circle`);
            maybe_left.print();
            left.event = maybe_left;
            events.add(maybe_left);
        }

        const maybe_right = right.check_circles(point.y, events);
        if (maybe_right && maybe_right.center.coords[0] >= middle.point.x) {
            console.log(`Adding circle`);
            maybe_right.print();
            right.event = maybe_right;
            events.add(maybe_right);
        }
    }

    check_circles(y: number, events: Queue): CircleEvent | undefined {
        const left = this.left;
        const right = this.right;

        if (left && right) {
            const circle = CircleEvent.from_sites(left.point, this.point, right.point, this);
            console.log(`${circle.y} < ${y}`);
            if (circle.y < y ) {
                return circle;
            }
        }
    }

    delete(vertex: Vertex) {
        if (this.parent instanceof Breakpoint) {
            this.parent.remove_me(this.point, vertex);
        } else {
            console.error("Shouldnt be here");
        }
    }

    print(indent: string) {
        console.log(`${indent}Leaf from ${this.point.toString()} vertex: ${this.half_edge.to_string()}`);
    }
}

class Breakpoint {
    left: [Point, Node];
    right: [Point, Node];
    parent: Parent;
    half_edge: HalfEdge;

    constructor(left: [Point, Node], right: [Point, Node], parent: Parent) {
        this.left = left;
        this.right = right;
        this.left[1].parent = this;
        this.right[1].parent = this;
        this.parent = parent;

        this.half_edge = new HalfEdge(undefined, left[0].face, right[0].face);
    }

    remove_me(point: Point, vertex: Vertex) {
        const edge = this.half_edge.insert(vertex);
        const other = this.get_other(point);
        if (this.parent instanceof Breakpoint) {
            this.parent.set_me(this, other);
            this.parent.set_edge(edge);
        } else {
            this.parent.root = other;
        }

    }

    set_edge(edge: HalfEdge) {
        this.left[1].half_edge = edge;
        // this.right[1].half_edge = edge.split(edge.origin);
    }

    set_me(old_me: Node, new_me: Node) {
        if (this.left[1] == old_me) {
            this.left[1] = new_me;
        } else {
            this.right[1] = new_me;
        }
    }

    get(point: Point): Leaf {
        const { x, y } = point;
        const test_x = calc_x(this.left[0], this.right[0], y);

        if (test_x >= x) {
            if (this.left[1] instanceof Leaf) {
                return this.left[1];
            } else {
                return this.left[1].get(point);
            }
        } else {
            if (this.right[1] instanceof Leaf) {
                return this.right[1];
            } else {
                return this.right[1].get(point);
            }
        }
    }

    get_other(point: Point): Node {
        if (this.left[0].equals(point)) {
            return this.right[1];
        } else {
            return this.left[1];
        }
    }

    print(indent: string) {
        console.log(`${indent}vertex: ${this.half_edge.to_string()}`);
        console.log(`${indent}left`);
        this.left[1].print(indent + '  ');
        console.log(`${indent}right`);
        this.right[1].print(indent + '  ');
    }
}

function get_from_node(root: Node, point: Point): Leaf {
    if (root instanceof Leaf) {
        return root;
    } else {
        return root.get(point);
    }
}

class State {
    root: Node | undefined;

    print() {
        if (this.root) {
            this.root.print('');
        } else {
            console.log("No root no tree");
        }
    }
}

export function voronoi(points: Point[]): Point[] {
    const out = [];
    const state = new State;
    const queue = new Heap<WithPriority>(cmp_event);


    for (let point of points) {
        queue.add(new SiteEvent(point));
    }

    let event;
    while (event = queue.pop()){
        console.log('---------------------------');
        event.print();

        if (event instanceof SiteEvent) {
            handle_site_event(event, queue, state, out);
        } else {
            if (!event.alive) {
                console.log("Dead");
                continue;
            }
            handle_circle_event(event, queue, state, out);
        }
        state.print();
        console.log(queue);
        print_leaves(get_from_node(state.root, new Point(0, 0)));
    }

    return out;
}

function handle_site_event(event: SiteEvent, queue: Queue, state: State, out: Point[]) {
    if (state.root) {
        const leaf = get_from_node(state.root, event.point);
        leaf.split(event.point, queue);

    } else {
        state.root = new Leaf(event.point, state);
    }
}

function handle_circle_event(event: CircleEvent, queue: Queue, state: State, out: [number, number][]) {
    if (!event.alive) return;

    event.leaf.delete(event.center);
    const right = event.leaf.right;
    const left = event.leaf.left;

    if (right) {
        right.false_alarm();
        // if (right.right) right.right.false_alarm();

        right.left = left;
        const maybe_right = right.check_circles(event.y, queue);
        if (maybe_right){
            console.log(`Adding circle event`);
            maybe_right.print();
            right.event = maybe_right;
            queue.add(maybe_right);
        }
    }

    if (left) {
        left.false_alarm();
        // if (left.left) left.left.false_alarm();
        left.right = right;
        const maybe_left = left.check_circles(event.y, queue);

        if (maybe_left){
            console.log(`Adding circle event`);
            maybe_left.print();
            left.event = maybe_left;
            queue.add(maybe_left);
        }
    }

    out.push(event.center.coords);
}

function print_leaves(start: Leaf) {
    let current = start;

    while (current.left) {
        current = current.left;
    }

    const points = [current.point];

    while (current.right) {
        current = current.right;
        points.push(current.point);
    }

    console.log(JSON.stringify(points.map((p) => p.toString())));
}
