/*
 * Copyright (C) 2022 StApps
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 */
import {Location} from '@angular/common';
import {ChangeDetectorRef, Component, ElementRef, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Keyboard} from '@capacitor/keyboard';
import {AlertController, IonRouterOutlet, ModalController} from '@ionic/angular';
import {TranslateService} from '@ngx-translate/core';
import {SCBuilding, SCPlace, SCRoom, SCSearchFilter, SCUuid} from '@openstapps/core';
import {featureGroup, geoJSON, LatLng, Layer, Map, MapOptions, Marker, tileLayer} from 'leaflet';
import {Subscription} from 'rxjs';
import {DataRoutingService} from '../../data/data-routing.service';
import {ContextMenuService} from '../../menu/context/context-menu.service';
import {MapProvider} from '../map.provider';
import {MapPosition, PositionService} from '../position.service';
import {Geolocation, PermissionStatus} from '@capacitor/geolocation';
import {Capacitor} from '@capacitor/core';

/**
 * The main page of the map
 */
@Component({
  styleUrls: ['./map-page.scss'],
  templateUrl: './map-page.html',
  providers: [ContextMenuService],
})
export class MapPageComponent {
  /**
   * Default map zoom level
   */
  DEFAULT_ZOOM = 16;

  /**
   * Distance to the shown place
   */
  distance?: number;

  /**
   * Api query filter
   */
  filterQuery?: SCSearchFilter;

  /**
   * Places to show
   */
  items: SCPlace[] = [];

  /**
   * Leaflet (map) layers to show items on (not the position)
   */
  layers: Layer[] = [];

  /**
   * Location settings on the user's device
   */
  locationStatus?: PermissionStatus;

  /**
   * The leaflet map
   */
  map: Map;

  /**
   * Container element of the map
   */
  @ViewChild('mapContainer') mapContainer: ElementRef;

  /**
   * Map layers to show as marker clusters
   */
  markerClusterData: Layer[] = [];

  /**
   * Options how to show the marker clusters
   */
  markerClusterOptions = {
    // don't show rectangles containing the markers in a cluster
    showCoverageOnHover: false,
  };

  /**
   * Maximal map zoom level
   */
  MAX_ZOOM = 18;

  /**
   * Options of the leaflet map
   */
  options: MapOptions;

  /**
   * Position of the user on the map
   */
  position: MapPosition | null;

  /**
   * Marker showing the position of the user on the map
   */
  positionMarker: Marker;

  /**
   * Search value from search bar
   */
  queryText: string;

  /**
   * Subscriptions used by the page
   */
  subscriptions: Subscription[] = [];

  constructor(
    private translateService: TranslateService,
    private router: Router,
    private mapProvider: MapProvider,
    public location: Location,
    private ref: ChangeDetectorRef,
    private readonly contextMenuService: ContextMenuService,
    private alertController: AlertController,
    private route: ActivatedRoute,
    private modalController: ModalController,
    private dataRoutingService: DataRoutingService,
    private positionService: PositionService,
    readonly routerOutlet: IonRouterOutlet,
  ) {
    // initialize the options
    this.options = {
      center: geoJSON(this.mapProvider.defaultPolygon).getBounds().getCenter(),
      layers: [
        tileLayer('https://osm.server.uni-frankfurt.de/tiles/roads/x={x}&y={y}&z={z}', {
          attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
          maxZoom: this.MAX_ZOOM,
        }),
      ],
      zoom: this.DEFAULT_ZOOM,
      zoomControl: false,
    };
  }

  /**
   * Animate to coordinates
   *
   * @param latLng Coordinates to animate to
   */
  private focus(latLng?: LatLng) {
    if (latLng !== undefined) {
      this.map.flyTo(latLng, this.MAX_ZOOM);

      return;
    }
  }

  /**
   * Add places to the map
   *
   * @param items Places to add to the map
   * @param clean Remove everything from the map first
   * @param focus Animate to the item(s)
   */
  addToMap(items: SCPlace[], clean = false, focus = false) {
    if (clean) {
      this.removeAll();
    }
    const addSCPlace = (place: SCPlace): Layer | Marker => {
      if (place.geo.polygon !== undefined) {
        const polygonLayer = geoJSON(place.geo.polygon, {
          // eslint-disable-next-line @typescript-eslint/ban-ts-comment
          // @ts-ignore
          bubblingMouseEvents: false,
        }).getLayers()[0];

        return polygonLayer.on('click', this.showItem.bind(this, place.uid));
      }

      const markerLayer = MapProvider.getPointMarker(place.geo.point, 'stapps-location', 32);

      return markerLayer.on('click', this.showItem.bind(this, place.uid));
    };

    items.map(thing => {
      // IMPORTANT: change this to support inPlace.geo when there is a need to show floors (the building of the floor)
      if (thing.geo !== undefined) {
        this.layers.push(addSCPlace(thing as SCPlace));
      }
    });

    this.markerClusterData = this.layers;

    if (!focus || this.items.length === 0) {
      return;
    }

    if (this.items.length === 1) {
      this.focus(geoJSON(this.items[0].geo.point).getBounds().getCenter());

      return;
    }

    const groupedLayers = featureGroup(this.layers);
    this.map.flyToBounds(groupedLayers.getBounds());
  }

  /**
   * Fetches items with set query configuration
   *
   * @param fetchAll Should fetch all items
   * @param animate Should the fly animation be used
   */
  async fetchAndUpdateItems(fetchAll = false, animate?: boolean): Promise<void> {
    try {
      const result = await this.mapProvider.searchPlaces(this.filterQuery, fetchAll ? '' : this.queryText);
      if (result.data.length === 0) {
        const alert = await this.alertController.create({
          buttons: [this.translateService.instant('ok')],
          header: this.translateService.instant('search.nothing_found'),
        });
        await alert.present();

        return;
      }
      // override items with results
      this.items = result.data as SCPlace[];
      this.addToMap(result.data as Array<SCBuilding | SCRoom>, true, animate);
      // update filter options if result contains facets
      if (result.facets !== undefined) {
        this.contextMenuService.updateContextFilter(result.facets);
      }
    } catch (error) {
      const alert: HTMLIonAlertElement = await this.alertController.create({
        buttons: ['Dismiss'],
        header: 'Error',
        subHeader: (error as Error).message,
      });

      await alert.present();
    }
  }

  /**
   * Hides keyboard in native app environments
   */
  hideKeyboard() {
    if (Capacitor.isNativePlatform()) {
      Keyboard.hide();
    }
  }

  /**
   * Subscribe to needed observables and get the location status when user is entering the page
   */
  async ionViewWillEnter() {
    if (this.positionService.position) {
      this.position = this.positionService.position;
      this.positionMarker = MapProvider.getPositionMarker(this.position, 'stapps-device-location', 32);
    }

    this.subscriptions.push(
      this.dataRoutingService.itemSelectListener().subscribe(async item => {
        // in case the list item is clicked
        if (this.items.length > 1) {
          await Promise.all([this.modalController.dismiss(), this.showItem(item.uid)]);
        } else {
          void this.router.navigate(['/data-detail', item.uid]);
        }
      }),
      this.positionService
        .watchCurrentLocation(this.constructor.name, {enableHighAccuracy: true, maximumAge: 1000})
        .subscribe({
          next: (position: MapPosition) => {
            this.position = position;
            this.positionMarker = MapProvider.getPositionMarker(position, 'stapps-device-location', 32);
          },
          error: async _error => {
            this.locationStatus = await Geolocation.checkPermissions();
            // eslint-disable-next-line unicorn/no-null
            this.position = null;
          },
        }),
    );

    // get detailed location status (diagnostics only supports devices)
    this.locationStatus = await Geolocation.checkPermissions();
  }

  /**
   * Unsubscribe from all subscriptions when user leaves page
   */
  ionViewWillLeave() {
    void this.positionService.clearWatcher(this.constructor.name);
    for (const sub of this.subscriptions) {
      sub.unsubscribe();
    }
  }

  /**
   * What happens when the leaflet map is ready (note: doesn't mean that tiles are loaded)
   */
  async onMapReady(map: Map) {
    this.map = map;
    const interval = window.setInterval(() =>
      MapProvider.invalidateWhenRendered(map, this.mapContainer, interval),
    );

    const uid = this.route.snapshot.paramMap.get('uid');
    const response = await (uid === null
      ? this.mapProvider.searchPlaces()
      : this.mapProvider.searchPlace(uid));

    if (response.data.length === 0) {
      return;
    }

    this.items = response.data as SCBuilding[];
    this.addToMap(this.items, true, uid !== null);
    this.contextMenuService.updateContextFilter(response.facets);

    this.subscriptions.push(
      this.contextMenuService.filterQueryChanged$.subscribe(query => {
        this.filterQuery = query;
        this.fetchAndUpdateItems(false, true);
      }),
    );

    this.distance = this.positionService.getDistance(this.items[0].geo.point);
  }

  /**
   * What happens when position button is clicked
   */
  async onPositionClick() {
    if (this.position) {
      this.focus(this.positionMarker.getLatLng());

      return;
    }

    this.locationStatus = await (Capacitor.isNativePlatform()
      ? Geolocation.requestPermissions()
      : Geolocation.checkPermissions());

    this.translateService
      .get(['map.page.geolocation', 'app.errors.UNKNOWN'])
      .subscribe(async translations => {
        const [location, unknownError] = [
          translations['map.page.geolocation'],
          translations['app.errors.UNKNOWN'],
        ];
        await (
          await this.alertController.create({
            header: location.TITLE,
            message: `${
              this.locationStatus?.location === 'denied'
                ? location.NOT_ALLOWED
                : this.locationStatus?.location === 'granted'
                ? unknownError
                : location.NOT_ENABLED
            }`,
            buttons: ['OK'],
          })
        ).present();
      });
  }

  /**
   * Remove all of the layers
   */
  removeAll() {
    for (const layer of this.layers) {
      this.map.removeLayer(layer);
    }

    this.layers = [];
  }

  /**
   * Resets the map = fetch all the items based on the filters (and go to component's base location)
   */
  async resetView() {
    this.location.go('/map');
    await this.fetchAndUpdateItems(this.items.length > 0);

    this.ref.detectChanges();
  }

  /**
   * On enter key up do the search
   *
   * @param event Keyboard keyup event
   */
  searchKeyUp(event: KeyboardEvent) {
    if (event.key === 'Enter') {
      this.searchStringChanged((event.target as HTMLInputElement).value);
    }
  }

  /**
   * Search event of search bar
   *
   * @param queryText New query text to be set
   */
  searchStringChanged(queryText?: string) {
    this.queryText = queryText || '';
    void this.fetchAndUpdateItems(false, true);
  }

  /**
   * Show an single place
   *
   * @param uid Uuid of the place
   */
  async showItem(uid: SCUuid) {
    const response = await this.mapProvider.searchPlace(uid);
    this.items = response.data as SCPlace[];
    this.distance = this.positionService.getDistance(this.items[0].geo.point);
    this.addToMap(this.items, true);
    this.ref.detectChanges();
    const url = this.router.createUrlTree(['/map', uid]).toString();
    this.location.go(url);
    // center the selected place
    this.focus(geoJSON(this.items[0].geo.point).getBounds().getCenter());
  }
}
