import type Point from '@mapbox/point-geometry';
import {DOM} from '../../util/dom.ts';
import type {Map} from '../map.ts';
import {type Handler, type HandlerResult} from '../handler_manager.ts';

/**
 * An options object sent to the enable function of some of the handlers
 */
export type AroundCenterOptions = {
    /**
     * If "center" is passed, map will zoom around the center of map
     */
    around: 'center';
};

/**
 * The `TwoFingersTouchHandler`s allows the user to zoom, pitch and rotate the map using two fingers
 *
 */
abstract class TwoFingersTouchHandler implements Handler {

    _enabled?: boolean;
    _active?: boolean;
    _firstTwoTouches?: [number, number];
    _vector?: Point;
    _startVector?: Point;
    _aroundCenter?: boolean;

    /** @internal */
    constructor() {
        this.reset();
    }

    reset(): void {
        this._active = false;
        delete this._firstTwoTouches;
    }

    abstract _start(points: [Point, Point]): void;
    abstract _move(points: [Point, Point], pinchAround: Point | null, e: TouchEvent): HandlerResult | void;

    touchstart(e: TouchEvent, points: Point[], mapTouches: Touch[]): void {
        if (this._firstTwoTouches || mapTouches.length < 2) return;

        this._firstTwoTouches = [
            mapTouches[0].identifier,
            mapTouches[1].identifier
        ];

        // implemented by child classes
        this._start([points[0], points[1]]);
    }

    touchmove(e: TouchEvent, points: Point[], mapTouches: Touch[]): HandlerResult | void {
        if (!this._firstTwoTouches) return;

        e.preventDefault();

        const [idA, idB] = this._firstTwoTouches;
        const a = getTouchById(mapTouches, points, idA);
        const b = getTouchById(mapTouches, points, idB);
        if (!a || !b) return;
        const pinchAround = this._aroundCenter ? null : a.add(b).div(2);

        // implemented by child classes
        return this._move([a, b], pinchAround, e);

    }

    touchend(e: TouchEvent, points: Point[], mapTouches: Touch[]): void {
        if (!this._firstTwoTouches) return;

        const [idA, idB] = this._firstTwoTouches;
        const a = getTouchById(mapTouches, points, idA);
        const b = getTouchById(mapTouches, points, idB);
        if (a && b) return;

        if (this._active) DOM.suppressClick();

        this.reset();
    }

    touchcancel(): void {
        this.reset();
    }

    /**
     * Enables the "drag to pitch" interaction.
     *
     * @example
     * ```ts
     * map.touchPitch.enable();
     * ```
     */
    enable(options?: AroundCenterOptions | boolean | null): void {
        this._enabled = true;
        this._aroundCenter = !!options && (options as AroundCenterOptions).around === 'center';
    }

    /**
     * Disables the "drag to pitch" interaction.
     *
     * @example
     * ```ts
     * map.touchPitch.disable();
     * ```
     */
    disable(): void {
        this._enabled = false;
        this.reset();
    }

    /**
     * Returns a Boolean indicating whether the "drag to pitch" interaction is enabled.
     *
     * @returns  `true` if the "drag to pitch" interaction is enabled.
     */
    isEnabled(): boolean {
        return !!this._enabled;
    }

    /**
     * Returns a Boolean indicating whether the "drag to pitch" interaction is active, i.e. currently being used.
     *
     * @returns `true` if the "drag to pitch" interaction is active.
     */
    isActive(): boolean {
        return !!this._active;
    }
}

function getTouchById(mapTouches: Touch[], points: Point[], identifier: number): Point | undefined {
    for (let i = 0; i < mapTouches.length; i++) {
        if (mapTouches[i].identifier === identifier) return points[i];
    }
    return undefined;
}

/* ZOOM */

const defaultZoomRate = 1;
const defaultZoomThreshold = 0.1;

function getZoomDelta(distance: number, lastDistance: number): number {
    return Math.log(distance / lastDistance) / Math.LN2;
}

/**
 * The `TwoFingersTouchHandler`s allows the user to zoom the map two fingers
 *
 * @group Handlers
 */
export class TwoFingersTouchZoomHandler extends TwoFingersTouchHandler {

    _distance?: number;
    _startDistance?: number;
    _zoomRate: number;
    _zoomThreshold: number;

    constructor() {
        super();
        this._zoomRate = defaultZoomRate;
        this._zoomThreshold = defaultZoomThreshold;
    }

    /**
     * Sets the zoom rate of touch gestures.
     * @param zoomRate - 1 The rate used to scale touch movement to a zoom value. Set to `undefined` to restore the default.
     * @example
     * Slow down touch zoom
     * ```ts
     * map.touchZoomRotate.setZoomRate(0.5);
     * ```
     */
    setZoomRate(zoomRate?: number): void {
        this._zoomRate = zoomRate ?? defaultZoomRate;
    }

    /**
     * Sets the threshold before a pinch gesture starts zooming.
     * @param zoomThreshold - 0.1 The minimum zoom delta before the pinch gesture becomes active. Set to `undefined` to restore the default.
     * @example
     * Make pinch zoom less sensitive
     * ```ts
     * map.touchZoomRotate.setZoomThreshold(0.3);
     * ```
     */
    setZoomThreshold(zoomThreshold?: number): void {
        this._zoomThreshold = zoomThreshold ?? defaultZoomThreshold;
    }

    reset(): void {
        super.reset();
        delete this._distance;
        delete this._startDistance;
    }

    _start(points: [Point, Point]): void {
        this._startDistance = this._distance = points[0].dist(points[1]);
    }

    _move(points: [Point, Point], pinchAround: Point | null): HandlerResult | void {
        const lastDistance = this._distance;
        this._distance = points[0].dist(points[1]);
        if (!this._active && Math.abs(getZoomDelta(this._distance, this._startDistance)) < this._zoomThreshold) return;
        this._active = true;
        return {
            zoomDelta: getZoomDelta(this._distance, lastDistance) * this._zoomRate,
            pinchAround
        };
    }
}

/* ROTATE */

const ROTATION_THRESHOLD = 25; // pixels along circumference of touch circle

function getBearingDelta(a: Point, b: Point): number {
    return a.angleWith(b) * 180 / Math.PI;
}

/**
 * The `TwoFingersTouchHandler`s allows the user to rotate the map two fingers
 *
 * @group Handlers
 */
export class TwoFingersTouchRotateHandler extends TwoFingersTouchHandler {
    _minDiameter?: number;

    reset(): void {
        super.reset();
        delete this._minDiameter;
        delete this._startVector;
        delete this._vector;
    }

    _start(points: [Point, Point]): void {
        this._startVector = this._vector = points[0].sub(points[1]);
        this._minDiameter = points[0].dist(points[1]);
    }

    _move(points: [Point, Point], pinchAround: Point | null, _e: TouchEvent): HandlerResult | void {
        const lastVector = this._vector;
        this._vector = points[0].sub(points[1]);

        if (!this._active && this._isBelowThreshold(this._vector)) return;
        this._active = true;

        return {
            bearingDelta: getBearingDelta(this._vector, lastVector),
            pinchAround
        };
    }

    _isBelowThreshold(vector: Point): boolean {
        /*
         * The threshold before a rotation actually happens is configured in
         * pixels along the circumference of the circle formed by the two fingers.
         * This makes the threshold in degrees larger when the fingers are close
         * together and smaller when the fingers are far apart.
         *
         * Use the smallest diameter from the whole gesture to reduce sensitivity
         * when pinching in and out.
         */

        this._minDiameter = Math.min(this._minDiameter, vector.mag());
        const circumference = Math.PI * this._minDiameter;
        const threshold = ROTATION_THRESHOLD / circumference * 360;

        const bearingDeltaSinceStart = getBearingDelta(vector, this._startVector);
        return Math.abs(bearingDeltaSinceStart) < threshold;
    }
}

/* PITCH */

function isVertical(vector: Point): boolean {
    return Math.abs(vector.y) > Math.abs(vector.x);
}

const ALLOWED_SINGLE_TOUCH_TIME = 100;

/**
 * The `TwoFingersTouchPitchHandler` allows the user to pitch the map by dragging up and down with two fingers.
 *
 * @group Handlers
 */
export class TwoFingersTouchPitchHandler extends TwoFingersTouchHandler {

    _valid?: boolean;
    _firstMove?: number;
    _lastPoints?: [Point, Point];
    _map: Map;
    _currentTouchCount: number = 0;

    constructor(map: Map) {
        super();
        this._map = map;
    }

    reset(): void {
        super.reset();
        this._valid = undefined;
        delete this._firstMove;
        delete this._lastPoints;
    }

    touchstart(e: TouchEvent, points: Point[], mapTouches: Touch[]): void {
        super.touchstart(e, points, mapTouches);
        this._currentTouchCount = mapTouches.length;
    }

    _start(points: [Point, Point]): void {
        this._lastPoints = points;
        if (isVertical(points[0].sub(points[1]))) {
            // fingers are more horizontal than vertical
            this._valid = false;
        }
    }

    _move(points: [Point, Point], center: Point | null, e: TouchEvent): HandlerResult | void {
        // If cooperative gestures is enabled, we need a 3-finger minimum for this gesture to register
        if (this._map.cooperativeGestures.isEnabled() && this._currentTouchCount < 3) {
            return;
        }

        const vectorA = points[0].sub(this._lastPoints[0]);
        const vectorB = points[1].sub(this._lastPoints[1]);

        this._valid = this.gestureBeginsVertically(vectorA, vectorB, e.timeStamp);
        if (!this._valid) return;

        this._lastPoints = points;
        this._active = true;
        const yDeltaAverage = (vectorA.y + vectorB.y) / 2;
        const degreesPerPixelMoved = -0.5;
        return {
            pitchDelta: yDeltaAverage * degreesPerPixelMoved
        };
    }

    gestureBeginsVertically(vectorA: Point, vectorB: Point, timeStamp: number): boolean | undefined {
        if (this._valid !== undefined) return this._valid;

        const threshold = 2;
        const movedA = vectorA.mag() >= threshold;
        const movedB = vectorB.mag() >= threshold;

        // neither finger has moved a meaningful amount, wait
        if (!movedA && !movedB) return;

        // One finger has moved and the other has not.
        // If enough time has passed, decide it is not a pitch.
        if (!movedA || !movedB) {
            if (this._firstMove === undefined) {
                this._firstMove = timeStamp;
            }

            if (timeStamp - this._firstMove < ALLOWED_SINGLE_TOUCH_TIME) {
                // still waiting for a movement from the second finger
                return undefined;
            } else {
                return false;
            }
        }

        const isSameDirection = vectorA.y > 0 === vectorB.y > 0;
        return isVertical(vectorA) && isVertical(vectorB) && isSameDirection;
    }
}
