import { EventDispatcher, HtmlFocusUtils, EventUtils } from "../../Utils";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { EvtUtils } from "@devexpress/utils/lib/utils/evt";
import { SetAbsoluteX, SetAbsoluteY } from "../../Utils/Data";
import { DomUtils } from "@devexpress/utils/lib/utils/dom";
import { IReadOnlyChangesListener } from "../../Settings";
import { IShapeDescriptionManager } from "../../Model/Shapes/Descriptions/ShapeDescriptionManager";
import { Browser } from "@devexpress/utils/lib/browser";
import { RenderHelper } from "../RenderHelper";
import { NOT_VALID_CSSCLASS } from "../CanvasItemsManager";
import { ITextMeasurer } from "../Measurer/ITextMeasurer";

export interface IToolboxDragListener {
    notifyToolboxDragStart(evt: MouseEvent);
    notifyToolboxDragEnd(evt: MouseEvent);
    notifyToolboxDraggingMouseMove(evt: MouseEvent);
}

export interface IToolboxClickListener {
    notifyToolboxClick(shapeType: string);
}

const TOOLBOX_CSSCLASS = "dxdi-toolbox";
const DRAG_CAPTURED_CSSCLASS = "dxdi-tb-drag-captured";
const START_DRAG_CSSCLASS = "dxdi-tb-start-drag-flag";
const TOUCH_DRAGTIMEOUT_MS = 300;

export abstract class Toolbox implements IReadOnlyChangesListener {
    private dragStartPoint: Point;
    private dragStartShapeType: string;
    private mouseDownShapeType: string;
    private touchDownPoint: Point; 
    measurer: ITextMeasurer;
    private mainElement: HTMLDivElement;

    private mouseDownEventName: string;
    private mouseMoveEventName: string;
    private mouseUpEventName: string;

    private onElementMouseDownHandler: any;
    private onElementMouseUpHandler: any;
    private onMouseDownHandler: any;
    private onMouseMoveHandler: any;
    private onMouseUpHandler: any;
    private onDragStartHandler: any;
    private onTouchMoveHandler: any;
    private onContextMenuHandler: any;

    private dragPrepareTimeout: number = -1;
    private dragPrepareEvent: MouseEvent;

    protected dragState: number = DragState.None;
    private emulateDragEvents = Browser.WebKitTouchUI;

    draggingObject: ToolboxDraggingObject;
    onDragOperation: EventDispatcher<IToolboxDragListener> = new EventDispatcher();
    onClickOperation: EventDispatcher<IToolboxClickListener> = new EventDispatcher();

    constructor(parent: HTMLElement,
        protected readOnly: boolean,
        protected allowDragging: boolean,
        protected shapeDescriptionManager: IShapeDescriptionManager,
        protected shapeTypes: string[],
        protected getAllowedShapeTypes: (shapes: string[]) => string[]) {
        if(!parent) return;

        this.mainElement = this.createMainElement(parent);
        this.attachHandlers(this.mainElement);
    }

    clean(removeElement?: (element: HTMLElement) => void) {
        this.detachHandlers(this.mainElement);
        if(removeElement)
            removeElement(this.mainElement);
    }

    private createMainElement(parent: HTMLElement): HTMLDivElement {
        const element = document.createElement("div");
        element.classList.add(TOOLBOX_CSSCLASS);
        if(Browser.AndroidMobilePlatform)
            element.classList.add("dxdi-nodrag");
        element.draggable = true;
        if(this.emulateDragEvents)
            element.tabIndex = 0;
        parent.appendChild(element);
        return element;
    }
    private attachHandlers(element: HTMLElement) {
        this.onElementMouseDownHandler = this.onElementMouseDown.bind(this);
        this.onElementMouseUpHandler = this.onElementMouseUp.bind(this);
        this.onMouseDownHandler = this.onMouseDown.bind(this);
        this.onMouseMoveHandler = this.onMouseMove.bind(this);
        this.onMouseUpHandler = this.onMouseUp.bind(this);
        this.onContextMenuHandler = this.onContextMenu.bind(this);

        this.onDragStartHandler = this.onDragStart.bind(this);
        this.onTouchMoveHandler = this.onTouchMove.bind(this);

        if(!this.emulateDragEvents)
            RenderHelper.addEventListener(element, "dragstart", this.onDragStartHandler);

        if(EventUtils.isPointerEvents()) {
            this.mouseDownEventName = "pointerdown";
            this.mouseMoveEventName = "pointermove";
            this.mouseUpEventName = "pointerup";
        }
        else {
            this.mouseDownEventName = Browser.TouchUI ? "touchstart" : "mousedown";
            this.mouseMoveEventName = Browser.TouchUI ? "touchmove" : "mousemove";
            this.mouseUpEventName = Browser.TouchUI ? "touchend" : "mouseup";
        }

        RenderHelper.addEventListener(element, "touchmove", this.onTouchMoveHandler);
        RenderHelper.addEventListener(element, this.mouseDownEventName, this.onElementMouseDownHandler);
        RenderHelper.addEventListener(element, this.mouseUpEventName, this.onElementMouseUpHandler);
        RenderHelper.addEventListener(element, this.mouseDownEventName, this.onMouseDownHandler);
        RenderHelper.addEventListener(document, this.mouseMoveEventName, this.onMouseMoveHandler);
        RenderHelper.addEventListener(document, this.mouseUpEventName, this.onMouseUpHandler);
        RenderHelper.addEventListener(element, "contextmenu", this.onContextMenuHandler);
    }
    private detachHandlers(element: HTMLElement) {
        if(!this.emulateDragEvents)
            RenderHelper.removeEventListener(element, "dragstart", this.onDragStartHandler);
        RenderHelper.removeEventListener(element, "touchmove", this.onTouchMoveHandler);
        RenderHelper.removeEventListener(element, this.mouseDownEventName, this.onElementMouseDownHandler);
        RenderHelper.removeEventListener(element, this.mouseUpEventName, this.onElementMouseUpHandler);
        RenderHelper.removeEventListener(element, this.mouseDownEventName, this.onMouseDownHandler);
        RenderHelper.removeEventListener(document, this.mouseMoveEventName, this.onMouseMoveHandler);
        RenderHelper.removeEventListener(document, this.mouseUpEventName, this.onMouseUpHandler);
        RenderHelper.removeEventListener(element, "contextmenu", this.onContextMenuHandler);
    }

    render(filter?: (shapeType: string) => boolean): boolean {
        if(this.mainElement.childNodes)
            this.mainElement.innerHTML = "";
        let shapeTypes = this.shapeTypes;
        shapeTypes = this.getAllowedShapeTypes ? this.getAllowedShapeTypes(shapeTypes) : shapeTypes;
        shapeTypes = filter ? shapeTypes.filter(filter) : shapeTypes;
        if(shapeTypes.length)
            this.createElements(this.mainElement, shapeTypes);
        return !!shapeTypes.length;
    }

    protected abstract createElements(element: HTMLElement, shapeTypes: string[]);

    protected createDraggingObject(shapeType: string): ToolboxDraggingObject {
        const evt = new DiagramDraggingEvent();
        evt.data = shapeType;
        evt.onFinishDragging = this.resetDragState.bind(this);
        evt.onCaptured = this.capture.bind(this);
        return new ToolboxDraggingObject(evt);
    }

    protected getDragShapeType(element: Element): string {
        while(element && !DomUtils.hasClassName(element, TOOLBOX_CSSCLASS)) {
            if(element.getAttribute && element.getAttribute("data-tb-type"))
                return element.getAttribute("data-tb-type");
            element = <Element>element.parentNode;
        }
        return undefined;
    }
    getTouchPointFromEvent(evt: MouseEvent): Point {
        let touchPosition;
        const touches = evt["touches"];
        if(touches && touches.length > 0)
            touchPosition = new Point(touches[0].clientX, touches[0].clientY);
        else if(evt.clientX && evt.clientY)
            touchPosition = new Point(evt.clientX, evt.clientY);
        return touchPosition;
    }
    private onElementMouseDown(evt: MouseEvent) {
        this.mouseDownShapeType = this.getDragShapeType(EvtUtils.getEventSource(evt));
        this.touchDownPoint = this.getTouchPointFromEvent(evt);
    }
    private onElementMouseUp(evt: MouseEvent) {
        const shapeType = this.getDragShapeType(EvtUtils.getEventSource(evt));
        if(shapeType && shapeType === this.mouseDownShapeType)
            this.onClickOperation.raise("notifyToolboxClick", shapeType);
        this.mouseDownShapeType = undefined;
        this.touchDownPoint = undefined;
    }
    private onMouseDown(evt: MouseEvent) {
        this.setDragState(DragState.Prepare, evt);
        if(Browser.TouchUI && EventUtils.isMousePointer(evt))
            this.setDragState(DragState.Start, evt);
    }
    private onDragStart(evt: MouseEvent) {
        this.setDragState(DragState.Start, evt);
        evt.preventDefault();
    }
    private onTouchMove(evt: TouchEvent) {
        if(this.draggingObject)
            evt.preventDefault();
    }

    isLeftButtonPressed(evt: Event) {
        return EvtUtils.isLeftButtonPressed(evt) ||
            (evt.type === "pointermove" && Browser.TouchUI && Browser.MacOSMobilePlatform && EventUtils.isMousePointer(evt));
    }
    private onContextMenu(evt: MouseEvent) {
        if(this.dragState !== DragState.None)
            evt.preventDefault();
    }
    private onMouseMove(evt: MouseEvent) {
        if(Browser.TouchUI && Browser.MacOSMobilePlatform) {
            const currentTouchPoint = this.getTouchPointFromEvent(evt);
            if(this.touchDownPoint && currentTouchPoint && this.touchDownPoint.x === currentTouchPoint.x && this.touchDownPoint.y === currentTouchPoint.y)
                return;
        }

        if(this.dragState === DragState.Prepare && EventUtils.isTouchEvent(evt)) {
            this.setDragState(DragState.Start, evt);
            return; 
        }

        this.setDragState(this.isLeftButtonPressed(evt) ? DragState.Dragging : DragState.None, evt);
        if(EventUtils.isPointerEvents())
            this.raiseDraggingMouseMove(evt);
    }
    private onMouseUp(evt: MouseEvent) {
        this.setDragState(DragState.None, evt);
    }
    private updateDraggingElementPosition(evtX: number, evtY: number) {
        const element = this.draggingObject.element;
        const xPos = evtX - element.offsetWidth / 2;
        const yPos = evtY - element.offsetHeight / 2;
        SetAbsoluteX(element, xPos);
        SetAbsoluteY(element, yPos);
    }
    protected setDragState(newState: DragState, evt: MouseEvent) {
        if(this.readOnly || !this.allowDragging)
            return;
        if(newState === DragState.None && newState === this.dragState)
            return;
        if(this.dragPrepareTimeout > -1) {
            clearTimeout(this.dragPrepareTimeout);
            this.dragPrepareTimeout = -1;
            this.dragPrepareEvent = undefined;
        }
        if(newState - this.dragState > 1 || newState !== DragState.None && newState < this.dragState)
            return;
        this.dragState = newState;
        switch(newState) {
            case DragState.Prepare:
                if(!this.prepareDragging(evt))
                    this.setDragState(DragState.None, evt);
                if(this.emulateDragEvents || !EventUtils.isMousePointer(evt)) {
                    this.dragPrepareTimeout = setTimeout(this.onDragPrepareTimeout.bind(this), TOUCH_DRAGTIMEOUT_MS);
                    this.dragPrepareEvent = evt;
                }
                break;
            case DragState.Start:
                DomUtils.addClassName(document.body, "dxdi-dragging");
                this.startDragging(evt);
                break;
            case DragState.Dragging:
                this.doDragging(evt);
                break;
            case DragState.None:
                this.finishDragging(evt);
                break;
        }
    }
    private resetDragState() {
        this.setDragState(DragState.None, undefined);
    }
    onDragPrepareTimeout() {
        this.dragPrepareTimeout = -1;
        if(this.dragState === DragState.Prepare)
            this.setDragState(DragState.Start, this.dragPrepareEvent);
        this.dragPrepareEvent = undefined;
    }
    prepareDragging(evt: MouseEvent): boolean {
        this.dragStartPoint = new Point(EvtUtils.getEventX(evt), EvtUtils.getEventY(evt));
        this.dragStartShapeType = this.getDragShapeType(EvtUtils.getEventSource(evt));
        if(EventUtils.isMousePointer(evt))
            DomUtils.addClassName(this.mainElement, START_DRAG_CSSCLASS);
        if(this.emulateDragEvents || !EventUtils.isMousePointer(evt))
            HtmlFocusUtils.focusWithPreventScroll(this.mainElement);
        return !!this.dragStartShapeType;
    }
    startDragging(evt: MouseEvent) {
        this.draggingObject = this.createDraggingObject(this.dragStartShapeType);
        if(this.dragStartShapeType) {
            this.raiseDragStart(evt);
            this.draggingObject.element = this.createDraggingElement(this.draggingObject);
            if(this.draggingObject.captured !== undefined)
                this.capture(this.draggingObject.captured, true);
            this.updateDraggingElementPosition(this.dragStartPoint.x, this.dragStartPoint.y);
        }
        else
            DomUtils.addClassName(document.body, NOT_VALID_CSSCLASS);
    }
    doDragging(evt: MouseEvent) {
        if(this.draggingObject.element)
            this.updateDraggingElementPosition(EvtUtils.getEventX(evt), EvtUtils.getEventY(evt));
    }
    finishDragging(evt: MouseEvent) {
        if(this.draggingObject) {
            this.raiseDragEnd(evt);

            const element = this.draggingObject.element;
            if(element)
                element.parentNode.removeChild(element);
            delete this.draggingObject;
        }
        this.dragStartPoint = undefined;
        this.dragStartShapeType = undefined;
        DomUtils.removeClassName(this.mainElement, START_DRAG_CSSCLASS);
        DomUtils.removeClassName(document.body, NOT_VALID_CSSCLASS);
        setTimeout(() => DomUtils.removeClassName(document.body, "dxdi-dragging"), 500); 
    }

    capture(captured: boolean, forced?: boolean) {
        if(this.draggingObject && (this.draggingObject.captured !== captured || forced)) {
            this.draggingObject.captured = captured;
            if(this.draggingObject.element)
                DomUtils.toggleClassName(this.draggingObject.element, DRAG_CAPTURED_CSSCLASS, captured);
        }
    }
    protected abstract createDraggingElement(dragginObject: ToolboxDraggingObject): HTMLElement;

    raiseDragStart(evt: MouseEvent) {
        this.onDragOperation.raise("notifyToolboxDragStart", evt);
    }
    raiseDragEnd(evt: MouseEvent) {
        this.onDragOperation.raise("notifyToolboxDragEnd", evt); 
    }
    raiseDraggingMouseMove(evt: MouseEvent) {
        this.onDragOperation.raise("notifyToolboxDraggingMouseMove", evt);
    }

    notifyReadOnlyChanged(readOnly: boolean) {
        this.readOnly = readOnly;
    }
}

enum DragState {
    None = -1,
    Prepare = 0,
    Start = 1,
    Dragging = 2
}

export class ToolboxDraggingObject {
    constructor(evt: DiagramDraggingEvent) {
        this.evt = evt;
    }
    element: HTMLElement;
    evt: DiagramDraggingEvent;
    captured: boolean;
}

export class DiagramDraggingEvent {
    data: string;
    onFinishDragging: () => void;
    onCaptured: (captured: boolean) => void;
}

export interface IShapeToolboxOptions { }
