import pyscript
import js
from pyscript import document, fetch as pyfetch
from pyscript.ffi import create_proxy
import asyncio
import json
import math

document.getElementById("shape-options").style.display = "flex"

image = document.getElementById("annotation-image")
zone = document.getElementById("annotation-zone")
confirm_button = document.getElementById("annotation-confirm")
cancel_button = document.getElementById("annotation-cancel")
backspace_button = document.getElementById("annotation-backspace")
delete_button = document.getElementById("annotation-delete")
previous_button = document.getElementById("annotation-previous")
next_button = document.getElementById("annotation-next")
save_button = document.getElementById("annotation-save")
zoom_slider = document.getElementById("zoom-slider")
zoom_in_button = document.getElementById("zoom-in")
zoom_out_button = document.getElementById("zoom-out")
zoom_indicator = document.getElementById("zoom-indicator")

object_list = document.getElementById("object-types")
object_list_content = document.getElementById("object-types-content")
object_list_filter = document.getElementById("filter-object-types")

confirm_button.style.display = "none"
cancel_button.style.display = "none"
backspace_button.style.display = "none"
delete_button.style.display = "none"
previous_button.style.display = "none"
next_button.style.display = "none"
shape_type = ""
bbox_pos = None
new_shape = None
selected_shape = None


def make_shape_container():
    shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
    shape.setAttribute("width", "100%")
    shape.setAttribute("height", "100%")
    shape.setAttribute("viewBox", "0 0 1 1")
    shape.setAttribute("preserveAspectRatio", "none")
    shape.classList.add("shape-container")

    return shape


async def get_all_objects():
    response = await pyfetch("/api/object-types")
    if response.ok:
        return await response.json()


def follow_cursor(event):
    rect = zone.getBoundingClientRect()
    x = (event.clientX - rect.left) / scale
    y = (event.clientY - rect.top) / scale
    vertical_ruler.style.left = str(x) + "px"
    horizontal_ruler.style.top = str(y) + "px"


def change_object_type(event):
    global selected_shape
    if selected_shape is None:
        return
    selected_shape.setAttribute("data-object-type", event.currentTarget.value)


change_object_type_proxy = create_proxy(change_object_type)


def list_shapes():
    shapes = list(zone.getElementsByClassName("shape"))
    json_shapes = []
    for shape in shapes:
        shape_dict = {}
        if shape.tagName == "rect":
            shape_dict["type"] = "bbox"
            shape_dict["shape"] = {
                "x": float(shape.getAttribute("x")[:-1]),
                "y": float(shape.getAttribute("y")[:-1]),
                "w": float(shape.getAttribute("width")[:-1]),
                "h": float(shape.getAttribute("height")[:-1])
            }
        elif shape.tagName == "polygon" or shape.tagName == "polyline":
            if shape.tagName == "polygon":
                shape_dict["type"] = "polygon"
            elif shape.tagName == "polyline":
                shape_dict["type"] = "polyline"

            points = shape.getAttribute("points").split(" ")
            json_points = []
            for point in points:
                x, y = point.split(",")
                x, y = float(x), float(y)
                json_points.append({
                    "x": x,
                    "y": y
                })

            shape_dict["shape"] = json_points
        elif shape.tagName == "circle" and shape.classList.contains("shape-point"):
            shape_dict["type"] = "point"
            shape_dict["shape"] = {
                "x": float(shape.getAttribute("cx")),
                "y": float(shape.getAttribute("cy"))
            }
        else:
            continue

        shape_dict["object"] = shape.getAttribute("data-object-type")
        json_shapes.append(shape_dict)

    return json_shapes


def put_shapes(json_shapes):
    for shape in json_shapes:
        new_shape = make_shape_container()
        zone_rect = zone.getBoundingClientRect()

        if shape["type"] == "bbox":
            rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect")
            rectangle.setAttribute("x", str(shape["shape"]["x"]))
            rectangle.setAttribute("y", str(shape["shape"]["y"]))
            rectangle.setAttribute("width", str(shape["shape"]["w"]))
            rectangle.setAttribute("height", str(shape["shape"]["h"]))
            rectangle.setAttribute("fill", "none")
            rectangle.setAttribute("data-object-type", shape["object"] or "")
            rectangle.classList.add("shape-bbox")
            rectangle.classList.add("shape")
            new_shape.appendChild(rectangle)
        elif shape["type"] == "polygon" or shape["type"] == "polyline":
            polygon = document.createElementNS("http://www.w3.org/2000/svg", shape["type"])
            points = " ".join(
                [f"{point['x']},{point['y']}" for point in shape["shape"]])
            polygon.setAttribute("points", points)
            polygon.setAttribute("fill", "none")
            polygon.setAttribute("data-object-type", shape["object"] or "")
            polygon.classList.add(f"shape-{shape['type']}")
            polygon.classList.add("shape")
            new_shape.appendChild(polygon)
        elif shape["type"] == "point":
            point = document.createElementNS("http://www.w3.org/2000/svg", "circle")
            point.setAttribute("cx", str(shape["shape"]["x"]))
            point.setAttribute("cy", str(shape["shape"]["y"]))
            point.setAttribute("r", "0")
            point.classList.add("shape-point")
            point.classList.add("shape")
            point.setAttribute("data-object-type", shape["object"] or "")
            new_shape.appendChild(point)

        zone.appendChild(new_shape)


async def load_shapes():
    resource_id = document.getElementById("resource-id").value
    response = await pyfetch(f"/picture/{resource_id}/get-annotations")
    if response.ok:
        shapes = await response.json()
        return shapes


async def save_shapes(event):
    shapes = list_shapes()
    resource_id = document.getElementById("resource-id").value
    #print("Saving shapes:", shapes)
    response = await pyfetch(f"/picture/{resource_id}/save-annotations",
        method="POST",
        headers={
            "Content-Type": "application/json"
        },
        body=json.dumps(shapes)
    )


save_shapes_proxy = create_proxy(save_shapes)
save_button.addEventListener("click", save_shapes_proxy)
delete_shape_key_proxy = create_proxy(lambda event: event.key == "Delete" and delete_shape_proxy(None))
next_shape_key_proxy = create_proxy(lambda event: event.key == "ArrowRight" and next_shape_proxy(None))
previous_shape_key_proxy = create_proxy(lambda event: event.key == "ArrowLeft" and previous_shape_proxy(None))


def get_centre(shape):
    if shape.tagName == "rect":
        x = float(shape.getAttribute("x")) + float(shape.getAttribute("width")) / 2
        y = float(shape.getAttribute("y")) + float(shape.getAttribute("height")) / 2
    elif shape.tagName == "polygon":
        points = shape.getAttribute("points").split(" ")
        # Average of the extreme points
        sorted_x = sorted([float(point.split(",")[0]) for point in points])
        sorted_y = sorted([float(point.split(",")[1]) for point in points])
        top = sorted_y[0]
        bottom = sorted_y[-1]
        left = sorted_x[0]
        right = sorted_x[-1]

        x = (left + right) / 2
        y = (top + bottom) / 2
    elif shape.tagName == "polyline":
        points = shape.getAttribute("points").split(" ")
        # Median point
        x = float(points[len(points) // 2].split(",")[0])
        y = float(points[len(points) // 2].split(",")[1])
    elif shape.tagName == "circle" and shape.classList.contains("shape-point"):
        x = float(shape.getAttribute("cx"))
        y = float(shape.getAttribute("cy"))
    else:
        return None

    return x, y


async def update_object_list_filter(event):
    filter_text = event.currentTarget.value
    for label in object_list_content.children:
        if label.innerText.lower().find(filter_text.lower()) == -1:
            label.style.display = "none"
        else:
            label.style.display = "flex"


async def focus_shape(shape):
    global selected_shape

    if shape_type != "select":
        return
    if selected_shape is not None:
        selected_shape.classList.remove("selected")

    selected_shape = shape

    selected_shape.classList.add("selected")

    objects = await get_all_objects()

    delete_button.style.display = "flex"
    next_button.style.display = "flex"
    previous_button.style.display = "flex"
    document.addEventListener("keydown", delete_shape_key_proxy)
    document.addEventListener("keydown", next_shape_key_proxy)
    document.addEventListener("keydown", previous_shape_key_proxy)

    object_list_content.innerHTML = ""
    object_list_filter.value = ""
    new_radio = document.createElement("input")
    new_radio.setAttribute("type", "radio")
    new_radio.setAttribute("name", "object-type")
    new_radio.setAttribute("tabindex", "1")      # Focus first
    new_radio.setAttribute("value", "")
    new_label = document.createElement("label")
    new_label.appendChild(new_radio)
    new_label.append("Undefined")
    object_list_content.appendChild(new_label)
    new_radio.addEventListener("change", change_object_type_proxy)

    selected_object = selected_shape.getAttribute("data-object-type")
    if not selected_object:
        new_radio.setAttribute("checked", "")

    for object, description in objects.items():
        new_radio = document.createElement("input")
        new_radio.setAttribute("type", "radio")
        new_radio.setAttribute("name", "object-type")
        new_radio.setAttribute("value", object)
        if selected_object == object:
            new_radio.setAttribute("checked", "")
        new_label = document.createElement("label")
        new_label.appendChild(new_radio)
        new_label.append(object)
        object_list_content.appendChild(new_label)
        new_radio.addEventListener("change", change_object_type_proxy)

    object_list.style.display = "flex"
    object_list_filter.focus()
    object_list_filter.addEventListener("input", update_object_list_filter)
    object_list.style.left = str(get_centre(shape)[0] * 100) + "%"
    object_list.style.top = str(get_centre(shape)[1] * 100) + "%"
    object_list.style.right = "auto"
    object_list.style.bottom = "auto"
    object_list.style.maxHeight = str(image.height * (1 - get_centre(shape)[1])) + "px"
    object_list.style.maxWidth = str(image.width * (1 - get_centre(shape)[0])) + "px"
    transform_origin = ["top", "left"]

    if get_centre(shape)[0] * image.width + object_list.offsetWidth > image.width:
        object_list.style.left = "auto"
        object_list.style.right = str((image.width - get_centre(shape)[0]) * 100) + "%"
        object_list.style.maxWidth = str(get_centre(shape)[0] * image.width) + "px"
        transform_origin[1] = "right"

    if get_centre(shape)[1] / image.height * image.height + object_list.offsetHeight > image.height:
        object_list.style.top = "auto"
        object_list.style.bottom = str((image.height - get_centre(shape)[1]) * 100) + "%"
        object_list.style.maxHeight = str(get_centre(shape)[1] * image.height) + "px"
        transform_origin[0] = "bottom"

    object_list.style.transformOrigin = " ".join(transform_origin)


async def select_shape(event):
    if drag_distance == 0:
        await focus_shape(event.target)


async def next_shape(event):
    global selected_shape
    if selected_shape is None:
        return

    selected_svg = selected_shape.parentNode

    while selected_svg is not None:
        next_sibling = selected_svg.nextElementSibling
        if next_sibling and next_sibling.classList.contains("shape-container"):
            selected_svg = next_sibling
            break
        elif next_sibling is None:
            # If no more siblings, loop back to the first child
            selected_svg = selected_svg.parentNode.firstElementChild
            while selected_svg is not None and not selected_svg.classList.contains(
                    "shape-container"):
                selected_svg = selected_svg.nextElementSibling
            break
        else:
            selected_svg = next_sibling

    if selected_svg:
        shape = selected_svg.firstElementChild
        await focus_shape(shape)

    event.preventDefault()


async def previous_shape(event):
    global selected_shape
    if selected_shape is None:
        return

    selected_svg = selected_shape.parentNode

    while selected_svg is not None:
        next_sibling = selected_svg.previousElementSibling
        if next_sibling and next_sibling.classList.contains("shape-container"):
            selected_svg = next_sibling
            break
        elif next_sibling is None:
            # If no more siblings, loop back to the last child
            selected_svg = selected_svg.parentNode.lastElementChild
            while selected_svg is not None and not selected_svg.classList.contains(
                    "shape-container"):
                selected_svg = selected_svg.previousElementSibling
            break
        else:
            selected_svg = next_sibling

    if selected_svg:
        shape = selected_svg.firstElementChild
        await focus_shape(shape)

    event.preventDefault()


def unselect_shape(event):
    global selected_shape

    if drag_distance == 0:
        if selected_shape is not None:
            selected_shape.classList.remove("selected")
            selected_shape = None

        object_list_content.innerHTML = ""
        object_list.style.display = "none"
        delete_button.style.display = "none"
        next_button.style.display = "none"
        previous_button.style.display = "none"
        document.removeEventListener("keydown", delete_shape_key_proxy)
        document.removeEventListener("keydown", next_shape_key_proxy)
        document.removeEventListener("keydown", previous_shape_key_proxy)


def delete_shape(event):
    global selected_shape
    if selected_shape is None:
        return
    # Shape is SVG shape inside SVG so we need to remove the parent SVG
    selected_shape.parentNode.remove()
    selected_shape = None
    object_list_content.innerHTML = ""
    object_list.style.display = "none"
    delete_button.style.display = "none"
    next_button.style.display = "none"
    previous_button.style.display = "none"
    document.removeEventListener("keydown", delete_shape_key_proxy)
    document.removeEventListener("keydown", next_shape_key_proxy)
    document.removeEventListener("keydown", previous_shape_key_proxy)


select_shape_proxy = create_proxy(select_shape)
unselect_shape_proxy = create_proxy(unselect_shape)
delete_shape_proxy = create_proxy(delete_shape)
next_shape_proxy = create_proxy(next_shape)
previous_shape_proxy = create_proxy(previous_shape)

delete_button.addEventListener("click", delete_shape_proxy)
next_button.addEventListener("click", next_shape_proxy)
previous_button.addEventListener("click", previous_shape_proxy)

# These are functions usable in JS
cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event))
make_bbox_proxy = create_proxy(lambda event: make_bbox(event))
make_polygon_proxy = create_proxy(lambda event: make_polygon(event))
follow_cursor_proxy = create_proxy(follow_cursor)


def switch_shape(event):
    global shape_type
    object_list_content.innerHTML = ""
    unselect_shape(None)
    shape = event.currentTarget.id
    shape_type = shape
    for button in list(document.getElementById("shape-selector").children):
        if not button.classList.contains("button-flat"):
            button.classList.add("button-flat")
    event.currentTarget.classList.remove("button-flat")
    if shape_type == "select":
        # Add event listeners to existing shapes
        print(len(list(document.getElementsByClassName("shape"))), "shapes found")
        for shape in document.getElementsByClassName("shape"):
            print("Adding event listener to shape:", shape)
            shape.addEventListener("click", select_shape_proxy)
        image.addEventListener("click", unselect_shape_proxy)
        helper_message.innerText = "Click on a shape to select"
        zone.style.cursor = "inherit"
        # Cancel the current shape creation
        if shape_type == "shape-bbox":
            cancel_bbox(None)
        elif shape_type == "shape-polygon":
            cancel_polygon(None)
        elif shape_type == "shape-polyline":
            cancel_polygon(None)
    else:
        zone.style.cursor = "pointer"
        if shape_type == "shape-point":
            zone.style.cursor = "crosshair"
        # Remove event listeners for selection
        for shape in document.getElementsByClassName("shape"):
            print("Removing event listener from shape:", shape)
            shape.removeEventListener("click", select_shape_proxy)
        image.removeEventListener("click", unselect_shape_proxy)
        helper_message.innerText = "Select a shape type then click on the image to begin defining it"
    print("Shape is now of type:", shape)


def select_mode():
    global shape_type
    document.getElementById("select").click()


vertical_ruler = document.getElementById("annotation-ruler-vertical")
horizontal_ruler = document.getElementById("annotation-ruler-horizontal")
vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary")
horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary")
helper_message = document.getElementById("annotation-helper-message")

helper_message.innerText = "Select a shape type then click on the image to begin defining it"


def cancel_bbox(event):
    global bbox_pos, new_shape

    # Key must be ESCAPE
    if event is not None and hasattr(event, "key") and event.key != "Escape":
        return

    if new_shape is not None and event is not None:
        # Require event so the shape is kept when it ends normally
        new_shape.remove()
    zone.removeEventListener("click", make_bbox_proxy)
    document.removeEventListener("keydown", cancel_bbox_proxy)
    cancel_button.removeEventListener("click", cancel_bbox_proxy)

    bbox_pos = None
    vertical_ruler.style.display = "none"
    horizontal_ruler.style.display = "none"
    vertical_ruler_2.style.display = "none"
    horizontal_ruler_2.style.display = "none"
    zone.style.cursor = "auto"
    cancel_button.style.display = "none"
    helper_message.innerText = "Select a shape type then click on the image to begin defining it"
    new_shape = None
    update_transform()


def make_bbox(event):
    global new_shape, bbox_pos
    zone_rect = zone.getBoundingClientRect()

    if bbox_pos is None:
        helper_message.innerText = "Now define the second point"

        bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width,
                    (event.clientY - zone_rect.top) / zone_rect.height]
        vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%"
        horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%"
        vertical_ruler_2.style.display = "block"
        horizontal_ruler_2.style.display = "block"

    else:
        x0, y0 = bbox_pos.copy()
        x1 = (event.clientX - zone_rect.left) / zone_rect.width
        y1 = (event.clientY - zone_rect.top) / zone_rect.height

        rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect")

        new_shape = make_shape_container()
        zone_rect = zone.getBoundingClientRect()

        new_shape.appendChild(rectangle)
        zone.appendChild(new_shape)

        minx = min(x0, x1)
        miny = min(y0, y1)
        maxx = max(x0, x1)
        maxy = max(y0, y1)

        rectangle.setAttribute("x", str(minx))
        rectangle.setAttribute("y", str(miny))
        rectangle.setAttribute("width", str(maxx - minx))
        rectangle.setAttribute("height", str(maxy - miny))
        rectangle.setAttribute("fill", "none")
        rectangle.setAttribute("data-object-type", "")
        rectangle.classList.add("shape-bbox")
        rectangle.classList.add("shape")

        # Add event listeners to the new shape
        rectangle.addEventListener("click", select_shape_proxy)

        cancel_bbox(None)


polygon_points = []


def make_polygon(event):
    global new_shape, polygon_points

    polygon = new_shape.children[0]

    zone_rect = zone.getBoundingClientRect()

    polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width,
                           (event.clientY - zone_rect.top) / zone_rect.height))

    # Update the polygon
    polygon.setAttribute("points", " ".join(
            [f"{point[0]},{point[1]}" for point in
             polygon_points]))


def reset_polygon():
    global new_shape, polygon_points

    zone.removeEventListener("click", make_polygon_proxy)
    document.removeEventListener("keydown", close_polygon_proxy)
    document.removeEventListener("keydown", cancel_polygon_proxy)
    document.removeEventListener("keydown", backspace_polygon_proxy)
    confirm_button.style.display = "none"
    cancel_button.style.display = "none"
    backspace_button.style.display = "none"
    confirm_button.removeEventListener("click", close_polygon_proxy)
    cancel_button.removeEventListener("click", cancel_polygon_proxy)
    backspace_button.removeEventListener("click", backspace_polygon_proxy)
    polygon_points.clear()

    zone.style.cursor = "auto"
    new_shape = None

    update_transform()


def close_polygon(event):
    if event is not None and hasattr(event, "key") and event.key != "Enter":
        return
    # Polygon is already there, but we need to remove the events
    reset_polygon()


def cancel_polygon(event):
    if event is not None and hasattr(event, "key") and event.key != "Escape":
        return
    # Delete the polygon
    new_shape.remove()
    reset_polygon()


def backspace_polygon(event):
    if event is not None and hasattr(event, "key") and event.key != "Backspace":
        return
    if not polygon_points:
        return
    polygon_points.pop()
    polygon = new_shape.children[0]
    polygon.setAttribute("points", " ".join(
            [f"{point[0]},{point[1]}" for point in
             polygon_points]))


close_polygon_proxy = create_proxy(close_polygon)
cancel_polygon_proxy = create_proxy(cancel_polygon)
backspace_polygon_proxy = create_proxy(backspace_polygon)


def open_shape(event):
    global new_shape, bbox_pos
    if bbox_pos or shape_type == "select":
        return
    print("Creating a new shape of type:", shape_type)

    if shape_type == "shape-bbox":
        helper_message.innerText = ("Define the first point at the intersection of the lines "
                                    "by clicking on the image, or click the cross to cancel")

        cancel_button.addEventListener("click", cancel_bbox_proxy)
        document.addEventListener("keydown", cancel_bbox_proxy)
        cancel_button.style.display = "flex"
        bbox_pos = None
        zone.addEventListener("click", make_bbox_proxy)
        vertical_ruler.style.display = "block"
        horizontal_ruler.style.display = "block"
        zone.style.cursor = "crosshair"
    elif shape_type == "shape-polygon" or shape_type == "shape-polyline":
        if shape_type == "shape-polygon":
            helper_message.innerText = ("Click on the image to define the points of the polygon, "
                                        "press escape to cancel, enter to close, or backspace to "
                                        "remove the last point")
        elif shape_type == "shape-polyline":
            helper_message.innerText = ("Click on the image to define the points of the polyline, "
                                        "press escape to cancel, enter to finish, or backspace to "
                                        "remove the last point")

        if not polygon_points and not new_shape:
            new_shape = make_shape_container()
            zone_rect = zone.getBoundingClientRect()

        if not polygon_points and int(new_shape.children.length) == 0:
            zone.addEventListener("click", make_polygon_proxy)
            document.addEventListener("keydown", close_polygon_proxy)
            document.addEventListener("keydown", cancel_polygon_proxy)
            document.addEventListener("keydown", backspace_polygon_proxy)
            cancel_button.addEventListener("click", cancel_polygon_proxy)
            cancel_button.style.display = "flex"
            confirm_button.addEventListener("click", close_polygon_proxy)
            confirm_button.style.display = "flex"
            backspace_button.addEventListener("click", backspace_polygon_proxy)
            backspace_button.style.display = "flex"
            if shape_type == "shape-polygon":
                polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
                polygon.classList.add("shape-polygon")
            elif shape_type == "shape-polyline":
                polygon = document.createElementNS("http://www.w3.org/2000/svg", "polyline")
                polygon.classList.add("shape-polyline")
            polygon.setAttribute("fill", "none")
            polygon.setAttribute("data-object-type", "")
            polygon.classList.add("shape")
            new_shape.appendChild(polygon)
            zone.appendChild(new_shape)
            zone.style.cursor = "crosshair"
    elif shape_type == "shape-point":
        point = document.createElementNS("http://www.w3.org/2000/svg", "circle")
        zone_rect = zone.getBoundingClientRect()
        point.setAttribute("cx", str((event.clientX - zone_rect.left) / zone_rect.width))
        point.setAttribute("cy", str((event.clientY - zone_rect.top) / zone_rect.height))
        point.setAttribute("r", "0")
        point.classList.add("shape-point")
        point.classList.add("shape")
        point.setAttribute("data-object-type", "")

        new_shape = make_shape_container()
        zone_rect = zone.getBoundingClientRect()

        new_shape.appendChild(point)
        zone.appendChild(new_shape)

        new_shape = None

    update_transform()



for button in list(document.getElementById("shape-selector").children):
    button.addEventListener("click", create_proxy(switch_shape))
    print("Shape", button.id, "is available")

zone.addEventListener("mousemove", follow_cursor_proxy)
zone.addEventListener("click", create_proxy(open_shape))

# Zoom
zoom_container = document.getElementById("annotation-zoom-container")
scale = 1
translate_x = 0
translate_y = 0

scale_exponent = 0

def update_transform():
    zone.style.transform = f"translate({translate_x}px, {translate_y}px) scale({scale}) "
    object_list.style.transform = f"scale({1/scale})"   # neutralise the zoom
    vertical_ruler.style.transform = f"scale({1/scale})"
    horizontal_ruler.style.transform = f"scale({1/scale})"
    vertical_ruler_2.style.transform = f"scale({1/scale})"
    horizontal_ruler_2.style.transform = f"scale({1/scale})"

    for svg in zone.getElementsByClassName("shape-container"):
        # Because vector-effect="non-scaling-stroke" doesn't work when an HTML ancestor is
        # scaled, we need to scale the shapes down then scale them back up
        svg.style.transform = f"scale({1/scale})"
        svg.style.transformOrigin = "0 0"
        svg.setAttribute("width", f"{100 * scale}%")
        svg.setAttribute("height", f"{100 * scale}%")

    zoom_indicator.innerText = f"{scale:.3f}x"


def compute_zoom_translation_around_point(element, x, y, sc, nsc):
    rect = element.getBoundingClientRect()
    # The difference in size between the new and old scales.
    size_difference_x = (nsc - sc) * image.width
    size_difference_y = (nsc - sc) * image.height
    width = rect.width / sc
    height = rect.height / sc

    # (size difference) * ((cursor position) / (image size) - (transform origin))
    tx = size_difference_x * (x / width - 0.5)
    ty = size_difference_y * (y / height - 0.5)

    return tx, ty


def on_wheel(event):
    global scale, scale_exponent, translate_x, translate_y
    if object_list.matches(":hover"):
        return

    event.preventDefault()

    rect = zone.getBoundingClientRect()

    # Position of the cursor in the unscaled image.
    mouse_x = (event.clientX - rect.left) / scale
    mouse_y = (event.clientY - rect.top) / scale

    # Adjust scale exponent and compute new scale.
    scale_exponent += (-1 if event.deltaY > 0 else 1) / 4
    new_scale = 2 ** scale_exponent

    # Limit the scale to a reasonable range.
    new_scale = max(0.0625, min(64, new_scale))

    # Compute the new translation.
    offset_x, offset_y = compute_zoom_translation_around_point(zone, mouse_x, mouse_y, scale, new_scale)
    translate_x -= offset_x
    translate_y -= offset_y

    scale = new_scale
    zoom_slider.value = scale_exponent

    update_transform()


is_dragging = False
drag_distance = 0
start_x = 0
start_y = 0

def on_mouse_down(event):
    global is_dragging, start_x, start_y
    if object_list.matches(":hover"):
        return

    if event.button == 0:
        is_dragging = True
        start_x = event.clientX
        start_y = event.clientY
        zoom_container.style.cursor = "grabbing"

def on_mouse_move(event):
    global translate_x, translate_y, drag_distance
    if object_list.matches(":hover"):
        return

    if is_dragging and min(event.movementX, event.movementY) != 0:
        drag_distance += abs(event.movementX) + abs(event.movementY)
        translate_x += event.movementX
        translate_y += event.movementY
        update_transform()

def on_mouse_up(event):
    global is_dragging, drag_distance
    if drag_distance:
        # Don't click if the image was panned
        event.preventDefault()
        event.stopPropagation()
    if object_list.matches(":hover"):
        return
    is_dragging = False
    drag_distance = 0
    zoom_container.style.cursor = "grab"

last_touch_positions = None
last_touch_distance = None

def calculate_distance(touches):
    dx = touches[1].clientX - touches[0].clientX
    dy = touches[1].clientY - touches[0].clientY
    return (dx**2 + dy**2) ** 0.5

def on_touch_start(event):
    global last_touch_positions, last_touch_distance

    if event.touches.length == 2:
        event.preventDefault()
        last_touch_positions = [
            ((event.touches[0].clientX - zone.getBoundingClientRect().left) / scale, (event.touches[0].clientY - zone.getBoundingClientRect().top) / scale),
            ((event.touches[1].clientX - zone.getBoundingClientRect().left) / scale, (event.touches[1].clientY - zone.getBoundingClientRect().top) / scale)
        ]
        last_touch_distance = calculate_distance(event.touches)

def on_touch_move(event):
    global translate_x, translate_y, scale, last_touch_positions, last_touch_distance, scale_exponent

    if event.touches.length == 2:
        event.preventDefault()

        # Finger positions.
        current_positions = [
            ((event.touches[0].clientX - zone.getBoundingClientRect().left) / scale, (event.touches[0].clientY - zone.getBoundingClientRect().top) / scale),
            ((event.touches[1].clientX - zone.getBoundingClientRect().left) / scale, (event.touches[1].clientY - zone.getBoundingClientRect().top) / scale)
        ]

        current_distance = calculate_distance(event.touches)
        # Ignore small movements.
        if last_touch_distance and abs(current_distance - last_touch_distance) > min(image.width, image.height) / 64:
            zoom_factor = current_distance / last_touch_distance
            new_scale = max(0.0625, min(64, scale * zoom_factor))
            scale_exponent = math.log2(new_scale)
            zoom_slider.value = scale_exponent
        else:
            new_scale = scale

        # Finger midpoint.
        midpoint_x = (current_positions[0][0] + current_positions[1][0]) / 2
        midpoint_y = (current_positions[0][1] + current_positions[1][1]) / 2

        # Compute translation for zooming around the midpoint.
        tx, ty = compute_zoom_translation_around_point(zone, midpoint_x, midpoint_y, scale, new_scale)

        # Compute translation for panning.
        if last_touch_positions:
            delta_x = sum(p[0] - lp[0] for p, lp in zip(current_positions, last_touch_positions)) / 2
            delta_y = sum(p[1] - lp[1] for p, lp in zip(current_positions, last_touch_positions)) / 2
        else:
            delta_x = delta_y = 0

        translate_x += delta_x - tx
        translate_y += delta_y - ty

        # Update state.
        scale = new_scale
        last_touch_positions = current_positions
        last_touch_distance = current_distance

        zoom_slider.value = scale_exponent
        update_transform()


def on_touch_end(event):
    global last_touch_positions, last_touch_distance
    if event.touches.length < 2:
        last_touch_positions = None
        last_touch_distance = None


def zoom_slider_change(event):
    global scale, scale_exponent
    scale_exponent = float(event.currentTarget.value)
    scale = 2 ** scale_exponent

    update_transform()


def zoom_in(event):
    zoom_slider.stepUp()
    zoom_slider.dispatchEvent(js.Event.new("input"))


def zoom_out(event):
    zoom_slider.stepDown()
    zoom_slider.dispatchEvent(js.Event.new("input"))


zoom_container.addEventListener("wheel", on_wheel)
zoom_container.addEventListener("mousedown", on_mouse_down)
document.addEventListener("mousemove", on_mouse_move)
document.addEventListener("click", on_mouse_up)
zone.addEventListener("touchstart", on_touch_start)
zone.addEventListener("touchmove", on_touch_move)
zone.addEventListener("touchend", on_touch_end)
zoom_slider.addEventListener("input", zoom_slider_change)
zoom_in_button.addEventListener("click", zoom_in)
zoom_out_button.addEventListener("click", zoom_out)

# Load existing annotations, if any
put_shapes(await load_shapes())
update_transform()
print("Ready!")