/*
 * Copyright (C) 2018 Robert Bosch GmbH Copyright (C) 2018 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
 */

import { BOSCH_DDS_COLORS } from '@bci-web-core/core';
import type { Coordinate } from 'ol/coordinate';
import Feature from 'ol/Feature';
import LineString from 'ol/geom/LineString';
import MultiLineString from 'ol/geom/MultiLineString';
import Point from 'ol/geom/Point';
import Polygon from 'ol/geom/Polygon';
import Draw, { DrawEvent } from 'ol/interaction/Draw';
import Snap from 'ol/interaction/Snap';
import VectorLayer from 'ol/layer/Vector';
import MapBrowserEvent from 'ol/MapBrowserEvent';
import VectorSource from 'ol/source/Vector';
import { getDistance } from 'ol/sphere';
import CircleStyle from 'ol/style/Circle';
import Fill from 'ol/style/Fill';
import Stroke from 'ol/style/Stroke';
import Style from 'ol/style/Style';
import Text from 'ol/style/Text';
import { fromEvent, merge, Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { InteractionMode } from '../interaction-mode';

/**
 * Drawing guides for polygons and polylines.
 */
export class PolyGuidesInteractionMode extends InteractionMode {
  private snapLayer: VectorLayer;
  private snapLayerInteraction: Snap;
  private snapLayerPoints: VectorLayer;
  private snapLayerPointsInteraction: Snap;
  private drawnFeature: Feature;

  private snapLineLengthInMeters = 10000;

  private ngUnsubscribe = new Subject<void>();

  private metaInfoLayer: VectorLayer;
  private measurementFeature: Feature;

  private textStyleMeasurement = new Style({
    text: new Text({
      placement: 'point',
      offsetY: 15,
      offsetX: 30,
      font: '14px sans-serif',
      text: '',
    }),
  });

  constructor(private drawInteraction: Draw) {
    super();
  }

  activate() {
    const snapLayerStyle = [
      new Style({
        stroke: new Stroke({
          color: BOSCH_DDS_COLORS.boschGray80,
          lineDash: [1, 2],
          width: 1,
        }),
        image: new CircleStyle({
          radius: 1,
          fill: new Fill({
            color: BOSCH_DDS_COLORS.boschGray80,
          }),
        }) as any, // TODO: remove any
      }),
    ];

    this.snapLayer = new VectorLayer({
      source: new VectorSource(),
      style: snapLayerStyle,
    });
    this.snapLayerInteraction = new Snap({
      source: this.snapLayer.getSource(),
    });
    this.activateInteraction(this.snapLayerInteraction);

    this.snapLayerPoints = new VectorLayer({
      source: new VectorSource(),
      style: snapLayerStyle,
    });
    this.snapLayerPointsInteraction = new Snap({
      source: this.snapLayerPoints.getSource(),
    });
    this.activateInteraction(this.snapLayerPointsInteraction);
    this.metaInfoLayer = new VectorLayer({
      source: new VectorSource(),
      style: this.textStyleMeasurement,
    });

    // register start and end of polyline / polygon creation
    this.registerListener(
      this.drawInteraction.on('drawstart', (event: DrawEvent) => {
        this.map.addLayer(this.snapLayer);
        this.map.addLayer(this.snapLayerPoints);
        this.map.addLayer(this.metaInfoLayer);
        this.drawnFeature = event.feature;
        this.measurementFeature = new Feature(new MultiLineString([]));
        this.metaInfoLayer.getSource().addFeature(this.measurementFeature);
      })
    );
    this.registerListener(
      this.drawInteraction.on('drawend', () => {
        this.drawEnd();
      })
    );

    merge(fromEvent(window, 'keyup'), fromEvent(window, 'keydown'))
      .pipe(takeUntil(this.ngUnsubscribe))
      .subscribe((event) => {
        if (this.showSnaplinesCondition(event)) {
          this.updateSnapLayers();
        } else {
          this.clearSnapLayers();
        }
      });

    const pointerMoveSubject = new Subject<void>();
    pointerMoveSubject
      .pipe(takeUntil(this.ngUnsubscribe), debounceTime(10))
      .subscribe(() => this.showMeasurementOfLastSegment());
    this.registerListener(this.map.on('pointermove', () => pointerMoveSubject.next()));

    // update snap guides
    this.registerListener(
      this.map.on('singleclick', (event: MapBrowserEvent) => {
        this.showSnaplinesCondition(event.originalEvent) ? this.updateSnapLayers() : this.clearSnapLayers();
      })
    );

    super.activate();
  }

  deactivate() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
    this.drawEnd();
    super.deactivate();
  }

  protected showSnaplinesCondition(event) {
    return event.ctrlKey || event.metaKey;
  }

  private drawEnd() {
    this.clearSnapLayers();
    this.map.removeLayer(this.snapLayer);
    this.map.removeLayer(this.snapLayerPoints);
    this.drawnFeature = undefined;

    this.map.removeLayer(this.metaInfoLayer);
    this.metaInfoLayer.getSource().clear();
    this.measurementFeature = undefined;
  }

  private clearSnapLayers() {
    this.snapLayer.getSource().clear();
    this.snapLayerPoints.getSource().clear();
  }

  private updateSnapLayers() {
    if (!this.drawnFeature) {
      return;
    }

    const geometry = this.drawnFeature.getGeometry();
    this.clearSnapLayers();

    let pointBeforeOrigin: Coordinate;
    let lineOrigin: Coordinate;
    let coordinates: Coordinate[];
    let coordinateOffset: number;
    if (geometry instanceof Polygon) {
      coordinates = geometry.getCoordinates()[0];
      coordinateOffset = 3;
    } else if (geometry instanceof LineString) {
      coordinates = geometry.getCoordinates();
      coordinateOffset = 2;
    } else {
      return;
    }

    if (coordinates.length > coordinateOffset) {
      pointBeforeOrigin = coordinates[coordinates.length - (coordinateOffset + 1)];
      lineOrigin = coordinates[coordinates.length - coordinateOffset];

      if (!!lineOrigin && !!pointBeforeOrigin) {
        // lines for start point
        const lines = this.getSnapLinesForSegment(coordinates[1], coordinates[0]);

        // lines for last drawn point
        lines.push(...this.getSnapLinesForSegment(pointBeforeOrigin, lineOrigin));

        // draw all lines
        this.snapLayer.getSource().addFeatures(lines.map((line) => new Feature({ geometry: new LineString(line) })));

        // add point for each line intersection in order to snap there
        lines.forEach((line) => {
          this.snapLayerPoints.getSource().addFeatures(
            lines
              .map((otherLine) => this.intersection(line, otherLine))
              .filter((intersection) => !!intersection)
              .map(
                (intersection) =>
                  new Feature({
                    geometry: new Point(<Coordinate>intersection),
                  })
              )
          );
        });
      }
    } else if (coordinates.length === coordinateOffset) {
      // only one vertex defined, other vertices are the mouse position and in case of polygons the closing point
      const point = coordinates[0];
      const lines = this.getSnapLinesForSegment(point, [point[0] + 0.00001, point[1]], 5);
      this.snapLayer.getSource().addFeatures(lines.map((line) => new Feature({ geometry: new LineString(line) })));
    }
  }

  private getSnapLinesForSegment(
    pointBeforeOrigin: Coordinate,
    lineOrigin: Coordinate,
    angleOffsetInDegree = 45
  ) {
    const [dx, dy] = this.directionVector(pointBeforeOrigin, lineOrigin);
    const lines = [];
    for (let angleInDegree = 0; angleInDegree < 180; angleInDegree += angleOffsetInDegree) {
      const angleInRadians = (angleInDegree * Math.PI) / 180;
      const cosAlpha = Math.cos(angleInRadians);
      const sinAlpha = Math.sin(angleInRadians);
      const dxRotated = cosAlpha * dx - sinAlpha * dy;
      const dyRotated = sinAlpha * dx + cosAlpha * dy;
      const lineDestination = [
        lineOrigin[0] + dxRotated * this.snapLineLengthInMeters,
        lineOrigin[1] + dyRotated * this.snapLineLengthInMeters,
      ];
      const lineStart = [
        lineOrigin[0] - dxRotated * this.snapLineLengthInMeters,
        lineOrigin[1] - dyRotated * this.snapLineLengthInMeters,
      ];
      lines.push([lineStart, lineDestination]);
    }
    return lines;
  }

  private directionVector(from: Coordinate, to: Coordinate) {
    const dx = to[0] - from[0];
    const dy = to[1] - from[1];
    const length = Math.sqrt(dx * dx + dy * dy);
    return [dx / length, dy / length];
  }

  private intersection([[xA1, yA1], [xA2, yA2]], [[xB1, yB1], [xB2, yB2]]): Coordinate | null {
    const dxA = xA2 - xA1;
    const dxB = xB2 - xB1;
    const dyA = yA2 - yA1;
    const dyB = yB2 - yB1;
    const denominator = dxA * dyB - dyA * dxB;

    if (denominator === 0) {
      // lines are parallel or coincident
      return null;
    }

    const dx1 = xA1 - xB1;
    const dy1 = yA1 - yB1;
    const uA = (dxB * dy1 - dyB * dx1) / denominator;
    const uB = (dxA * dy1 - dyA * dx1) / denominator;
    if (uA < 0 || uA > 1 || uB < 0 || uB > 1) {
      return null;
    }

    const x = xA1 + uA * dxA;
    const y = yA1 + uA * dyA;
    return [x, y];
  }

  private showMeasurementOfLastSegment() {
    if (!this.drawnFeature) {
      return;
    }

    const geometry = this.drawnFeature.getGeometry();
    const isPolygon = geometry instanceof Polygon;
    if (isPolygon || geometry instanceof LineString) {
      const offset = isPolygon ? 2 : 1;
      const mapCoordinates = isPolygon
        ? (<Polygon>geometry).getCoordinates()[0]
        : (<LineString>geometry).getCoordinates();
      if (mapCoordinates.length < offset + 1) {
        return;
      }
      const wgs84Geometry = new LineString([
        mapCoordinates[mapCoordinates.length - offset],
        mapCoordinates[mapCoordinates.length - (offset + 1)],
      ]);
      wgs84Geometry.transform(this.map.getView().getProjection(), 'EPSG:4326');
      const wgs84Coordinates = wgs84Geometry.getCoordinates();

      const coordinateEnd = wgs84Coordinates[0];
      const coordinateBeforeEnd = wgs84Coordinates[1];

      const distance = getDistance(coordinateEnd, coordinateBeforeEnd);
      const dEW = coordinateEnd[0] - coordinateBeforeEnd[0];
      const dNS = coordinateEnd[1] - coordinateBeforeEnd[1];
      const distanceNorthSouth =
        Math.sign(dNS) * getDistance([coordinateBeforeEnd[0], coordinateEnd[1]], coordinateBeforeEnd);
      const distanceEastWest =
        Math.sign(dEW) * getDistance([coordinateEnd[0], coordinateBeforeEnd[1]], coordinateBeforeEnd);
      const arccos = distance === 0 ? 0 : Math.acos(distanceEastWest / distance);
      const angleInRadians = dNS > 0 ? arccos : 2 * Math.PI - arccos;
      const angleInDegree = (180 * angleInRadians) / Math.PI;

      const metaGeometry = <MultiLineString>this.measurementFeature.getGeometry();
      metaGeometry.getCoordinates().length = 0;
      metaGeometry.setCoordinates([
        [mapCoordinates[mapCoordinates.length - offset], mapCoordinates[mapCoordinates.length - offset]],
      ]);
      this.measurementFeature.setStyle(() => {
        this.textStyleMeasurement
          .getText()
          .setText(
            distance.toFixed(3) +
              'm, ' +
              angleInDegree.toFixed(1) +
              '°, X: ' +
              distanceEastWest.toFixed(3) +
              'm, Y: ' +
              distanceNorthSouth.toFixed(3) +
              'm'
          );
        return this.textStyleMeasurement;
      });
    }
  }
}
