/*
 *    Copyright [2021] [wisemapping]
 *
 *   Licensed under WiseMapping Public License, Version 1.0 (the "License").
 *   It is basically the Apache License, Version 2.0 (the "License") plus the
 *   "powered by wisemapping" text requirement on every single page;
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the license at
 *
 *       http://www.wisemapping.org/license
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
import { $assert } from '@wisemapping/core-js';
import { Workspace as Workspace2D, ElementClass, ElementPeer } from '@wisemapping/web2d';
import ScreenManager from './ScreenManager';
import SizeType from './SizeType';
import CanvasElement from './CanvasElement';

class Canvas {
  private _zoom: number;

  private _screenManager: ScreenManager;

  private _isReadOnly: boolean;

  private _containerSize: SizeType;

  private _workspace: Workspace2D;

  private _eventsEnabled: boolean;

  private _visibleAreaSize!: SizeType;

  private _renderQueue: (ElementClass<ElementPeer> | CanvasElement)[];

  private _queueRenderEnabled: boolean;

  private _mouseMoveListener;

  private _mouseUpListener;

  constructor(screenManager: ScreenManager, zoom: number, isReadOnly: boolean) {
    // Create a suitable container ...
    $assert(screenManager, 'Div container can not be null');
    $assert(zoom, 'zoom container can not be null');

    this._zoom = zoom;
    this._screenManager = screenManager;
    this._isReadOnly = isReadOnly;

    const divContainer = screenManager.getContainer();
    this._containerSize = {
      width: Number.parseInt(divContainer.css('width'), 10),
      height: Number.parseInt(divContainer.css('height'), 10),
    };
    // Initialize web2d workspace.
    const workspace = this._createWorkspace();
    this._workspace = workspace;

    // Append to the workspace...
    workspace.addItAsChildTo(divContainer);

    this.setZoom(zoom, true);
    this._renderQueue = [];
    this._eventsEnabled = false;
    this._queueRenderEnabled = false;
    this._mouseMoveListener = null;
    this._mouseUpListener = null;
  }

  private _adjustWorkspace(): void {
    this.setZoom(this._zoom, false);
  }

  registerEvents() {
    // Register drag events ...
    this._registerDragEvents();
    this._eventsEnabled = true;

    // Readjust if the window is resized ...
    window.addEventListener('resize', () => {
      this._adjustWorkspace();
    });
  }

  isReadOnly(): boolean {
    return this._isReadOnly;
  }

  private _createWorkspace(): Workspace2D {
    // Initialize workspace ...
    const browserVisibleSize = this._screenManager.getVisibleBrowserSize();
    const coordOriginX = -(browserVisibleSize.width / 2);
    const coordOriginY = -(browserVisibleSize.height / 2);

    const workspaceProfile = {
      width: `${this._containerSize.width}px`,
      height: `${this._containerSize.height}px`,
      coordSizeWidth: browserVisibleSize.width,
      coordSizeHeight: browserVisibleSize.height,
      coordOriginX,
      coordOriginY,
      fillColor: 'transparent',
      strokeWidth: 0,
    };

    return new Workspace2D(workspaceProfile);
  }

  append(shape: ElementClass<ElementPeer> | CanvasElement): void {
    if (this._queueRenderEnabled) {
      this._renderQueue.push(shape);
    } else {
      this.appendInternal(shape);
    }
  }

  private appendInternal(shape: CanvasElement | ElementClass<ElementPeer>): void {
    // eslint-disable-next-line no-prototype-builtins, dot-notation
    if (typeof shape['addToWorkspace'] === 'function') {
      (shape as CanvasElement).addToWorkspace(this);
    } else {
      this._workspace.append(shape as ElementClass<ElementPeer>);
    }
  }

  enableQueueRender(value: boolean): Promise<void> {
    this._queueRenderEnabled = value;

    let result = Promise.resolve();
    if (!value) {
      // eslint-disable-next-line arrow-body-style
      result = Canvas.delay(100).then(() => {
        return this.processRenderQueue(this._renderQueue.reverse(), 300);
      });
    }
    return result;
  }

  private static delay(t: number) {
    return new Promise((resolve) => setTimeout(resolve, t));
  }

  private processRenderQueue(
    renderQueue: (ElementClass<ElementPeer> | CanvasElement)[],
    batch: number,
  ): Promise<void> {
    let result: Promise<void>;

    if (renderQueue.length > 0) {
      result = new Promise(
        (resolve: (queue: (ElementClass<ElementPeer> | CanvasElement)[]) => void) => {
          for (let i = 0; i < batch && renderQueue.length > 0; i++) {
            const elem = renderQueue.pop()!;
            this.appendInternal(elem);
          }

          resolve(renderQueue);
        },
      ).then((queue) => Canvas.delay(30).then(() => this.processRenderQueue(queue, batch)));
    } else {
      result = Promise.resolve();
    }
    return result;
  }

  removeChild(shape: ElementClass<ElementPeer> | CanvasElement): void {
    // eslint-disable-next-line dot-notation
    if (typeof shape['removeFromWorkspace'] === 'function') {
      (shape as CanvasElement).removeFromWorkspace(this);
    } else {
      this._workspace.removeChild(shape as ElementClass<ElementPeer>);
    }
  }

  addEvent(type: string, listener: (event: MouseEvent) => void): void {
    this._workspace.addEvent(type, listener);
  }

  removeEvent(type: string, listener: (event: MouseEvent) => void): void {
    $assert(type, 'type can not be null');
    $assert(listener, 'listener can not be null');
    this._workspace.removeEvent(type, listener);
  }

  getSize(): SizeType {
    return this._workspace.getCoordSize();
  }

  setZoom(zoom: number, center = false): void {
    this._zoom = zoom;
    const workspace = this._workspace;

    const divContainer = this._screenManager.getContainer();
    const containerWidth = divContainer.width()!;
    const containerHeight = divContainer.height()!;
    const newVisibleAreaSize = { width: containerWidth, height: containerHeight };

    // - svg must fit container size
    const svgElement = divContainer.find('svg');
    svgElement.attr('width', containerWidth);
    svgElement.attr('height', containerHeight);
    // - svg viewPort must fit container size with zoom adjustment
    const newCoordWidth = containerWidth * this._zoom;
    const newCoordHeight = containerHeight * this._zoom;

    let coordOriginX: number;
    let coordOriginY: number;
    if (center) {
      // Center and define a new center of coordinates ...
      coordOriginX = -(newVisibleAreaSize.width / 2) * zoom;
      coordOriginY = -(newVisibleAreaSize.height / 2) * zoom;
    } else {
      const oldCoordOrigin = workspace.getCoordOrigin();

      // Next coordSize is always centered in the middle of the visible area ...
      const newCoordOriginX = -(newVisibleAreaSize.width / 2) * zoom;
      const newCoordOriginY = -(newVisibleAreaSize.height / 2) * zoom;

      // Calculate the offset with the original center to ...
      const oldCenterOriginX = -(this._visibleAreaSize.width / 2) * zoom;
      const oldCenterOriginY = -(this._visibleAreaSize.height / 2) * zoom;

      const offsetX = oldCoordOrigin.x - oldCenterOriginX;
      const offsetY = oldCoordOrigin.y - oldCenterOriginY;

      // Update to new coordinate ...
      coordOriginX = Math.round(newCoordOriginX + offsetX);
      coordOriginY = Math.round(newCoordOriginY + offsetY);
    }

    workspace.setCoordOrigin(coordOriginX, coordOriginY);
    workspace.setCoordSize(newCoordWidth, newCoordHeight);
    this._visibleAreaSize = newVisibleAreaSize;

    // Update screen.
    this._screenManager.setOffset(coordOriginX, coordOriginY);
    this._screenManager.setScale(zoom);

    // Some changes in the screen. Let's fire an update event...
    this._screenManager.fireEvent('update');
  }

  getScreenManager(): ScreenManager {
    return this._screenManager;
  }

  enableWorkspaceEvents(value: boolean) {
    this._eventsEnabled = value;
  }

  isWorkspaceEventsEnabled(): boolean {
    return this._eventsEnabled;
  }

  getSVGElement(): Element {
    return this._workspace.getSVGElement();
  }

  setBackgroundStyle(css: string): void {
    const elem = this.getSVGElement().parentElement!.parentElement!;
    elem.setAttribute('style', css);
  }

  private _registerDragEvents() {
    const workspace = this._workspace;
    const screenManager = this._screenManager;
    const mWorkspace = this;
    const mouseDownListener = (event: MouseEvent) => {
      if (!this._mouseMoveListener) {
        if (mWorkspace.isWorkspaceEventsEnabled()) {
          mWorkspace.enableWorkspaceEvents(false);

          const mouseDownPosition = screenManager.getWorkspaceMousePosition(event);
          const originalCoordOrigin = workspace.getCoordOrigin();

          let wasDragged = false;
          this._mouseMoveListener = (mouseMoveEvent: MouseEvent) => {
            const currentMousePosition = screenManager.getWorkspaceMousePosition(mouseMoveEvent);

            const offsetX = currentMousePosition.x - mouseDownPosition.x;
            const coordOriginX = -offsetX + originalCoordOrigin.x;

            const offsetY = currentMousePosition.y - mouseDownPosition.y;
            const coordOriginY = -offsetY + originalCoordOrigin.y;

            workspace.setCoordOrigin(coordOriginX, coordOriginY);

            // Change cursor.
            window.document.body.style.cursor = 'move';
            // If I dont ignore touchmove events, browser console shows a lot of errors:
            // Unable to preventDefault inside passive event listener invocation.
            if (mouseMoveEvent.type !== 'touchmove') {
              mouseMoveEvent.preventDefault();
            }

            // Fire drag event ...
            screenManager.fireEvent('update');
            wasDragged = true;
          };
          screenManager.addEvent('mousemove', this._mouseMoveListener);
          screenManager.addEvent('touchmove', this._mouseMoveListener);

          // Register mouse up listeners ...
          this._mouseUpListener = () => {
            screenManager.removeEvent('mousemove', this._mouseMoveListener);
            screenManager.removeEvent('mouseup', this._mouseUpListener);
            screenManager.removeEvent('touchmove', this._mouseUpListener);
            screenManager.removeEvent('touchend', this._mouseMoveListener);
            this._mouseUpListener = null;
            this._mouseMoveListener = null;
            window.document.body.style.cursor = 'default';

            // Update screen manager offset.
            const coordOrigin = workspace.getCoordOrigin();
            screenManager.setOffset(coordOrigin.x, coordOrigin.y);
            mWorkspace.enableWorkspaceEvents(true);

            if (!wasDragged) {
              screenManager.fireEvent('click');
            }
          };
          screenManager.addEvent('mouseup', this._mouseUpListener);
          screenManager.addEvent('touchend', this._mouseUpListener);
        }
      } else {
        this._mouseUpListener();
      }
    };
    screenManager.addEvent('mousedown', mouseDownListener);
    screenManager.addEvent('touchstart', mouseDownListener);
  }

  getZoom() {
    return this._zoom;
  }
}

export default Canvas;
