// Copyright (c) 2022 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import React, { TouchEvent, PointerEvent, SyntheticEvent } from 'react';import { Theme } from '@mui/material/styles';
import { WithStyles } from '@mui/styles';
import withStyles from '@mui/styles/withStyles';
import createStyles from '@mui/styles/createStyles';
import { PiPedalModel, PiPedalModelFactory, ZoomedControlInfo } from './PiPedalModel';

const SELECTED_OPACITY = 0.8;
const DEFAULT_OPACITY = 0.6;
const FINE_RANGE_SCALE = 0.1; //
const ULTRA_FINE_RANGE_SCALE = 0.02; // 12000 pixels to move from 0 to 1.

const MIN_ANGLE = -135;
const MAX_ANGLE = 135;
const DEAD_ZONE_SIZE = 5;


const styles = (theme: Theme) => createStyles({
});


interface ZoomedDialProps extends WithStyles<typeof styles> {
    theme: Theme,
    size: number,

    controlInfo: ZoomedControlInfo | undefined,

    onPreviewValue(value: number): void,
    onSetValue(value: number): void
}

interface ZoomedDialState {

}

const ZoomedDial = withStyles(styles, { withTheme: true })(

    class extends React.Component<ZoomedDialProps, ZoomedDialState> {

        model: PiPedalModel = PiPedalModelFactory.getInstance();
        imgRef: React.RefObject<HTMLImageElement> = React.createRef();

        constructor(props: ZoomedDialProps) {
            super(props);
            this.state = {

            };

            this.onTouchStart = this.onTouchStart.bind(this);
            this.onTouchMove = this.onTouchMove.bind(this);
            this.onBodyPointerDownCapture = this.onBodyPointerDownCapture.bind(this);

            this.onPointerDown = this.onPointerDown.bind(this);
            this.onPointerUp = this.onPointerUp.bind(this);
            this.onPointerMove = this.onPointerMove.bind(this);
            this.onPointerLostCapture = this.onPointerLostCapture.bind(this);

        }

        onPointerUp(e: PointerEvent<HTMLImageElement>) {

            if (this.isCapturedPointer(e)) {
                --this.pointersDown;
                

                e.preventDefault();
                this.updateAngle(e);
                this.commitAngle();

                this.releaseCapture(e);

            } else {
                --this.pointersDown;
                
            }
        }


        releaseCapture(e: PointerEvent<HTMLImageElement>)
        {
            let img = this.imgRef.current;
            
            if (img && img.style) {
                img.releasePointerCapture(e.pointerId);
                img.style.opacity = "" + DEFAULT_OPACITY;
                // they get automaticlly released.
                // for (let i = 0; i < this.capturedPointers.length; ++i)
                // {
                //     img.releasePointerCapture (this.capturedPointers[i]);
                // }
                this.capturedPointers = [];
            }
            document.body.removeEventListener(
                "pointerdown",
                this.onBodyPointerDownCapture,true
                );

            this.mouseDown = false;

        }


        onPointerLostCapture(e: PointerEvent<HTMLImageElement>) {
            if (this.isCapturedPointer(e)) {
                --this.pointersDown;
                

                this.releaseCapture(e);
            }

        }

        onTouchStart(e: TouchEvent<HTMLImageElement>) {
            //must be defined to get onTouchMove
        }
        onTouchMove(e: TouchEvent<HTMLImageElement>) {
            // e.preventDefault();
            e.stopPropagation(); // cancels scroll!!!

        }
        capturedPointers: number[] = [];

        isExtraTouch(e: PointerEvent): boolean {
            return (this.mouseDown && this.pointerType === "touch" && e.pointerType === "touch" && e.pointerId !== this.pointerId);
        }

        mouseDown: boolean = false;
        pointerId: number = -1;
        pointerType: string = "";
        startX: number = 0;
        startY: number = 0;
        currentAngle: number = 0;
        lastPointerAngle: number = 0;

        isValidPointer(e: PointerEvent<HTMLImageElement>): boolean {
            if (e.pointerType === "mouse") {
                return e.button === 0;
            } else if (e.pointerType === "pen") {
                return true;
            } else if (e.pointerType === "touch") {
                return true;
            }
            return false;
        }

        defaultValue: number = 0;

        getCurrentValue(): number {
            if (this.props.controlInfo == null) {
                return this.defaultValue;
            } else {
                let uiControl = this.props.controlInfo.uiControl;
                let instanceId = this.props.controlInfo.instanceId;
                if (instanceId === -1) return 0;
                let pedalBoardItem = this.model.pedalBoard.get()?.getItem(instanceId);
                let value: number = pedalBoardItem?.getControlValue(uiControl.symbol) ?? 0;
                this.defaultValue = value;
                return value;
            }
        }

        angleToValue(angle: number): number {
            if (this.props.controlInfo) {
                if (angle > MAX_ANGLE) angle = MAX_ANGLE;
                if (angle < MIN_ANGLE) angle = MIN_ANGLE;

                let r = (angle - MIN_ANGLE) / (MAX_ANGLE - MIN_ANGLE);

                let uiControl = this.props.controlInfo.uiControl;

                return r * (uiControl.max_value-uiControl.min_value)+uiControl.min_value;
            }
            return 0;
        }

        valueToAngle(value: number): number {
            if (this.props.controlInfo) {
                let uiControl = this.props.controlInfo.uiControl;
                let range = (value - uiControl.min_value) / (uiControl.max_value - uiControl.min_value);
                if (range < 0) range = 0;
                if (range > 1) range = 1;
                return (MAX_ANGLE - MIN_ANGLE) * range + MIN_ANGLE;
            }
            return 0;
        }

        pointerToAngle(e: PointerEvent<HTMLImageElement>): number {

            if (this.imgRef.current) {
                let img = this.imgRef.current;
                let rc = img.getBoundingClientRect();
                let x = e.clientX - (rc.left+ rc.width / 2);
                let y = e.clientY - (rc.top + rc.height / 2);

                // dead zone in center of image. 

                if (x*x+y*y < DEAD_ZONE_SIZE*DEAD_ZONE_SIZE) return NaN;

                let angle = -Math.atan2(-y, x) * 180 / Math.PI + 90;

                return angle;
            }
            return NaN;
        }
        captureElement?: HTMLElement;
        pointersDown: number = 0;

        onPointerDown(e: PointerEvent<HTMLImageElement>): void {
            if (!this.mouseDown && this.isValidPointer(e)) {
                e.preventDefault();
                e.stopPropagation();



                this.mouseDown = true;
                this.pointersDown = 1;
                this.pointerId = e.pointerId;
                this.pointerType = e.pointerType;
                this.startX = e.clientX;
                this.startY = e.clientY;
                this.currentAngle = this.valueToAngle(this.getCurrentValue());
                this.lastPointerAngle = this.pointerToAngle(e);

                let img = this.imgRef.current;

                if (img) {
                    this.captureElement = img;
                    document.body.addEventListener(
                        "pointerdown",
                        this.onBodyPointerDownCapture, true
                    );

                    img.setPointerCapture(e.pointerId);
                    if (img.style) {
                        img.style.opacity = "" + SELECTED_OPACITY;
                    }
                }

            } else {
                if (this.isExtraTouch(e)) {
                    ++this.pointersDown;

                }
            }
        }

        isCapturedPointer(e: PointerEvent<HTMLImageElement>): boolean {
            return this.mouseDown
                && e.pointerId === this.pointerId
                && e.pointerType === this.pointerType;

        }



        onPointerMove(e: PointerEvent<HTMLImageElement>): void {
            if (this.isCapturedPointer(e)) {
                e.preventDefault();
                this.updateAngle(e)
            }
        }


        commitAngle() {
            let previewValue = this.angleToValue(this.currentAngle);
            this.props.onSetValue(previewValue);
            this.defaultValue = previewValue;


        }
        updateAngle(e: PointerEvent<HTMLImageElement>): void {
            let angle: number = this.pointerToAngle(e);
            if (!isNaN(angle)) {
                if (!isNaN(this.lastPointerAngle)) {
                    let dAngle = angle-this.lastPointerAngle;
                    while (dAngle > 180) dAngle -= 360;
                    while (dAngle < -180) dAngle += 360;

                    let scale = 1.0;
                    if (this.pointersDown === 2)
                    {
                        scale = FINE_RANGE_SCALE;
                    } else if (this.pointersDown >= 3)
                    {
                        scale = ULTRA_FINE_RANGE_SCALE;
                    }

                    this.currentAngle += dAngle*scale;
                    let previewValue = this.angleToValue(this.currentAngle);
                    this.previewValue(previewValue);
                    this.props.onPreviewValue(previewValue);
                }
                this.lastPointerAngle = angle;
            }
        }
        makeRotationTransform(angle: number): string
        {
            return "rotate(" + angle + "deg)";
        }

        getDefaultRotationTransform(): string
        {
            let v = this.getCurrentValue();
            let a = this.valueToAngle(v);
            return this.makeRotationTransform(a);
        }
        previewValue(value: number): void
        {
            if (this.imgRef.current)
            {
                let img = this.imgRef.current;
                let angle = this.valueToAngle(value);
                img.style.transform = this.makeRotationTransform(angle);
            }

        }


        onBodyPointerDownCapture(e_: any): any {
            let e = e_ as PointerEvent;
            if (this.isExtraTouch(e)) {
                this.captureElement!.setPointerCapture(e.pointerId);
                this.capturedPointers.push(e.pointerId);
                ++this.pointersDown;
            }

        }
        onDrag(e: SyntheticEvent) {
            e.preventDefault();
            e.stopPropagation();
            return false;
        }



        render() {
            return (
                <img  ref={this.imgRef} src="img/fx_dial.svg" alt=""
                    style={{
                        overscrollBehavior: "none", touchAction: "none", 
                        width: this.props.size, height: this.props.size, opacity: DEFAULT_OPACITY,
                         transform: this.getDefaultRotationTransform()
                    }}
                    draggable="true"
                    onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
                    onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp}
                    onPointerMoveCapture={this.onPointerMove} onDrag={this.onDrag}
                />
            );
        }
    }
);

export default ZoomedDial;
