import { KeyUtils } from "@devexpress/utils/lib/utils/key";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { TextStyle } from "../Model/Style";
import { DomUtils } from "@devexpress/utils/lib/utils/dom";
import { RenderUtils, raiseEvent } from "./Utils";
import { ITextInputOperationListener, DiagramKeyboardEvent, DiagramFocusEvent, DiagramClipboardEvent } from "../Events/Event";
import { DiagramItem } from "../Model/DiagramItem";
import { Shape } from "../Model/Shapes/Shape";
import { Connector } from "../Model/Connectors/Connector";
import { EvtUtils } from "@devexpress/utils/lib/utils/evt";
import { IEventManager } from "../Events/EventManager";

import { ILayoutPointResolver } from "./CanvasItemsManager";
import { ICanvasViewListener } from "./CanvasViewManager";
import { TextAngle } from "./Primitives/TextPrimitive";
import { ITextMeasurer, TextOwner } from "./Measurer/ITextMeasurer";
import { Browser } from "@devexpress/utils/lib/browser";
import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter";
import { RenderHelper } from "./RenderHelper";
import { getTextHeight, getLineHeight, textToParagraphs } from "../Utils/TextUtils";
import { HtmlFocusUtils } from "../Utils";

const TEXT_INPUT_CSSCLASS = "dxdi-text-input";

export class InputManager implements ITextInputOperationListener, ICanvasViewListener {
    textInputElementContainer: HTMLDivElement;
    mouseWheelHandler: (evt: WheelEvent) => void;

    private inputElement: HTMLTextAreaElement;
    private textInputElement: HTMLTextAreaElement;
    private clipboardInputElement: HTMLTextAreaElement;
    private focused = false;
    private focusLocked: boolean = false;

    private savedTextInputPosition: Point;
    private savedTextInputSize: Size;
    private savedTextInputAngle: TextAngle;
    private savedTextInputStyle: TextStyle;

    private onInputBlurHandler: any;
    private onInputFocusHandler: any;
    private onInputKeyDownHandler: any;
    private onInputKeyPressHandler: any;
    private onInputKeyUpHandler: any;
    private onTextInputMouseWheelHandler: any;
    private onTextInputMouseUpHandler: any;
    private onTextInputBlurHandler: any;
    private onTextInputFocusHandler: any;
    private onTextInputKeyDownHandler: any;
    private onTextInputKeyUpHandler: any;
    private onTextInputChangeHandler: any;
    private onPasteHandler: any;

    constructor(
        private mainElement: HTMLElement,
        private layoutPointResolver: ILayoutPointResolver,
        private eventManager: IEventManager,
        private textMeasurer: ITextMeasurer,
        public actualZoom: number,
        private focusElementsParent?: HTMLElement
    ) {
        this.createInputElements(this.mainElement, this.focusElementsParent);
    }
    detachEvents() {
        this.detachInputElementEvents();
        this.detachTextInputElementEvents();
    }

    isFocused() {
        return this.focused;
    }
    captureFocus(keepTextInputFocused?: boolean) {
        if(keepTextInputFocused && document.activeElement === this.textInputElement)
            HtmlFocusUtils.focusWithPreventScroll(this.textInputElement || this.inputElement);
        else
            HtmlFocusUtils.focusWithPreventScroll(this.inputElement);
    }
    clear() {
        this.setInputElementFocusHandlerMode(false);
    }

    setClipboardData(data: string) {
        this.clipboardInputElement.value = data;
        HtmlFocusUtils.focusWithPreventScroll(this.clipboardInputElement);
        this.clipboardInputElement.select();
        document.execCommand("copy");
        this.captureFocus();
    }
    getClipboardData(callback: (data: string) => void) {
        if(navigator && navigator["clipboard"])
            navigator["clipboard"].readText().then(clipText => {
                callback(clipText);
                this.captureFocus();
            }).catch(() => {
                callback("");
                this.captureFocus();
            });

        else if(Browser.IE) {
            this.clipboardInputElement.value = "";
            HtmlFocusUtils.focusWithPreventScroll(this.clipboardInputElement);
            this.clipboardInputElement.select();
            document.execCommand("Paste");
            callback(this.clipboardInputElement.value);
            this.captureFocus();
        }
    }
    isPasteSupportedByBrowser() {
        return Browser.IE || ((Browser.WebKitFamily || Browser.Firefox) && navigator && navigator["clipboard"] !== undefined);
    }

    private createInputElements(parent: HTMLElement, focusElementsParent: HTMLElement) {
        this.createFocusInputElement(focusElementsParent || parent);
        this.createTextInputElement(parent);
        this.createClipboardInputElement(focusElementsParent || parent);
        this.attachInputElementEvents();
    }

    private setInputElementFocusHandlerMode(captureFocus?: boolean) {
        this.textInputElementContainer.setAttribute("class", "dxdi-text-input-container");
        if(captureFocus)
            this.captureFocus();
    }
    private setInputElementTextInputMode(text: string, position: Point, size: Size, style: TextStyle,
        className: string, textAngle: TextAngle) {
        this.textInputElementContainer.setAttribute("class", "dxdi-text-input-container " + className);
        this.textInputElement.value = text;

        this.setTextInputElementBounds(position, size, textAngle);
        this.setTextInputElementStyle(style);
        this.updateTextInputPadding();

        const element = this.textInputElement || this.inputElement;
        HtmlFocusUtils.focusWithPreventScroll(element);
        if(element.select)
            element.select();
    }
    private setTextInputElementBounds(position: Point, size: Size, textAngle: TextAngle) {
        this.savedTextInputPosition = position;
        this.savedTextInputSize = size;
        this.savedTextInputAngle = textAngle;

        const abs = this.layoutPointResolver.getAbsolutePoint(position, true);

        this.textInputElementContainer.style.left = abs.x + "px";
        this.textInputElementContainer.style.top = abs.y + "px";
        this.textInputElementContainer.style.width = size && size.width + "px" || "0px";
        this.textInputElementContainer.style.height = size && size.height + "px" || "0px";

        const transforms = [];
        this.textInputElementContainer.style.transform = "";
        if(this.actualZoom !== 1)
            transforms.push("scale(" + this.actualZoom + ")");
        if(textAngle)
            transforms.push("rotate(" + textAngle + "deg)");
        this.textInputElementContainer.style.transform = transforms.join(" ");

        this.textInputElement.style.width = size && size.width + "px" || "";
        this.textInputElement.style.height = size && size.height + "px" || "auto";
    }
    private setTextInputElementStyle(style: TextStyle) {
        this.savedTextInputStyle = style;
        RenderUtils.applyStyleToElement(style, this.textInputElement);
    }

    private createFocusInputElement(parent: HTMLElement) {
        this.inputElement = document.createElement("textarea");
        this.inputElement.readOnly = Browser.TouchUI; 
        this.inputElement.setAttribute("class", "dxdi-focus-input");
        parent.appendChild(this.inputElement);
    }
    private attachInputElementEvents() {
        this.onInputBlurHandler = this.onInputBlur.bind(this);
        this.onInputFocusHandler = this.onInputFocus.bind(this);
        this.onInputKeyDownHandler = this.onInputKeyDown.bind(this);
        this.onInputKeyPressHandler = this.onInputKeyPress.bind(this);
        this.onInputKeyUpHandler = this.onInputKeyUp.bind(this);
        this.onPasteHandler = this.onPaste.bind(this);

        RenderHelper.addEventListener(this.inputElement, "blur", this.onInputBlurHandler);
        RenderHelper.addEventListener(this.inputElement, "focus", this.onInputFocusHandler);
        RenderHelper.addEventListener(this.inputElement, "keydown", this.onInputKeyDownHandler);
        RenderHelper.addEventListener(this.inputElement, "keypress", this.onInputKeyPressHandler);
        RenderHelper.addEventListener(this.inputElement, "keyup", this.onInputKeyUpHandler);
        RenderHelper.addEventListener(this.inputElement, "paste", this.onPasteHandler);

    }
    private detachInputElementEvents() {
        RenderHelper.removeEventListener(this.inputElement, "blur", this.onInputBlurHandler);
        RenderHelper.removeEventListener(this.inputElement, "focus", this.onInputFocusHandler);
        RenderHelper.removeEventListener(this.inputElement, "keydown", this.onInputKeyDownHandler);
        RenderHelper.removeEventListener(this.inputElement, "keypress", this.onInputKeyPressHandler);
        RenderHelper.removeEventListener(this.inputElement, "keyup", this.onInputKeyUpHandler);
        RenderHelper.removeEventListener(this.inputElement, "paste", this.onPasteHandler);
    }
    private createTextInputElement(parent: HTMLElement) {
        this.textInputElementContainer = document.createElement("div");
        this.textInputElementContainer.setAttribute("class", "dxdi-text-input-container");
        parent.appendChild(this.textInputElementContainer);
        this.textInputElement = document.createElement("textarea");
        this.textInputElement.setAttribute("class", TEXT_INPUT_CSSCLASS);
        this.attachTextInputElementEvents();
        this.textInputElementContainer.appendChild(this.textInputElement);
    }
    private attachTextInputElementEvents() {
        this.onTextInputBlurHandler = this.onTextInputBlur.bind(this);
        this.onTextInputFocusHandler = this.onTextInputFocus.bind(this);
        this.onTextInputKeyDownHandler = this.onTextInputKeyDown.bind(this);
        this.onTextInputMouseWheelHandler = this.onTextInputMouseWheel.bind(this);
        this.onTextInputMouseUpHandler = this.onTextInputMouseUp.bind(this);
        this.onTextInputKeyUpHandler = this.onTextInputKeyUp.bind(this);
        this.onTextInputChangeHandler = this.onTextInputChange.bind(this);

        RenderHelper.addEventListener(this.textInputElement, "mousewheel", this.onTextInputMouseWheelHandler);
        RenderHelper.addEventListener(this.textInputElement, "mouseup", this.onTextInputMouseUpHandler);
        RenderHelper.addEventListener(this.textInputElement, "blur", this.onTextInputBlurHandler);
        RenderHelper.addEventListener(this.textInputElement, "focus", this.onTextInputFocusHandler);
        RenderHelper.addEventListener(this.textInputElement, "keydown", this.onTextInputKeyDownHandler);
        RenderHelper.addEventListener(this.textInputElement, "keyup", this.onTextInputKeyUpHandler);
        RenderHelper.addEventListener(this.textInputElement, "change", this.onTextInputChangeHandler);
    }
    private detachTextInputElementEvents() {
        RenderHelper.removeEventListener(this.textInputElement, "mousewheel", this.onTextInputMouseWheelHandler);
        RenderHelper.removeEventListener(this.textInputElement, "mouseup", this.onTextInputMouseUpHandler);
        RenderHelper.removeEventListener(this.textInputElement, "blur", this.onTextInputBlurHandler);
        RenderHelper.removeEventListener(this.textInputElement, "focus", this.onTextInputFocusHandler);
        RenderHelper.removeEventListener(this.textInputElement, "keydown", this.onTextInputKeyDownHandler);
        RenderHelper.removeEventListener(this.textInputElement, "keyup", this.onTextInputKeyUpHandler);
        RenderHelper.removeEventListener(this.textInputElement, "change", this.onTextInputChangeHandler);
    }
    private createClipboardInputElement(parent: HTMLElement) {
        this.clipboardInputElement = document.createElement("textarea");
        this.clipboardInputElement.setAttribute("class", "dxdi-clipboard-input");
        parent.appendChild(this.clipboardInputElement);
    }

    private blurControl() {
        if(!this.focusLocked) {
            this.focused = false;
            DomUtils.removeClassName(this.mainElement, "focused");
        }
    }
    private focusControl() {
        this.focused = true;
        this.focusLocked = false;
        DomUtils.addClassName(this.mainElement, "focused");
    }
    private updateTextInputPadding() {
        const text = this.textInputElement.value;
        if(!this.savedTextInputSize) {
            const measureResults = this.textMeasurer.measureWords(" ", this.savedTextInputStyle, TextOwner.Connector);
            const textHeight = getLineHeight(measureResults) * ((textToParagraphs(text).length || 1) + 1);
            this.textInputElement.style.height = Math.ceil(textHeight) + "px";
        }
        else {
            const measureResults = this.textMeasurer.measureWords(text, this.savedTextInputStyle, TextOwner.Shape);
            const textHeight = getTextHeight(text, this.savedTextInputSize.width, measureResults, true);
            const top = Math.max(0, (this.savedTextInputSize.height - textHeight) * 0.5);
            this.textInputElement.style.paddingTop = Math.ceil(top) + "px";
            this.textInputElement.style.height = Math.floor(this.savedTextInputSize.height) + "px";
        }
    }

    private onInputBlur(evt: FocusEvent) {
        this.blurControl();
        raiseEvent(evt, this.getDiagramFocusEvent(evt), e => this.eventManager.onBlur(e));
    }
    private onInputFocus(evt: FocusEvent) {
        this.focusControl();
        raiseEvent(evt, this.getDiagramFocusEvent(evt), e => this.eventManager.onFocus(e));
    }
    private onInputKeyDown(evt: KeyboardEvent) {
        raiseEvent(evt, this.getDiagramKeyboardEvent(evt), e => this.eventManager.onKeyDown(e));
    }
    private onInputKeyPress(evt: KeyboardEvent) {
        if(evt.preventDefault && !(Browser.Safari && evt.code === "KeyV"))
            evt.preventDefault(); 
    }
    private onInputKeyUp(evt: KeyboardEvent) {
        raiseEvent(evt, this.getDiagramKeyboardEvent(evt), e => this.eventManager.onKeyUp(e));
    }
    private onTextInputBlur(evt: FocusEvent) {
        if(this.eventManager.canFinishTextEditing()) {
            this.blurControl();
            raiseEvent(evt, this.getDiagramFocusEvent(evt), e => this.eventManager.onTextInputBlur(e));
        }
        else {
            const srcElement = EvtUtils.getEventSource(evt);
            if(document.activeElement !== srcElement) 
                srcElement.focus();
        }
    }
    private onTextInputFocus(evt: FocusEvent) {
        this.focusControl();
        raiseEvent(evt, this.getDiagramFocusEvent(evt), e => this.eventManager.onTextInputFocus(e));
    }
    private onTextInputKeyDown(evt: KeyboardEvent) {
        raiseEvent(evt, this.getDiagramKeyboardEvent(evt), e => this.eventManager.onTextInputKeyDown(e));
    }
    private onTextInputKeyUp(evt: KeyboardEvent) {
        this.updateTextInputPadding();
    }
    private onTextInputChange(evt: KeyboardEvent) {
        this.updateTextInputPadding();
    }

    private onPaste(evt: ClipboardEvent) {
        raiseEvent(evt, this.getDiagramClipboardEvent(evt), e => this.eventManager.onPaste(e));
    }

    private onTextInputMouseWheel(evt: WheelEvent) {
        this.mouseWheelHandler && this.mouseWheelHandler(evt);
    }
    private onTextInputMouseUp(evt: MouseEvent) {
        if(evt.stopPropagation)
            evt.stopPropagation();
        EvtUtils.cancelBubble(evt);
    }

    private getDiagramKeyboardEvent(evt: KeyboardEvent) {
        return new DiagramKeyboardEvent(KeyUtils.getKeyModifiers(evt), KeyUtils.getEventKeyCode(evt), this.textInputElement.value); 
    }
    getTextInputElementValue(): string {
        return this.textInputElement.value;
    }
    private getDiagramFocusEvent(evt: FocusEvent) {
        return new DiagramFocusEvent((<HTMLTextAreaElement>evt.target).value);
    }
    private getDiagramClipboardEvent(evt: ClipboardEvent) {
        let clipboardData;
        const evtClipboardData = evt.clipboardData || (evt["originalEvent"] && evt["originalEvent"].clipboardData);
        if(evtClipboardData !== undefined)
            clipboardData = evtClipboardData.getData("text/plain");
        else
            clipboardData = window["clipboardData"].getData("Text");
        return new DiagramClipboardEvent(clipboardData);
    }

    isTextInputElement(element: HTMLElement) {
        return typeof element.className === "string" && element.className.indexOf(TEXT_INPUT_CSSCLASS) > -1;
    }

    lockFocus() {
        this.focusLocked = true;
        setTimeout(() => this.focusLocked = false, 10);
    }

    notifyViewAdjusted(canvasOffset: Point) { }
    notifyActualZoomChanged(actualZoom: number) {
        this.actualZoom = actualZoom;

        if(this.savedTextInputPosition && this.savedTextInputSize)
            this.setTextInputElementBounds(this.savedTextInputPosition, this.savedTextInputSize, this.savedTextInputAngle);
    }
    notifyTextInputStart(item: DiagramItem, text: string, position: Point, size?: Size): void {
        let className = "";
        let textAngle: TextAngle;
        if(item instanceof Shape) {
            className = "shape-text";
            textAngle = item.textAngle;
        }
        else if(item instanceof Connector)
            className = "connector-text";
        size = size && size.clone().applyConverter(UnitConverter.twipsToPixels);
        this.setInputElementTextInputMode(text, position, size, item.styleText, className, textAngle);
    }
    notifyTextInputEnd(item: DiagramItem, captureFocus?: boolean): void {
        this.setInputElementFocusHandlerMode(captureFocus);
    }
    notifyTextInputPermissionsCheck(item: DiagramItem, allowed: boolean): void {}
}
