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

import {
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  EventEmitter,
  Input,
  OnDestroy,
  OnInit,
  Output,
  ViewChild,
  ViewEncapsulation,
} from '@angular/core';
import { Map, View } from 'ol';
import { defaults as controlDefaults } from 'ol/control';
import FullScreen from 'ol/control/FullScreen';
import type { Coordinate } from 'ol/coordinate';
import { EventsKey } from 'ol/events';
import type { Extent } from 'ol/extent';
import Geometry from 'ol/geom/Geometry';
import TileLayer from 'ol/layer/Tile';
import BaseLayer from 'ol/layer/Base';
import { unByKey } from 'ol/Observable';
import type { ProjectionLike } from 'ol/proj';
import { addCommon, fromLonLat, toLonLat } from 'ol/proj';
import OSM from 'ol/source/OSM';
import { ViewOptions } from 'ol/View';
import { interval, Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';
import { projectionFrom, Projections, projectionTo } from '../projection.utils';

export type BciMapsTheme = 'light' | 'dark';

@Component({
  selector: 'bci-open-layers-map',
  templateUrl: './open-layers-map.component.html',
  styleUrls: ['./open-layers-map.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None, // tslint:disable-line:use-view-encapsulation
})
export class OpenLayersMapComponent implements OnInit, OnDestroy {
  public static readonly OL_MAXZOOM = 50;
  public static readonly OL_MINZOOM = 1;
  @ViewChild('mapContainer', { static: true }) mapContainer: ElementRef;
  @Output() loaded = new EventEmitter<boolean>();
  @Output() zoomChange = new EventEmitter<number>();
  @Output() centerChange = new EventEmitter<Coordinate>();
  @Output() rotationChange = new EventEmitter<number>();
  /**
   * Initializes map. Defaults to true. If set to false, call initializeMap() to manually initialize map.
   * When done, the loaded event is emitted with true.
   */
  @Input() autoInit = true;
  /**
   * By default the center input field is converted from lon and lat. If you want to use your own conversion, set this to false.
   */
  @Input() centerFromLonLat = true;
  /**
   * Projection to use. Defaults to EPSG:3857 (WGS84 projection).
   */
  @Input() projection = Projections.EPSG3857_WGS84PROJECTED;
  /**
   * Choose between 'dark' (default) and 'light'.
   */
  @Input() theme: BciMapsTheme = 'dark';

  /**
   * Choose to show layer attribution or not.
   */
  @Input() showAttribution = true;

  private resolutionSubject = new Subject<void>();
  private osmLayer: TileLayer;
  private centerSubject = new Subject<void>();
  private rotationSubject = new Subject<void>();
  private ngUnsubscribe = new Subject<void>();
  private olListeners: EventsKey[] = [];

  private _maxExtent: Extent;
  private _minZoom = 3;
  private _maxZoom = 7;
  private _rotation = 0;
  private _zoom = 4;
  private _map: Map;
  private _center: Coordinate = [0, 0];

  constructor() {
    // TODO remove once upgraded to ol 5.4+, see https://github.com/openlayers/openlayers/issues/8815
    addCommon();
  }

  get maxExtent() {
    return this._maxExtent;
  }

  @Input()
  set maxExtent(extent: Extent) {
    this._maxExtent = extent;
    if (!!this._map) {
      this._map.getView().fit(extent);
    }
  }

  get minZoom() {
    return this._minZoom;
  }

  @Input()
  set minZoom(minZoom: number) {
    this._minZoom = minZoom;
    if (!!this._map) {
      this._map.getView().setMinZoom(minZoom);
    }
  }

  get maxZoom() {
    return this._maxZoom;
  }

  @Input()
  set maxZoom(maxZoom: number) {
    this._maxZoom = maxZoom;
    if (!!this._map) {
      this._map.getView().setMaxZoom(maxZoom);
    }
  }

  /**
   * Get the rotation in degrees.
   * @returns rotation in degrees
   */
  get rotation() {
    return Math.round(((this._rotation * 180) / Math.PI) * 100) / 100;
  }

  /**
   * Set the rotation in degrees.
   * @param rotation in degrees
   */
  @Input()
  set rotation(rotation: number) {
    this._rotation = (rotation * Math.PI) / 180;
    if (!!this._map) {
      this._map.getView().animate({
        rotation: this._rotation,
        duration: 250,
      });
    }
    this.rotationChange.emit(this.rotation);
  }

  get zoom() {
    return this._zoom;
  }

  /**
   * Set the zoom of the map (must be between minZoom and maxZoom).
   */
  @Input()
  set zoom(zoom: number) {
    this.validateInputs();
    this._zoom = zoom;
    if (!!this._map) {
      this._map.getView().animate({
        zoom: this._zoom,
        duration: 250,
      });
    }
    this.zoomChange.emit(this._zoom);
  }

  get map(): Map {
    return this._map;
  }

  get center() {
    return this._center;
  }

  /**
   * Set the center of the map.
   */
  @Input()
  set center(center: Coordinate) {
    this._center = center;
    if (!!this._map) {
      this._map.getView().animate({
        center: this.setCenterFromLonLat(this._center),
        duration: 250,
      });
    }
    this.centerChange.emit(center);
  }

  public isDarkTheme(): boolean {
    return this.theme === 'dark';
  }

  ngOnInit() {
    this.validateInputs();
    if (this.autoInit) {
      this.initializeMap();
    }
  }

  ngOnDestroy() {
    unByKey(this.olListeners);
    this.olListeners.length = 0;
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
    if (this._map) {
      this._map.getControls().forEach((c) => this._map.removeControl(c));
      this._map.setTarget(null);
    }
  }

  /**
   * Toggles fullscreen mode.
   * @todo To be implemented by ng-core.
   */
  public toggleFullScreen() {
    // this._fullscreenService.toggleFullScreen(this.mapContainer);
  }

  /**
   * Initializes the map with the view options (center, zoom, extent, projection, min and max zoom),
   * zoom controls and a fullscreen control.
   * The loaded event emits 'true' when initialization is done.
   */
  public initializeMap() {
    const viewOptions: ViewOptions = {
      center: this.setCenterFromLonLat(this._center),
      zoom: this._zoom,
      extent: this._maxExtent,
      maxZoom: this._maxZoom,
      minZoom: this._minZoom,
      projection: this.projection,
    };
    const view = new View(viewOptions);
    this._map = new Map({
      target: this.mapContainer.nativeElement,
      view: view,
      // logo: false,
      controls: controlDefaults({
        attribution: this.showAttribution,
        attributionOptions: {
          collapsible: false,
        },
      }).extend([
        new FullScreen({
          label: '',
          labelActive: '',
        }),
      ]),
    });
    this.olListeners.push(this._map.once('postcompose', () => this.loaded.emit(true)));

    this.olListeners.push(view.on('change:resolution', () => this.resolutionSubject.next()));
    this.resolutionSubject
      .asObservable()
      .pipe(takeUntil(this.ngUnsubscribe), debounceTime(251))
      .subscribe(() => {
        this._zoom = this._map.getView().getZoom();
        this.zoomChange.emit(this._zoom);
      });

    this.olListeners.push(view.on('change:center', () => this.centerSubject.next()));
    this.centerSubject
      .asObservable()
      .pipe(takeUntil(this.ngUnsubscribe), debounceTime(251))
      .subscribe(() => {
        this._center = toLonLat(this._map.getView().getCenter());
        this.centerChange.emit(this._center);
      });
    this.olListeners.push(view.on('change:rotation', () => this.rotationSubject.next()));
    this.rotationSubject
      .asObservable()
      .pipe(takeUntil(this.ngUnsubscribe), debounceTime(251))
      .subscribe(() => {
        this._rotation = this._map.getView().getRotation();
        this.rotationChange.emit(this.rotation);
      });

    // keep map from being distorted after routing
    setTimeout(() => this._map.updateSize(), 0);

    // Temporary workaround to fix distorted maps until resize event is thrown when needed
    interval(100)
      .pipe(takeUntil(this.ngUnsubscribe))
      .subscribe(() => {
        const viewport = this.map.getViewport();
        const [width, height] = this.map.getSize();
        if (viewport.clientWidth !== width || viewport.clientHeight !== height) {
          this.map.updateSize();
        }
      });
  }

  /**
   * Adds a new openlayers layer to the map.
   * @param layerToAdd Layer to add
   */
  addLayer(layerToAdd: BaseLayer) {
    this._map.addLayer(layerToAdd);
  }

  /**
   * Removes a layer from the map if existent.
   * @param layerToRemove Layer reference to remove
   */
  removeLayer(layerToRemove: BaseLayer) {
    this._map.removeLayer(layerToRemove);
  }

  /**
   * Adds an OpenStreetMap layer to the map if not already added
   */
  addOSM(zIndex = 0, opacity = 1) {
    if (!this.osmLayer) {
      this.osmLayer = new TileLayer({
        source: new OSM(),
      });
      this.addLayer(this.osmLayer);
    } else if (this.map.getLayers().getArray().indexOf(this.osmLayer) === -1) {
      this.addLayer(this.osmLayer);
    }

    this.osmLayer.setZIndex(zIndex);
    this.osmLayer.setOpacity(opacity);
  }

  projectionFrom<T extends Geometry>(sourceProjection: ProjectionLike) {
    return projectionTo<T>(this.projection).from(sourceProjection);
  }

  projectionTo<T extends Geometry>(targetProjection: ProjectionLike) {
    return projectionFrom<T>(this.projection).to(targetProjection);
  }

  private setCenterFromLonLat(center: Coordinate): Coordinate {
    return this.centerFromLonLat ? fromLonLat(center) : center;
  }

  private validateInputs() {
    this._minZoom = Math.round(this._minZoom);
    this._maxZoom = Math.round(this._maxZoom);
    this._zoom = Math.round(this._zoom);

    this._minZoom = this.setZoomToRange(this._minZoom);
    this._maxZoom = this.setZoomToRange(this._maxZoom);
    this._zoom = this.setZoomToRange(this._zoom);

    if (this._minZoom > this.maxZoom) {
      this._minZoom = this._maxZoom;
      this._zoom = this._maxZoom;
      console.warn('OpenLayersMapComponent: minZoom greater than maxZoom. MinZoom and zoom set to MaxZoom.');
    }
    if (this._zoom < this._minZoom) {
      this._zoom = this._minZoom;
      console.warn('OpenLayersMapComponent: zoom lower than minZoom. Zoom set to MinZoom.');
    } else if (this._zoom > this._maxZoom) {
      this._zoom = this._maxZoom;
      console.warn('OpenLayersMapComponent: zoom greater than maxZoom. Zoom set to MaxZoom.');
    }
  }

  private setZoomToRange(zoom: number) {
    if (zoom < OpenLayersMapComponent.OL_MINZOOM) {
      console.warn(
        `OpenLayersMapComponent: minZoom, maxZoom and zoom cannot be lower than ${OpenLayersMapComponent.OL_MINZOOM}. ` +
          `Set to ${OpenLayersMapComponent.OL_MINZOOM}.`
      );
      zoom = OpenLayersMapComponent.OL_MINZOOM;
    } else if (zoom > OpenLayersMapComponent.OL_MAXZOOM) {
      console.warn(`OpenLayersMapComponent: minZoom, maxZoom and zoom
      cannot be greater than ${OpenLayersMapComponent.OL_MAXZOOM}. Set to ${OpenLayersMapComponent.OL_MAXZOOM}.`);
      zoom = OpenLayersMapComponent.OL_MAXZOOM;
    }
    return zoom;
  }
}
