import * as preact from "preact";
import { VNode } from "preact";

import * as css from "@lib/css";
import * as rpc from "@lib/rpc";
import * as cache from "@lib/cache";
import * as refs from "@lib/refs";
import * as geom from "@lib/geom";

let global = window as any;

export let dragging: any = null; // for user code to set to anything!
export function setDragging(d: any) {
    dragging = d;
}

// Claude
function _isTouchDevice() {
    return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}

// Claude 3.5 Sonnet
function _isDragAndDropSupported() {
  const div = document.createElement('div');
  return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
}

export const supportsDragDrop = _isDragAndDropSupported();
export const isTouchDevice = _isTouchDevice()

let _debugVars: Record<string, any> = {};

export function debugVar(data: any) {
    Object.assign(_debugVars, data);
}

let frameMessages = new Map<string, any>()
export function postFrameMessage(key: string, value: any) {
    frameMessages.set(key, value)
}
export function consumeFrameMessage<T = any>(key: string): T | null {
    let value = frameMessages.get(key)
    frameMessages.delete(key)
    return value
}

export let rootDiv = document.body;

export let event: Event | null = null; // stores current event
function _redraw(): number {
    // returns the time it took to render in ms
    if (rootDiv === null) {
        return 0;
    }
    if (_block_router) {
        return 0;
    }
    const t1 = performance.now();
    const vdom = renderPageAndModals(gRootPage, gModalStack);
    preact.render(vdom, rootDiv);
    event = null;
    gesture = zeroFrameGesture()
    _debugVars = {};
    frameMessages.clear()
    mousePrev = structuredClone(mouse)

    const t2 = performance.now();
    const duration = t2 - t1;
    // if (duration > 5) {
    //     console.info(`redraw ${(duration).toFixed(2)}ms`);
    // }
    return duration;
}

// global.redraw = redraw;

let _redrawSchedule = 0;
function _animationFrameRedraw() {
    _redrawSchedule = 0;
    _redraw();
}

// schedules redraw as soon as possible
//
// this is so that the caller can do a bunch of stuff and then have
// the redraw called only after they're done with everything or when
// they have to yield for an async call
//
// Safe to schedule multiple times per frame; only the first time will schedule
export function scheduleRedraw() {
    if (_redrawSchedule === 0) {
        _redrawSchedule = requestAnimationFrame(_animationFrameRedraw);
    }
}

let _deferredRedrawScheduled = 0;
let _deferredRedrawDuration = 30;
function _deferredRedraw() {
    _deferredRedrawScheduled = 0;
    let duration = _redraw();
    _deferredRedrawDuration = Math.max(30, Math.round(duration * 4));
}

export function deferRedraw() {
    if (_deferredRedrawScheduled == 0) {
        _deferredRedrawScheduled = setTimeout(
            _deferredRedraw,
            _deferredRedrawDuration,
        );
    }
}

// @ts-ignore
window.scheduleRedraw = scheduleRedraw;
// @ts-ignore
window.refs = refs;

export function debugVarsPanel(): VNode {
    if (DEBUG) {
        return preact.h("div", { class: _clsDebugVars }, [
            _showDebugEntries(_debugVars),
        ]);
    } else {
        return preact.h(preact.Fragment, {})
    }
}

function _showDebugEntries(data: any, prefix = ""): VNode {
    return preact.h(
        preact.Fragment,
        {},
        Object.entries(data).map(([key, value]) => {
            if (typeof value === "object") {
                return _showDebugEntries(value, prefix + key + ".");
            } else {
                return preact.h("div", {}, [`${prefix + key}: ${value}`]);
            }
        }),
    );
}

const _clsDebugVars = css.cls("debug-vars", {
    position: "fixed",
    zIndex: "100000000",
    top: "0px",
    left: "0px",
    width: "content-fit",
    height: "content-fit",
    padding: "2px 4px",
    borderBottomRightRadius: "4px",
    fontFamily: "monospace",
    background: "hsla(0, 0%, 0%, 0.6)",
    color: "white",
    textShadow: "0px 0px 2px black",
    fontSize: "14px",
    pointerEvents: "none",
    ":empty": {
        display: "none",
    },
});

function nodeCustomEvent(node: Node, eventName: string, options?: object) {
    const event = new CustomEvent(eventName, options);
    node.dispatchEvent(event);
    // console.log(eventName, node)
}

function recursivelyDispatchCreateEvent(node: Node) {
    if (node instanceof Element && node.hasAttribute("listen-create")) {
        nodeCustomEvent(node, "create");
    }
    node.childNodes.forEach(recursivelyDispatchCreateEvent);
}

const observer = new MutationObserver((mutationsList: MutationRecord[]) => {
    const t1 = performance.now();
    let targets: Element[] = []
    for (const mutation of mutationsList) {
        if (mutation.target instanceof Element) {
            if (mutation.target.hasAttribute("listen-mutate")) {
                nodeCustomEvent(mutation.target, "mutate", { bubbles: true });
                targets.push(mutation.target)
            }
        }
        mutation.addedNodes.forEach(recursivelyDispatchCreateEvent);
        // mutation.removedNodes.forEach(node => nodeCustomEvent(node, 'destroy'));
    }
    /*
    const dur = performance.now() - t1;
    if (dur > 1) {
        console.log(`mutation dispatch ${dur.toFixed(2)}ms`, targets);
    }
    */
});

observer.observe(rootDiv, {
    subtree: true,
    childList: true,
    characterData: true,
    attributes: true,
});

export function captureEvent(e: Event) {
    if (e instanceof KeyboardEvent) {
        if (e.isComposing) {
            return;
        }
    }

    if (e instanceof MouseEvent) {
        captureMouseLocation(e)
    }

    if (isTouchEvent(e)) {
        processFrameGesture(e)
    }

    event = e; // make it available to all renderers!
    scheduleRedraw();
}

export let mouse = geom.zeroPoint();
export let mousePrev = geom.zeroPoint()

// window.addEventListener("click", captureEvent)
window.addEventListener("touch", captureEvent);
window.addEventListener("touchstart", captureEvent);
window.addEventListener("touchend", captureEvent);
window.addEventListener("touchcancel", captureEvent);
window.addEventListener("touchenter", captureEvent);
window.addEventListener("touchleave", captureEvent);
window.addEventListener("touchmove", captureEvent, { passive: false });

window.addEventListener("mousemove", captureEvent, { passive: false });

window.addEventListener("wheel", captureEvent);

window.addEventListener("mousedown", captureEvent);
window.addEventListener("mouseup", captureEvent);
document.addEventListener("input", captureEvent);
document.addEventListener("keydown", captureEvent);

document.fonts.addEventListener('loadingdone', scheduleRedraw);

function hasMouseDevice() {
    // js wtf
    return matchMedia('(pointer:fine)').matches
}

function isActuallyTouchEvent(e: MouseEvent) {
    return !hasMouseDevice() || (e as any).sourceCapabilities?.firesTouchEvents === true
}

function captureMouseLocation(e: MouseEvent) {
    if (isActuallyTouchEvent(e)) return

    mouse.x = e.clientX;
    mouse.y = e.clientY;
}

function passiveCapture(_event: Event) {
    deferRedraw();
}

document.addEventListener("scroll", passiveCapture, { passive: true });
window.addEventListener("resize", passiveCapture, { passive: true });

export function isUnderMouse(el: Element | null | undefined, cursor = mouse): boolean {
    if (!el) {
        return false;
    }
    let underMouse = document.elementFromPoint(cursor.x, cursor.y);
    return el === underMouse || el.contains(underMouse);
}

export function elementRect(element: Element | null | undefined): geom.Rect {
    if (!element) {
        return geom.zeroRect();
    } else {
        let r0 = element.getBoundingClientRect();
        return {
            x: r0.x,
            y: r0.y,
            width: r0.width,
            height: r0.height,
        };
    }
}

export type ClickLocation = "inside" | "outside" | "na";

export function clickLocationRelativeTo(el: HTMLElement | null, eventType: string = "mousedown") {
    if (
        el &&
        event &&
        event.target instanceof HTMLElement &&
        event.type === "mousedown"
    ) {
        if (el.contains(event.target)) {
            return "inside";
        } else {
            return "outside";
        }
    } else {
        return "na";
    }
}

export function clickOutside(...elements: (HTMLElement|null)[]): boolean {
    if (
        event instanceof MouseEvent &&
        event.target instanceof HTMLElement &&
        event.type === "mousedown" &&
        event.buttons === 1
    ) {
        for (let el of elements) {
            if (el && el.contains(event.target)) {
                return false
            }
        }
        return true;
    } else {
        // there's no click ..
        return false;
    }
}

export function keydownOn(el: HTMLElement | null): string {
    if (event && event.type === "keydown" && event.target === el) {
        return (event as KeyboardEvent).key;
    } else {
        return "";
    }
}

export function eventTargetId(type: string, targetId: string): boolean {
    return Boolean(event && event.type === type && event.target === document.getElementById(targetId))
}

export function inputEventOn(el: HTMLElement | null) {
    return el && event && event.type === "input" && event.target === el;
}

export function getEventTarget(): HTMLElement | null {
    if (event && event.target instanceof HTMLElement) {
        return event.target;
    } else {
        return null;
    }
}

// from https://web.dev/articles/canvas-hidipi
export function getContext2D(canvas: HTMLCanvasElement): CanvasRenderingContext2D {
    // Get the device pixel ratio, falling back to 1.
    var dpr = window.devicePixelRatio || 1;
    // Get the size of the canvas in CSS pixels.
    var rect = canvas.getBoundingClientRect();

    // Give the canvas pixel dimensions of their CSS
    // size * the device pixel ratio.
    canvas.width = rect.width * dpr;
    canvas.height = rect.height * dpr;

    var ctx = canvas.getContext('2d')!;
    ctx.clearRect(0, 0, rect.width, rect.height);
    // Scale all drawing operations by the dpr, so you
    // don't have to worry about the difference.
    ctx.scale(dpr, dpr);
    return ctx;
}

export function getWindowSize(): geom.Size {
    return {
        width: Math.min(window.outerWidth, window.innerWidth),
        height: Math.min(window.outerHeight, window.innerHeight),
    };
}

export function getScreenSize(): geom.Size {
    return {
        width: screen.availWidth,
        height: screen.availHeight,
    }
}

/*
    getImageSize fetches the image if its size is not already known.

    It's a good idea to call it as soon as possible even if the result is not going to be needed yet.
*/
const _imageSizes = new Map<string, geom.Size>();
const _waitingImages = new Set<string>();
export function getImageSize(url: string): geom.Size {
    const storedSize = _imageSizes.get(url);
    if (storedSize !== undefined) {
        return storedSize;
    }
    // we cannot find, lets fetch it
    // but if it's already being fetched, no need to fetch it again.
    if (!_waitingImages.has(url)) {
        _waitingImages.add(url);
        const img = new Image();
        img.onload = () => {
            _waitingImages.delete(url);
            _imageSizes.set(url, {
                width: img.naturalWidth,
                height: img.naturalHeight,
            });
            scheduleRedraw();
            // console.log("downloaded", url)
        };
        img.src = url;
    }
    return { width: 0, height: 0 };
}

export type ResolverFn<R> = (v: R) => void;
export type ModalViewFn<T = any, R = any> = (
    vm: T,
    resolve: ResolverFn<R>,
) => VNode;

interface RootPage<Data = any> {
    route: string;
    prefix: string;
    data: Data;
    view: (route: string, prefix: string, data: Data) => VNode;
}

let gRootPage: RootPage | null = null;

export function setRootPage(r: RootPage) {
    gRootPage = r;
}

global.getRoot = () => gRootPage;

export function getPageData(): unknown {
    return gRootPage?.data;
}

const clsModalBackdrop = css.cls("modal_backdrop", {
    position: "fixed",
    zIndex: "100000",
    top: "0",
    left: "0",
    right: "0",
    bottom: "0",
    background: "hsla(0, 0%, 0%, 0.5)",

    display: "flex",
    flexDirection: "column",
    alignItems: "center",
    justifyContent: "center",
});

function modalContainer(
    zIndex: number,
    content: VNode,
    resolver: Function,
    clickOutsideValue?: any,
): VNode {
    const id = "modal_" + zIndex;
    // const style = `z-index: ${zIndex}`; // FIXME is this needed?
    const onMouseDown = (event: MouseEvent) => {
        // ignore events caused by event propagation
        // console.log("target", event.target, "currentTarget", event.currentTarget);
        if (event.target !== event.currentTarget) {
            return;
        }

        if (clickOutsideValue !== undefined) {
            resolver(clickOutsideValue);
            scheduleRedraw();
        }
    };
    return preact.h("div", { id: id, class: clsModalBackdrop, onMouseDown }, [
        content,
    ]);
}

function renderPageAndModals(
    rootPage: RootPage | null,
    modalStack: ModalEntry[],
): VNode {
    let main: VNode;
    let modals: VNode[] = [];
    if (rootPage !== null) {
        main = rootPage.view(rootPage.route, rootPage.prefix, rootPage.data);
    } else {
        main = preact.h(preact.Fragment, {});
    }
    const base_z_index = 10000;
    for (const [index, modalItem] of modalStack.entries()) {
        const modalContent = modalItem.view(modalItem.vm, modalItem.resolve);
        modals.push(
            modalContainer(
                base_z_index + 1 + index,
                modalContent,
                modalItem.resolve,
                modalItem.clickOutsideValue,
            ),
        );
    }
    let modalsFragment = preact.h(preact.Fragment, {}, modals)
    let debugPanels = debugVarsPanel()
    return preact.h(preact.Fragment, {}, [main, modalsFragment, debugPanels]);
}

type RouteHandler<Data = any> = {
    fetch: (route: string, prefix: string) => Promise<rpc.Response<Data>>;
    view: (route: string, prefix: string, data: Data) => VNode;
}

type FetchFn<Data> = RouteHandler<Data>["fetch"]
type ViewFn<Data> = RouteHandler<Data>["view"]

export interface RouteEntry<Data = any> {
    prefix: string;
    handler: () => Promise<RouteHandler<Data>>
}

export function routeEntry<Data>(prefix: string, fetch: FetchFn<Data>, view: ViewFn<Data>): RouteEntry<Data> {
    let handler = () => Promise.resolve({ fetch, view })
    return { prefix, handler };
}

export function routeHandler<Data>(prefix: string, handler: RouteEntry['handler']): RouteEntry<Data> {
    return { prefix, handler };
}

let errorView: (route: string, prefix: string, error: string) => VNode;

errorView = (_r, _p, e: string) => preact.h("h1", { children: e }); // default dummy error view

export function setErrorView(
    pErrorView: (r: string, p: string, e: string) => VNode,
) {
    errorView = pErrorView;
}

export function setRoute(route: string) {
    // handle special case when view is trying to change the root
    // and it takes time but because view functions are called multiple times
    // it tries to set the same route again and again
    // TODO: perhaps setting route should block re-rendering until the route
    // data fetching is done
    if (_route_in_transition === route) {
        return
    }

    history.pushState({ ts: Date.now() }, "", route);
    onRouteChange();
}

export function replaceRoute(route: string) {
    history.replaceState({ ts: Date.now() }, "", route);
    onRouteChange();
}

export function getRoute(): string {
    return decodeURI(location.pathname) + location.search;
}

export type ParsedRoute = {
    pathname: string;
    searchParams: URLSearchParams;
};

export function parseRoute(route: string): ParsedRoute {
    let url = new URL(route, location.origin);
    return {
        pathname: url.pathname,
        searchParams: url.searchParams,
    };
}

export function getRouteParsed(): ParsedRoute {
    return parseRoute(getRoute());
}

let gRoutes: RouteEntry[] = [];

interface ModalEntry<T = any, R = any> {
    vm: T;
    resolve: ResolverFn<R>;
    view: ModalViewFn<T, R>;
    clickOutsideValue?: R;
}

let gModalStack: ModalEntry[] = [];

export function openModal<T = any, R = any>(
    vm: T,
    view: ModalViewFn<T, R>,
    clickOutsideValue?: R,
): Promise<R> {
    const result = new Promise<R>((resolve) => {
        const entryIndex = gModalStack.length;
        const resolveAndClose = (result: R): void => {
            resolve(result);
            gModalStack.splice(entryIndex, 1);
            scheduleRedraw();
        };
        const entry: ModalEntry<T, R> = {
            vm,
            resolve: resolveAndClose,
            view,
            clickOutsideValue,
        };
        gModalStack.push(entry);
        // Note: resolve will be called by the view
    });
    scheduleRedraw();
    return result;
}

export function closeTopModal() {
    let topIndex = gModalStack.length-1
    gModalStack.splice(topIndex, 1);
    scheduleRedraw();
}

let cleanupFunctions: Function[] = [];

// register a cleanup function to be called when a page changes
// for modules that can't be imported here due circularity
export function registerCleanupFunction(fn: Function) {
    cleanupFunctions.push(fn);
}

// state to helps us set/restore scroll position across navigations
let preNavStateId = 0;
let postNavStateId = 0;

let _block_router = false

export function blockAndReload(url: string = "/") {
    _block_router = true
    location.href = url
    // location.reload()
}

let _route_in_transition: string | null = null
async function onRouteChange() {
    if (_block_router) {
        return
    }
    if (_route_in_transition) {
        return
    }
    // console.log({preNavStateId, postNavStateId})
    const routes = gRoutes;
    let route = getRoute();
    let entry = routes.find(entry => route.startsWith(entry.prefix))
    if (!entry) {
        // TODO: set a 404 or something
        return
    }


    _route_in_transition = route
    storePageScroll(preNavStateId, window.scrollY);
    let handler = await entry.handler()
    let [data, error] = await handler.fetch(route, entry.prefix)
    _route_in_transition = null

    // reset page data ...
    cache.clearCache();
    refs.clearRefs();
    gModalStack.splice(0) // remove all items (reset slice)
    for (let fn of cleanupFunctions) {
        fn();
    }

    if (data) {
        setRootPage({
            route,
            prefix: entry.prefix,
            data: data,
            view: handler.view,
        });
    } else {
        setRootPage({
            route,
            prefix: entry.prefix,
            data: error,
            view: errorView,
        });
    }
    scheduleRedraw();

    let desiredYScroll = retrievePageScroll(postNavStateId);
    requestAnimationFrame(() => {
        window.scrollTo(0, desiredYScroll);
    });
}

let _scrolls = new Map<number, number>();
function storePageScroll(stateId: number, value: number) {
    _scrolls.set(stateId, value);
}

function retrievePageScroll(stateId: number) {
    return _scrolls.get(stateId) ?? 0;
}

// @ts-ignore
window._scrolls = _scrolls

function onPopState(event: PopStateEvent) {
    console.log("popstate:", event)
    if (event.state) {
        preNavStateId = postNavStateId;
        postNavStateId = event.state.ts;
    }

    onRouteChange()
}

export function initRoutes(routes: RouteEntry[]) {
    gRoutes = routes;
    window.addEventListener("popstate", onPopState);

    // initial state; we use 0 becuase that's the default value for preNavStateId
    history.replaceState({ ts: Date.now() }, "");
    history.scrollRestoration = "auto";

    // rewrite legacy urls
    if (window.location.pathname === '/' &&
        !window.location.search &&
        window.location.hash &&
        window.location.hash.startsWith('#/')) {
        var newPath = window.location.hash.slice(1);
        history.replaceState({ ts: Date.now() }, '', newPath);
    }

    // handle link clicks
    document.addEventListener('click', (event) => {
        if (!(event.target instanceof Element)) {
            return
        }
        const link = event.target.closest('a[href]');
        if (!link) {
            return
        }
        const href = link.getAttribute('href') ?? '';
        if (href.startsWith('/')) {
            event.preventDefault();
            let ts = Date.now()
            history.pushState({ ts }, '', href);
            preNavStateId = postNavStateId;
            postNavStateId = ts;

            onRouteChange();
        }
    });

    onRouteChange();
}

interface Queryable {
    [key: string]: number | string;
}

export function queryString(params: Queryable): string {
    const elements: string[] = [];
    for (const key in params) {
        const value = String(params[key]);
        elements.push(key + "=" + encodeURIComponent(value));
    }
    return elements.join("&");
}

export function classes(...names: Array<string | false | undefined>): string {
    let stringNames: string[] = [];
    for (let name of names) {
        if (typeof name === "string") stringNames.push(name);
    }

    return stringNames.join(" ");
}

export async function waitUntil(t: number) {
    let now = Date.now();
    let dur = t - now;
    if (dur <= 0) {
        return Promise.resolve();
    }
    return new Promise((resolve) => setTimeout(resolve, dur));
}

export function localStorageRead<T>(key: string): T | undefined {
    let raw = localStorage.getItem(key);
    if (raw === null) {
        return undefined;
    }
    try {
        return JSON.parse(raw);
    } catch {
        return undefined;
    }
}

export function localStorageSet<T>(key: string, value: T) {
    localStorage.setItem(key, JSON.stringify(value))
}

export function focusAfterFrame<T extends HTMLElement>(
    ref: refs.Ref<T | null>,
) {
    setTimeout(() => {
        let el = refs.get(ref);
        if (el) {
            el.focus();
            scheduleRedraw();
        }
    });
}

export function watchElementSize(ref: refs.Ref<HTMLElement | null>) {
    let height = elementRect(refs.get(ref)).height
    requestAnimationFrame(() => {
        let height2 = elementRect(refs.get(ref)).height
        if (height2 !== height) {
            scheduleRedraw()
        }
    })

}

// ====== i18n ======

export type LANG = string;

export const EN: LANG = "en";
export const AR: LANG = "ar";
export const JA: LANG = "ja";

export let lang = EN;

export function setLang(v: string) {
    lang = v;
}

export type LocalizedTextMap = [LANG, string][];

export function selectLocalizedTextByLang(map: LocalizedTextMap, lang: string) {
    for (let [lang0, text0] of map) {
        if (lang === lang0) {
            return text0;
        }
    }
    return map[0][1];
}

export function selectLocalizedText(map: LocalizedTextMap): string {
    return selectLocalizedTextByLang(map, lang);
}


// =================================================================
// ============     Gestures     ===================================
// =================================================================

type ITouch = {
    identifier: number
    clientX: number
    clientY: number

    radiusX: number
    radiusY: number
    rotationAngle: number

    force: number
}

type TouchData = {
    position: geom.Point
    radius: geom.Size
    force: number
}

function getTouchData(touch: ITouch): TouchData {
    return {
        position: { x: touch.clientX, y: touch.clientY },
        radius: { width: touch.radiusX, height: touch.radiusY },
        force: touch.force,
    }
}

// internal; for accumulating gesture data across frames
export type GestureInfo = {
    target: Element,

    // internal
    panId: number,
    zoomId0: number,
    zoomId1: number,

    // internal
    panPointPrev: geom.Point,
    zoomVecPrev: geom.Point, // the (x,y) delta between the two touch points
}

export type FrameGesture = {
    target: Element

    isPanning: boolean
    isZooming: boolean

    /** How much in absolute pixels the scroll position should change */
    panDelta: geom.Point,

    /** How much in absolute pixels did the distance between the touch points increase */
    zoomDelta: number
    /** The center point between the two touch points */
    zoomCenter: geom.Point
}

function zeroGesture(): GestureInfo {
    return {
        target: document.body,
        panId: -1,
        zoomId0: -1,
        zoomId1: -1,
        panPointPrev: geom.zeroPoint(),
        zoomVecPrev: geom.zeroPoint(),
    }
}

function zeroFrameGesture(): FrameGesture {
    return {
        target: document.body,
        isPanning: false,
        isZooming: false,
        panDelta: geom.zeroPoint(),
        zoomDelta: 0,
        zoomCenter: geom.zeroPoint(),
    }
}

export let _gesture = zeroGesture()
export let gesture = zeroFrameGesture()

function isTouchEvent(event: Event): event is TouchEvent {
    return (window['TouchEvent'] && event instanceof TouchEvent)
}

function touchClientPoint(t: Touch): geom.Point {
    return { x: t.clientX, y: t.clientY }
}

function getTouch(event: TouchEvent, touchId: number): Touch | null {
    return Array.from(event.touches).find(t => t.identifier === touchId) ?? null
}

function getZoomVecAndCenter(touch0: Touch, touch1: Touch): [geom.Point, geom.Point] {
    let p0 = touchClientPoint(touch0)
    let p1 = touchClientPoint(touch1)
    let vec = geom.pointMinus(p1, p0)
    let center = geom.pointMiddle(p0, p1)
    return [vec, center]
}


// Generated by Claude
function processFrameGesture(event: TouchEvent) {
    if (event.touches.length === 0) {
        _gesture = zeroGesture()
        return
    }

    gesture.target = _gesture.target

    // Handle touch start
    if (event.type === 'touchstart') {
        if (event.touches.length === 1) {
            _gesture.target = event.target as Element;
        }

        if (event.touches.length === 1) {
            let touch = event.touches.item(0)!
            _gesture.panId = touch.identifier
            _gesture.panPointPrev = touchClientPoint(touch)
            gesture.isPanning = true;
        } else if (event.touches.length === 2) {
            let touch0 = event.touches.item(0)!
            let touch1 = event.touches.item(1)!
            _gesture.zoomId0 = touch0.identifier
            _gesture.zoomId1 = touch1.identifier
            let [zoomVec, _center] = getZoomVecAndCenter(touch0, touch1)
            _gesture.zoomVecPrev = zoomVec
            gesture.isZooming = true;
        }
    }

    if (event.type === 'touchmove') {
        let pan = getTouch(event, _gesture.panId)
        let zoom0 = getTouch(event, _gesture.zoomId0)
        let zoom1 = getTouch(event, _gesture.zoomId1)

        if (pan) {
            let point = touchClientPoint(pan)
            gesture.isPanning = true
            gesture.panDelta = geom.pointMinus(point, _gesture.panPointPrev)
            _gesture.panPointPrev = point
        }
        if (zoom0 && zoom1) {
            gesture.isZooming = true
            let [zoomVec, zoomCenter] = getZoomVecAndCenter(zoom0, zoom1)
            const prevDist = geom.vectorLength(_gesture.zoomVecPrev)
            const currDist = geom.vectorLength(zoomVec)
            gesture.zoomDelta = currDist - prevDist
            gesture.zoomCenter = zoomCenter
            _gesture.zoomVecPrev = zoomVec;
        }
    }
}
