﻿import * as _ from "lodash";
import * as ko from "knockout";
import * as Arrays from "@paperbits/common/arrays";
import * as Html from "@paperbits/common/html";
import template from "./defaultViewManager.html";
import "@paperbits/common/extensions";
import { Bag, Keys } from "@paperbits/common";
import { EventManager, Events, GlobalEventHandler } from "@paperbits/common/events";
import { IComponent, View, ViewManager, ICommand, ViewManagerMode, IHighlightConfig, IContextCommandSet, ISplitterConfig, Toast, IContextCommand } from "@paperbits/common/ui";
import { Router } from "@paperbits/common/routing";
import { DragSession } from "@paperbits/common/ui/draggables";
import { IWidgetBinding } from "@paperbits/common/editing";
import { Component, OnMounted, OnDestroyed } from "@paperbits/common/ko/decorators";
import { RoleModel, BuiltInRoles } from "@paperbits/common/user";
import { DesignerUserService } from "./designerUserService";
import { ViewStack } from "@paperbits/common/ui/viewStack";
import { ISettingsProvider } from "@paperbits/common/configuration";

declare let uploadDialog: HTMLInputElement;
const defaultEditorWidth = 320;


@Component({
    selector: "view-manager",
    template: template
})
export class DefaultViewManager implements ViewManager {
    private contextualCommandsBag: Bag<IContextCommandSet> = {};
    private pointerX: number;
    private pointerY: number;

    public readonly designTime: ko.Observable<boolean>;
    public readonly previewable: ko.Observable<boolean>;
    public readonly block: ko.Computed<boolean>;
    public readonly journey: ko.ObservableArray<View>;
    public readonly journeyName: ko.Computed<string>;
    public readonly toasts: ko.ObservableArray<Toast>;
    public readonly primaryToolboxVisible: ko.Observable<boolean>;
    public readonly activeView: ko.Observable<View>;
    public readonly contextualCommands: ko.ObservableArray<IContextCommandSet>;
    public readonly highlightedElement: ko.Observable<IHighlightConfig>;
    public readonly splitterElement: ko.Observable<ISplitterConfig>;
    public readonly selectedElement: ko.Observable<IHighlightConfig>;
    public readonly selectedElementContextualEditor: ko.Observable<IContextCommandSet>;
    public readonly viewport: ko.Observable<string>;
    public readonly rolesScope: ko.ObservableArray<RoleModel>;
    public readonly host: ko.Observable<IComponent>;
    public readonly shutter: ko.Observable<boolean>;
    public readonly dragSession: ko.Observable<DragSession>;
    public readonly locale: ko.Observable<string>;
    public readonly canPreview: ko.Computed<boolean>;
    public readonly canGoBack: ko.Computed<boolean>;
    public readonly websitePreviewEnabled: ko.Observable<boolean>;
    public readonly activeLayer: ko.Observable<string>;

    public mode: ViewManagerMode;
    public hostDocument: Document;

    private previousMode: ViewManagerMode;
    private previousHost: IComponent;

    constructor(
        private readonly eventManager: EventManager,
        private readonly globalEventHandler: GlobalEventHandler,
        private readonly designerUserService: DesignerUserService,
        private readonly settingsProvider: ISettingsProvider,
        private readonly router: Router,
        private readonly viewStack: ViewStack
    ) {
        this.designTime = ko.observable(false);
        this.previewable = ko.observable(true);
        this.activeLayer = ko.observable();
        this.block = ko.computed(() => {
            return this.designTime() && this.previewable();
        });
        this.mode = ViewManagerMode.selecting;
        this.toasts = ko.observableArray<Toast>();
        this.journey = ko.observableArray<View>();
        this.journeyName = ko.pureComputed<string>(() => {
            if (this.journey().length === 0) {
                return null;
            }

            return this.journey()[0].heading;
        });

        this.activeView = ko.observable<View>();
        this.contextualCommands = ko.observableArray<IContextCommandSet>([]);
        this.highlightedElement = ko.observable<IHighlightConfig>();
        this.splitterElement = ko.observable<ISplitterConfig>();
        this.selectedElement = ko.observable<IHighlightConfig>();
        this.selectedElementContextualEditor = ko.observable<IContextCommandSet>();
        this.viewport = ko.observable<string>("xl");
        this.locale = ko.observable<string>("en-us");
        this.rolesScope = ko.observableArray([BuiltInRoles.anonymous]);
        this.host = ko.observable<IComponent>();
        this.shutter = ko.observable(true);
        this.dragSession = ko.observable();
        this.primaryToolboxVisible = ko.observable(false);
        this.websitePreviewEnabled = ko.observable(false);
        this.canPreview = ko.pureComputed<boolean>(() => this.websitePreviewEnabled() && this.host()?.name === "page-host");
        this.canGoBack = ko.pureComputed<boolean>(() => ["style-guide", "layout-host"].includes(this.host()?.name));
    }

    @OnMounted()
    public async initialize(): Promise<void> {
        this.globalEventHandler.addDragEnterListener(this.hideToolboxes.bind(this));
        this.globalEventHandler.addDragDropListener(this.onDragEnd.bind(this));
        this.globalEventHandler.addDragEndListener(this.onDragEnd.bind(this));
        this.globalEventHandler.addDragLeaveScreenListener(this.showToolboxes.bind(this));
        this.eventManager.addEventListener(Events.VirtualDragEnd, this.onDragEnd.bind(this));

        this.router.addRouteChangeListener(this.onRouteChange.bind(this));
        this.globalEventHandler.appendDocument(document);

        this.eventManager.addEventListener("onTopLevelEscape", this.onEscape.bind(this));
        this.eventManager.addEventListener("onKeyDown", this.onKeyDown.bind(this));
        this.eventManager.addEventListener("onKeyUp", this.onKeyUp.bind(this));

        const websitePreviewEnabled = await this.settingsProvider.getSetting<boolean>("features/preview");
        this.websitePreviewEnabled(websitePreviewEnabled || false);
    }

    public onHostDocumentCreated(hostDocument: Document): void {
        this.hostDocument = hostDocument;
        this.hostDocument.addEventListener(Events.MouseMove, this.onPointerMove, true);
    }

    public onHostDocumentDisposed(): void {
        this.hostDocument = null;
        this.hostDocument.removeEventListener(Events.MouseMove, this.onPointerMove, true);
    }

    private onKeyDown(event: KeyboardEvent): void {
        if (event.ctrlKey && event.key === Keys.F10) {
            if (this.mode === ViewManagerMode.preview) {
                this.disablePreviewMode();
            }
            else {
                this.enablePreviewMode();
            }
        }

        if (this.getActiveView()) {
            return;
        }

        if (!event.ctrlKey && !event.metaKey) {
            return;
        }

        this.designTime(false);
        this.clearSelection();
    }

    private onKeyUp(event: KeyboardEvent): void {
        if (this.getActiveView()) {
            return;
        }

        if (event.ctrlKey || event.metaKey) {
            return;
        }

        if (this.mode === ViewManagerMode.preview) {
            return;
        }

        this.designTime(true);
    }

    public setHost(component: IComponent, canGoBack: boolean = false): void {
        const currentComponent = this.host();

        this.previousHost = canGoBack ? currentComponent : null;

        if (currentComponent && currentComponent.name === component.name && !currentComponent.params) {
            return;
        }

        this.clearContextualCommands();
        this.host(component);
        this.previewable(component.name !== "style-guide");
    }

    private onPointerMove(event: PointerEvent): void {
        this.pointerX = event.clientX;
        this.pointerY = event.clientY;
    }

    public getPointerPosition(): any {
        return { x: this.pointerX, y: this.pointerY };
    }

    public getHost(): IComponent {
        return this.host();
    }

    public getDesignerDocument(): Document {
        return document;
    }

    public getHostDocument(): Document {
        return this.hostDocument;
    }

    private onRouteChange(): void {
        this.clearContextualCommands();
        this.closeView();
    }

    public getCurrentJourney(): string {
        return this.journeyName();
    }

    public addToast(title: string, content: string, commands?: ICommand[]): Toast {
        const toast = new Toast(title, content, "info", null, commands);
        this.toasts.push(toast);

        return toast;
    }

    public removeToast(toast: Toast): void {
        this.toasts.splice(this.toasts().indexOf(toast), 1);
    }

    public notifySuccess(title: string, content: string): Toast {
        const toast = new Toast(title, content, "success");
        this.toasts.push(toast);
        this.scheduleToastRemoval(toast);
        return toast;
    }

    public notifyInfo(title: string, content: string, commands?: ICommand[]): Toast {
        const toast = new Toast(title, content, "info", null, commands);
        this.toasts.push(toast);
        this.scheduleToastRemoval(toast);
        return toast;
    }

    public notifyError(title: string, content: string): Toast {
        const toast = new Toast(title, content, "error");
        this.toasts.push(toast);
        this.scheduleToastRemoval(toast, 15000);
        return toast;
    }

    public notifyProgress(promise: Promise<any>, title: string, content: string): Toast {
        const toast = new Toast(title, content);

        this.toasts.push(toast);

        if (promise["progress"]) {
            promise["progress"](toast.progress);
        }

        promise.then(() => {
            toast.progress(100);
        }).catch(() => {
            toast.state("error");
        }).finally(() => {
            this.scheduleToastRemoval(toast);
        });

        return toast;
    }

    public updateJourneyComponent(view: View): void {
        let journey = this.journey();

        const existingComponent = journey.find(c => { return c.component.name === view.component.name; });

        if (existingComponent) {
            journey = journey.splice(0, journey.indexOf(existingComponent));
        }

        journey.push(view);

        this.journey(journey);
    }

    public clearJourney(): void {
        this.journey([]);
        this.activeView(null);
        this.viewStack.clear();
    }

    public hideToolboxes(): void {
        this.journey([]);
        this.primaryToolboxVisible(false);
        if (this.mode !== ViewManagerMode.preview) {
            this.mode = ViewManagerMode.dragging;
        }
        this.clearContextualCommands();
    }

    public showToolboxes(): void {
        this.primaryToolboxVisible(true);
        this.mode = ViewManagerMode.selecting;
    }

    public openViewAsWorkshop(view: View): void {
        this.viewStack.clear();
        this.clearContextualCommands();
        this.updateJourneyComponent(view);
        this.mode = ViewManagerMode.configure;
    }

    /**
     * Deletes specified view and all views after it in the stack.
     * @param view View
     */
    public closeWorkshop(view: View | string): void {
        const journey = this.journey();
        let viewToClose;

        if (typeof view === "string") {
            viewToClose = journey.find(x => x.component.name === view);
        }
        else {
            viewToClose = view;
        }

        const indexOfClosingView = journey.indexOf(viewToClose);

        if (indexOfClosingView < 0) {
            return;
        }

        journey.splice(indexOfClosingView);

        this.journey(journey);
        this.mode = ViewManagerMode.selecting;
    }

    private scheduleToastRemoval(toast: Toast, displayDuration: number = 8000): void {
        setTimeout(() => {
            this.toasts(_.without(this.toasts(), toast));
        }, displayDuration);
    }

    public openUploadDialog(...accept: string[]): Promise<File[]> {
        uploadDialog.accept = accept?.join(",");
        uploadDialog.click();

        return new Promise<File[]>((resolve) => {
            uploadDialog.onchange = () => {
                const selectedFiles = Arrays.coerce<File>(uploadDialog.files);
                uploadDialog.value = "";
                resolve(selectedFiles);
            };
        });
    }

    public openViewAsPopup(view: View): void {
        this.viewStack.clear();

        if (this.activeView() === view) {
            return;
        }

        if (!view.name) {
            view.name = `${view.component.name}-view`;
        }

        view.hitTest = (el) => { // TODO: Move to bindingHandler
            return !!Html.closest(el, (x: HTMLElement) =>
                (x.getAttribute && !!x.getAttribute(Html.Attributes.ContentEditable)) || // TODO: Move hitTest check to text editor
                (x?.classList && Arrays.coerce(x.classList).includes("toolbox")));
        };

        view.close = () => this.closeView();

        if (view.component.params) {
            view.component.params.onClose = () => this.closeView();
        }

        if (!view.resizing) {
            view.resizing = {
                directions: "vertically horizontally",
                initialWidth: defaultEditorWidth
            }
        }
        else {
            if (typeof view.resizing === "string" && view.name !== "text-block-editor") {
                view.resizing = {
                    directions: view.resizing,
                    initialWidth: defaultEditorWidth
                };
            }
        }

        if (!view.positioning) {
            view.positioning = {
                top: 20,
                left: 150
            }
        }

        this.clearContextualCommands();
        this.closeView();
        this.activeView(view);
        this.mode = ViewManagerMode.configure;

        if (view.component.name === "text-block-editor") {
            this.designTime(false); // Review: It's here for text editor
        }

        this.viewStack.pushView(view);
    }

    public getActiveView(): View {
        return this.activeView();
    }

    public onEscape(): void {
        const host = this.host();

        if (this.viewStack.getViews().length === 0 && this.journey().length > 0) {
            const journey = this.journey();
            journey.pop();
            this.journey(journey);

            return;
        }

        if (!this.getActiveView() && this.journey().length === 0 && host && host.name !== "page-host") {
            this.setHost({ name: "page-host" }); // TODO: Get host type by current route.
        }
    }

    public closeEditors(): void {
        this.closeView();
        this.clearJourney();
    }

    public openWidgetEditor(binding: IWidgetBinding<any, any>): void {
        if (!binding.editor) {
            return;
        }

        let editorComponentName: string;

        if (typeof binding.editor === "string") {
            editorComponentName = binding.editor;
        }
        else {
            const registration = Reflect.getMetadata("paperbits-component", binding.editor);
            editorComponentName = registration?.name;
        }

        const view: View = {
            name: `${binding.name}-editor`,
            component: {
                name: editorComponentName,
                definition: binding.editor,
                binder: binding.editorComponentBinder,
                params: {
                    model: binding.model,
                    onChange: binding.applyChanges
                }
            },
            scrolling: "editorScrolling" in binding ? <boolean>binding.editorScrolling : true,
            heading: binding.displayName,
            resizing: <string>binding.editorResizing,
            returnFocusTo: document.getElementById("contentEditor")
        };

        this.openViewAsPopup(view);

        this.eventManager.dispatchEvent(Events.HintRequest, {
            key: "fe7c",
            content: `You can resize and move almost every editor, and it will remember its size and position.`
        });
    }

    public closeView(): void {
        if (this.mode === ViewManagerMode.preview) {
            return;
        }

        const view = this.activeView();

        if (view) {
            this.viewStack.removeView(view);

            if (view.onClose) {
                view.onClose();
            }
        }

        this.activeView(null);
        this.eventManager.dispatchEvent("onViewClose");
        this.clearContextualCommands();
        this.mode = ViewManagerMode.selecting;

        this.primaryToolboxVisible(true);
        this.designTime(true);
    }

    public setContextualCommands(widgetName: string, contextualCommandSet: IContextCommandSet): void {
        this.contextualCommandsBag[widgetName] = contextualCommandSet;

        const commands = Object.keys(this.contextualCommandsBag).map(key => this.contextualCommandsBag[key]);

        this.contextualCommands(commands);
    }

    public removeContextualCommands(editorName: string): void {
        if (!this.contextualCommandsBag[editorName]) {
            return;
        }

        delete this.contextualCommandsBag[editorName];

        const editors = Object.keys(this.contextualCommandsBag).map(key => this.contextualCommandsBag[key]);

        this.contextualCommands(editors);
    }

    public clearContextualCommands(): void {
        if (this.mode === ViewManagerMode.configure) {
            return;
        }

        this.contextualCommandsBag = {};
        this.contextualCommands([]);
        this.highlightedElement(null);
        this.setSplitter(null);
        this.selectedElement(null);
        this.selectedElementContextualEditor(null);

        if (this.mode !== ViewManagerMode.preview) {
            this.designTime(true);
            this.mode = ViewManagerMode.selecting;
        }
    }

    public setHighlight(config: IHighlightConfig): void {
        if (this.mode === ViewManagerMode.preview) {
            return;
        }
        this.highlightedElement(null);
        this.setSplitter(null);
        this.highlightedElement(config);
    }

    public setSplitter(config: ISplitterConfig): void {
        if (this.mode === ViewManagerMode.preview) {
            return;
        }
        this.splitterElement(null);
        this.splitterElement(config);
    }

    public setSelectedElement(config: IHighlightConfig, contextualCommand: IContextCommandSet): void {
        if (this.mode === ViewManagerMode.preview) {
            return;
        }
        this.clearContextualCommands();
        this.closeView();
        this.selectedElement(null);
        this.selectedElement(config);
        this.selectedElementContextualEditor(contextualCommand);

        if (this.mode !== ViewManagerMode.configure) {
            this.mode = ViewManagerMode.selected;
        }

        this.clearJourney();
    }

    public clearSelection(): void {
        this.selectedElement(null);
    }

    public getSelectedElement(): IHighlightConfig {
        return this.selectedElement();
    }

    public setViewport(viewport: string): void {
        this.clearContextualCommands();
        this.viewport(viewport);
    }

    public getViewport(): string {
        return this.viewport();
    }

    public setViewRoles(roles: RoleModel[]): void {
        const roleKeys = roles.map(role => role.key);

        this.rolesScope(roles);
        this.designerUserService.setUserRoles(roleKeys);

        this.eventManager.dispatchEvent("onUserRoleChange", roleKeys);
    }

    public getViewRoles(): RoleModel[] {
        return this.rolesScope();
    }

    public setShutter(): void {
        this.previousMode = this.mode;
        this.mode = ViewManagerMode.pause;
        this.shutter(true);
    }

    public removeShutter(): void {
        this.mode = this.previousMode;
        this.shutter(false);
    }

    public pauseContextualCommands(...except: IContextCommand[]): void {
        this.mode = ViewManagerMode.pause;
        this.highlightedElement(null);
    }

    public resumeContextualCommands(): void {
        this.mode = ViewManagerMode.selecting;
        this.designTime(true);
        this.clearContextualCommands();
    }

    public beginDrag(session: DragSession): void {
        this.clearContextualCommands();
        this.closeView();
        this.dragSession(session);
        this.hideToolboxes();
    }

    public getDragSession(): DragSession {
        return this.dragSession();
    }

    public onDragEnd(): void {
        if (this.mode !== ViewManagerMode.preview) {
            this.showToolboxes();
        }
        this.highlightedElement(null);
        this.selectedElement(null);
    }

    public enablePreviewMode(): void {
        this.previousMode = this.mode;
        this.clearJourney();
        this.hideToolboxes();
        this.designTime(false);
        this.toasts().forEach(toast => {
            this.removeToast(toast);
        });
        this.mode = ViewManagerMode.preview;
    }

    public disablePreviewMode(): void {
        this.showToolboxes();
        this.designTime(true);
        this.mode = ViewManagerMode.selecting;
    }

    public returnToContentEditing(): void {
        if (this.previousHost) {
            this.setHost(this.previousHost);
        }

        this.designTime(true);
        this.mode = ViewManagerMode.selecting;
    }

    public getActiveLayer(): string {
        return this.activeLayer();
    }

    public setActiveLayer(layerName: string): void {
        this.activeLayer(layerName);
    }

    @OnDestroyed()
    public dispose(): void { }
}