import { I18N } from 'aurelia-i18n';
import { DialogController } from 'aurelia-dialog';
import { CustomLogger } from '../../modules/custom-logger';
import { Box } from '../../modules/box';
import { HttpClient } from 'aurelia-fetch-client';
import { bindable, autoinject, bindingMode } from 'aurelia-framework';
import { Config } from '../../config';
import { RemoteFileStorage } from './../../services/file-storage-service';

import { Layer } from 'konva/lib/Layer';
import { Stage } from 'konva/lib/Stage';
import { Image } from 'konva/lib/shapes/Image';
import { Group } from 'konva/lib/Group';
import { Line } from 'konva/lib/shapes/Line';
import { Text } from 'konva/lib/shapes/Text';
import { Transformer } from 'konva/lib/shapes/Transformer';
import { Node } from 'konva/lib/Node';

@autoinject
export class UIPictureEdit {
  @bindable({ defaultBindingMode: bindingMode.oneTime }) public imageUrl: string;
  @bindable({ defaultBindingMode: bindingMode.oneTime }) public imageType: "url" | "base64" = "url";

  public imageContainer: HTMLDivElement; // view-ref
  public isPainting = false;
  public canDeleteElements = false;
  public canPaint = false;
  public isSaving = false;

  private stage: Stage;
  private layer: Layer;
  private imageGroup: Group;
  private image: Image;
  private transformer: Transformer;
  private selectedNode: Node;

  public currentColor;
  public currentLineStrokeWidth;

  constructor(private box: Box, private i18n: I18N, private config: Config, private httpClient: HttpClient, private logger: CustomLogger, private dialogController: DialogController, private fileStorageService: RemoteFileStorage.Service) {
  }

  activate(params) {
    if (params != null) {
      this.imageUrl = params.url;
      if (params.type != null) this.imageType = params.type;
    }
  }

  attached() {
    this.imageContainer.style.height = ((this.imageContainer.parentElement?.parentElement?.clientHeight ?? 650) - 50) + "px";
    this.initKonva();
  }

  detached() {
    if (this.canPaint) {
      this.onClickTogglePainting();
    }
    if (this.canDeleteElements) {
      this.onClickToggleDeleteElement();
    }
  }

  /**
   * Init Konva container and layer.
   *
   * @private
   * @memberof UIPictureEdit
   */
  private async initKonva(): Promise<void> {
    this.currentColor = "#000000";
    this.currentLineStrokeWidth = 4;

    this.stage = new Stage({
      container: this.imageContainer,
      width: this.imageContainer.clientWidth,
      height: this.imageContainer.clientHeight
    });

    this.layer = new Layer();
    this.stage.add(this.layer);

    await this.addBackgroundImage();
    await this.addTransformer();

    this.stage.on("click", (e) => {
      if (this.canDeleteElements) {
        return;
      }
      if (e.target === e.target.getStage()) {
        this.selectedNode = null;
        this.updateTransformer();
        return;
      }

      this.selectedNode = e.target;
      this.updateTransformer();
    });

    this.initHandDrawing();
    this.initZooming();
  }

  private updateTransformer() {
    if (this.selectedNode === this.transformer.getNode()) {
      return;
    }

    if (this.selectedNode != null && this.selectedNode != this.imageGroup && this.selectedNode != this.image) {
      this.transformer.nodes([this.selectedNode]);
    } else {
      this.transformer.nodes([]);
    }
  }

  /**
   * Find Node of the given type in the given objects array.
   * 
   * @param fromNode 
   * @param name 
   * @returns 
   */
  private findNodes(fromNode: any, name: 'Stage' | 'Layer' | 'Group' | 'Image' | 'Line' | 'Text' | 'GroupText' | 'GroupTags'): Array<any> {
    if (fromNode) {
      if (fromNode.className == name || fromNode.attrs?.className == name) return [fromNode];

      if (fromNode.children && fromNode.children.length) {
        let founds = [];
        (<Array<any>>fromNode.children).forEach(n => {
          let found = this.findNodes(n, name);
          if (found != null) founds = founds.concat(found);
        });
        if (founds.length) return founds;
      }
    }
  }

  /**
   * Reset the stage zoom.
   */
  private resetZoom(): void {
    this.stage.scale({ x: 1, y: 1 });
    this.stage.position({ x: 0, y: 0 });
  }

  /**
   * Center the image group.
   *
   * @memberof UIPictureEdit
   */
  private centerImageGroup(): void {
    if (this.imageGroup != null) {
      this.centerNode(this.imageGroup);
    }
  }

  /**
   * Toggle imageGroup draggable.
   *
   * @private
   * @param {boolean} [state=null]
   * @memberof UIPictureEdit
   */
  private toggleImageGroupDraggable(state: boolean = null): void {
    if (this.imageGroup.draggable() === false || state === true) {
      this.imageGroup.draggable(true);
      this.imageGroup.on('mouseenter', () => { this.stage.container().style.cursor = 'grab'; });
      this.imageGroup.on('mouseleave', () => { this.stage.container().style.cursor = 'default'; });
    } else {
      this.imageGroup.draggable(false);
      this.imageGroup.removeEventListener('mouseenter');
      this.imageGroup.removeEventListener('mouseleave');
    }
  }

  /**
   * Add the given imagepath if not null as background.
   *
   * @private
   * @memberof UIPictureEdit
   */
  private async addBackgroundImage(): Promise<boolean> {
    return new Promise(async (resolve, reject) => {
      if (this.imageUrl != null) {
        // Create a group to host the image, tags, lines, etc...
        this.imageGroup = new Group({
          x: 0,
          y: 0,
          width: this.stage.getSize().width,
          height: this.stage.getSize().height
        });
        this.toggleImageGroupDraggable(true);

        this.stage.on('dblclick dbltap', (e) => {
          e.evt.preventDefault();

          if (!(e.target instanceof Stage) && !(e.target instanceof Image)) return;
          this.resetZoom();
          this.centerImageGroup();
        });
        this.layer.add(this.imageGroup);

        // Then load the image path
        let fileUrl = await this.fileStorageService.fileUrl(this.imageUrl);
        Image.fromURL(fileUrl, (img: Image) => {
          this.image = img;
          this.imageGroup.setSize(img.getSize());
          this.imageGroup.add(img);
          this.centerImageGroup();
          img.setZIndex(0.1);
        });
      }
      resolve(true);
    });
  }

  /**
   * Add global transformer.
   *
   * @private
   * @memberof UIPictureEdit
   */
  private addTransformer(): Promise<boolean> {
    return new Promise((resolve, reject) => {
      this.transformer = new Transformer({
        nodes: [],
        centeredScaling: true,
        rotationSnaps: [0, 90, 180, 270],
        resizeEnabled: true
      });
      this.layer.add(this.transformer);
      resolve(true);
    });
  }

  /**
   * Center the given node to the stage.
   *
   * @private
   * @param {Node} node
   * @memberof UIPictureEdit
   */
  private centerNode(node: Node): void {
    if (node != null && node instanceof Node) {
      node.setAttrs({
        x: (this.stage.width() - node.width()) / 2,
        y: (this.stage.height() - node.height()) / 2
      });
    }
  }

  /**
   * Set the given node removable.
   * 
   * @param node 
   */
  private setRemovable(node: Node): void {
    if (node != null && node instanceof Node) {

      let colorSave: string = null;
      node.on('mouseenter', () => {
        if (this.canDeleteElements) {
          if (node instanceof Line) {
            colorSave = node.getAttr('stroke');
            node.setAttr('stroke', 'blue');
          }
          else if (node instanceof Text) {
            colorSave = node.getAttr('fill');
            node.setAttr('fill', 'blue');
          }
          else if (node instanceof Group) {
            let rect = node.findOne('Rect');
            if (rect) {
              rect.setAttr('stroke', 'red');
              rect.setAttr('strokeWidth', 5);
            }
            let img = node.findOne('Image');
            if (img) {
              img.setAttr('stroke', 'red');
              img.setAttr('strokeWidth', 5);
            }
          }
        }
      });

      node.on('mouseleave', () => {
        if (this.canDeleteElements) {
          if (node instanceof Line) {
            node.setAttr('stroke', colorSave ?? this.currentColor);
          }
          else if (node instanceof Text) {
            node.setAttr('fill', colorSave ?? this.currentColor);
          }
          else if (node instanceof Group) {
            let rect = node.findOne('Rect');
            if (rect) {
              rect.setAttr('stroke', 'black');
              rect.setAttr('strokeWidth', 2);
            }
            let img = node.findOne('Image');
            if (img) {
              // img.setAttr('stroke', 'blue');
              img.setAttr('strokeWidth', 0);
            }
          }
        }
      });

      node.on('click tap', () => {
        if (this.canDeleteElements) {
          node.remove();
        }
      });
    }
  }

  /**
   * Init zooming on mouse wheel.
   *
   * @private
   * @memberof UIPictureEdit
   */
  private initZooming(): void {
    var scaleBy = 1.2;
    this.stage.on('wheel', (e) => {
      if (!this.isPainting) {
        e.evt.preventDefault();
        var oldScale = this.stage.scaleX();

        var pointer = this.stage.getPointerPosition();

        var mousePointTo = {
          x: (pointer.x - this.stage.x()) / oldScale,
          y: (pointer.y - this.stage.y()) / oldScale,
        };

        var newScale =
          e.evt.deltaY > 0 ? oldScale * scaleBy : oldScale / scaleBy;

        this.stage.scale({ x: newScale, y: newScale });

        var newPos = {
          x: pointer.x - mousePointTo.x * newScale,
          y: pointer.y - mousePointTo.y * newScale,
        };
        this.stage.position(newPos);
      }
    });
  }

  /**
   * Init hand drawing feature.
   *
   * @private
   * @memberof UIPictureEdit
   */
  private initHandDrawing(): void {
    var lastLine: Line;

    this.stage.on('mousedown touchstart', () => {
      if (this.canPaint) {

        this.stage.container().style.cursor = 'crosshair';
        this.isPainting = true;

        var pos = this.stage.getPointerPosition();
        let pos2 = this.stage.getPosition();
        lastLine = new Line({
          stroke: this.currentColor,
          strokeWidth: parseInt(this.currentLineStrokeWidth),
          // globalCompositeOperation: 'source-over',
          points: [(pos.x - pos2.x) / this.stage.scaleX() - this.imageGroup.x(), (pos.y - pos2.y) / this.stage.scaleY() - this.imageGroup.y()],
          draggable: true
        });
        this.insertLine(lastLine);
        lastLine.setZIndex(1);
      }
    });

    this.stage.on('mouseup touchend', () => {
      this.isPainting = false;
      this.stage.container().style.cursor = 'default';
    });

    // and core function - drawing
    this.stage.on('mousemove touchmove', () => {
      if (!this.isPainting || !this.canPaint) {
        return;
      }
      this.stage.container().style.cursor = 'crosshair';

      const pos = this.stage.getPointerPosition();
      let pos2 = this.stage.getPosition();
      var newPoints = lastLine.points().concat([((pos.x - pos2.x) / this.stage.scaleX()) - this.imageGroup.x(), ((pos.y - pos2.y) / this.stage.scaleY()) - this.imageGroup.y()]);
      lastLine.points(newPoints);
    });
  }

  /**
   * Insert the given Line to the image group.
   * @param line 
   */
  private insertLine(lineNode: Line): void {
    lineNode.on('mouseenter', () => {
      if (!this.isPainting) {
        this.stage.container().style.cursor = 'move';
      }
    });
    lineNode.on('mouseleave', () => {
      this.stage.container().style.cursor = 'default';
    });

    this.setRemovable(lineNode);
    this.imageGroup.add(lineNode);
  }

  /**
   * Add text.
   */
  public async onClickAddText(): Promise<void> {
    if (this.canDeleteElements) this.onClickToggleDeleteElement();
    if (this.canPaint) this.onClickTogglePainting();

    let textString: string = null;
    await this.box.showInput('Text', 'Text').whenClosed(closeResult => {
      if (!closeResult.wasCancelled) {
        textString = closeResult.output as string;
      }
    });

    if (textString != null) {
      let text = new Text({
        align: 'center',
        verticalAlign: 'middle',
        text: textString,
        fill: this.currentColor,
        fontSize: 20,
        padding: 10,
        draggable: true
      });
      this.setRemovable(text);
      this.imageGroup.add(text);
    }
  }

  /**
   * Reset the stage zoom and center the image group.
   */
  public onClickRecenter(): void {
    this.resetZoom();
    this.centerImageGroup();
  }

  /**
   * Export the stage config as json and preview of the image group as DataUrl.
   * It triggers the onsave calling.
   *
   * @memberof UIPictureEdit
   */
  public async onClickSave(): Promise<void> {
    if (this.canDeleteElements) this.onClickToggleDeleteElement();
    if (this.canPaint) this.onClickTogglePainting();

    this.onClickRecenter();

    if (this.selectedNode != null) {
      this.selectedNode = null;
      this.updateTransformer();
    }

    this.isSaving = true;
    let base64: string = this.imageGroup.toDataURL();
    if (this.imageType == "url") {
      let newImageUrl = await this.fileStorageService.uploadBase64(this.imageUrl, base64);
      this.isSaving = false;
      if (newImageUrl != null) {
        this.logger.Log(this.i18n.tr("general.successfullySaved", { ns: "common" }), this.imageUrl, null, true);
        this.dialogController.ok(newImageUrl);
      } else {
        this.logger.LogError(this.i18n.tr("general.error", { ns: "common" }), this.imageUrl, null, true);
      }
    }
    else {
      //this.imageUrl = base64;
      this.dialogController.ok(base64);
    }
  }

  /**
   * Allow to delete some elements by clicking on it.
   */
  public onClickToggleDeleteElement(): void {
    if (this.canPaint) this.onClickTogglePainting();

    this.canDeleteElements = !this.canDeleteElements;
    if (this.canDeleteElements) {
      this.selectedNode = null;
      this.updateTransformer();
      this.stage.on('mouseenter', () => { this.stage.container().style.cursor = 'crosshair !important'; });
      this.stage.on('mouseleave', () => { this.stage.container().style.cursor = 'default'; });
    }
  }

  /**
   * Toggle canPaint.
   * If can paint, it resets the stage scale and recenter the image group.
   *
   * @memberof UIPictureEdit
   */
  public onClickTogglePainting(): void {
    if (this.canDeleteElements) this.onClickToggleDeleteElement();

    this.canPaint = !this.canPaint;
    if (this.canPaint) {
      this.toggleImageGroupDraggable(false);
    } else {
      this.toggleImageGroupDraggable(true);
    }
  }

  /**
   * Zoom in stage.
   *
   * @memberof UIPictureEdit
   */
  public onClickZoomIn(): void {
    let oldX = this.stage.scaleX();
    let oldY = this.stage.scaleY();

    this.stage.scale({ x: (oldX * 1.2), y: (oldY * 1.2) });
  }

  /**
   * Zoom out stage.
   *
   * @memberof UIPictureEdit
   */
  public onClickZoomOut(): void {
    let oldX = this.stage.scaleX();
    let oldY = this.stage.scaleY();

    this.stage.scale({ x: (oldX * .8), y: (oldY * .8) });
  }

  private setLineWidth(width: number) {
    this.currentLineStrokeWidth = width;
  }
}