/*
 * 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 {ElementRef, Injectable} from '@angular/core';
import {
  SCBuilding,
  SCSearchFilter,
  SCSearchQuery,
  SCSearchResponse,
  SCThingType,
  SCUuid,
} from '@openstapps/core';
import {Point, Polygon} from 'geojson';
import {divIcon, geoJSON, LatLng, Map, marker, Marker} from 'leaflet';
import {DataProvider} from '../data/data.provider';
import {MapPosition, PositionService} from './position.service';
import {hasValidLocation} from '../data/types/place/place-types';
import {ConfigProvider} from '../config/config.provider';
import {SCIcon} from '../../util/ion-icon/icon';

/**
 * Provides methods for presenting the map
 */
@Injectable({
  providedIn: 'root',
})
export class MapProvider {
  /**
   * Area to show when the map is initialized (shown for the first time)
   */
  defaultPolygon: Polygon;

  /**
   * Provide a point marker for a leaflet map
   *
   * @param point Point to get marker for
   * @param className CSS class name
   * @param iconSize Size of the position icon
   */
  static getPointMarker(point: Point, className: string, iconSize: number) {
    return marker(geoJSON(point).getBounds().getCenter(), {
      icon: divIcon({
        className: className,
        html: `<span
                 name="${SCIcon`location_on`}"
                 class="material-symbols-rounded map-location-pin"
                 style="font-size: ${iconSize}px;"
               >${SCIcon`location_on`}</span>`,
        iconSize: [iconSize, iconSize],
        iconAnchor: [iconSize / 2, iconSize],
      }),
    });
  }

  /**
   * Provide a position marker for a leaflet map
   *
   * @param position Current position
   * @param className CSS class name
   * @param iconSize Size of the position icon
   */
  static getPositionMarker(position: MapPosition, className: string, iconSize: number) {
    return new Marker(new LatLng(position.latitude, position.longitude), {
      icon: divIcon({
        className: className,
        html:
          typeof position.heading !== 'undefined'
            ? `<span
                 class="material-symbols-rounded map-location-pin"
                 style="
                   transform-origin: center;
                   transform: rotate(${position.heading}deg);
                   font-size: ${iconSize}px;
                   color: var(--ion-color-primary);
                 "
               >${SCIcon`navigation`}</span>`
            : `<span
                 name="${SCIcon`person_pin_circle`}"
                 class="material-symbols-rounded map-location-pin"
                 style="font-size: ${iconSize}px; color: var(--ion-color-primary);"
               >${SCIcon`person_pin_circle`}</span>`,
        iconSize: [iconSize, iconSize],
      }),
      zIndexOffset: 1000,
    });
  }

  /**
   * Fixes the issue of missing tiles when map renders before its container element
   *
   * @param map The initialized map
   * @param element The element containing the map
   * @param interval Interval to clear when map's appearance is corrected
   */
  static invalidateWhenRendered = (map: Map, element: ElementRef, interval: number) => {
    if (element.nativeElement.offsetWidth === 0) {
      return;
    }

    // map's container is ready
    map.invalidateSize();
    // stop repeating when it's rendered and invalidateSize done
    clearInterval(interval);
  };

  constructor(
    private dataProvider: DataProvider,
    private positionService: PositionService,
    private configProvider: ConfigProvider,
  ) {
    this.defaultPolygon = this.configProvider.getValue('campusPolygon') as Polygon;
  }

  /**
   * Provide the specific place by its UID
   *
   * @param uid UUID of the place to look for
   */
  async searchPlace(uid: SCUuid): Promise<SCSearchResponse> {
    const uidFilter: SCSearchFilter = {
      arguments: {
        field: 'uid',
        value: uid,
      },
      type: 'value',
    };

    return this.dataProvider.search({filter: uidFilter});
  }

  /**
   * Provide places (buildings and canteens)    const result = await this.dataProvider.search(query);
   *
   * @param contextFilter Additional contextual filter (e.g. from the context menu)
   * @param queryText Query (text) of the search query
   */
  async searchPlaces(contextFilter?: SCSearchFilter, queryText?: string): Promise<SCSearchResponse> {
    const buildingFilter: SCSearchFilter = {
      arguments: {
        field: 'type',
        value: SCThingType.Building,
      },
      type: 'value',
    };

    const mensaFilter: SCSearchFilter = {
      arguments: {
        filters: [
          {
            arguments: {
              field: 'categories',
              value: 'canteen',
            },
            type: 'value',
          },
          {
            arguments: {
              field: 'categories',
              value: 'student canteen',
            },
            type: 'value',
          },
          {
            arguments: {
              field: 'categories',
              value: 'cafe',
            },
            type: 'value',
          },
          {
            arguments: {
              field: 'categories',
              value: 'restaurant',
            },
            type: 'value',
          },
        ],
        operation: 'or',
      },
      type: 'boolean',
    };

    // initial filter for the places
    const baseFilter: SCSearchFilter = {
      arguments: {
        operation: 'or',
        filters: [buildingFilter, mensaFilter],
      },
      type: 'boolean',
    };

    let filter = baseFilter;

    if (typeof contextFilter !== 'undefined') {
      filter = {
        arguments: {
          operation: 'and',
          filters: [baseFilter, contextFilter],
        },
        type: 'boolean',
      };
    }

    const query: SCSearchQuery = {
      filter,
    };

    if (queryText && queryText.length > 0) {
      query.query = queryText;
    }

    if (this.positionService.position) {
      query.sort = [
        {
          type: 'distance',
          order: 'asc',
          arguments: {
            field: 'geo',
            position: [this.positionService.position.longitude, this.positionService.position.latitude],
          },
        },
      ];
    }

    const result = await this.dataProvider.search(query);

    result.data = result.data.filter(place => hasValidLocation(place as SCBuilding));

    return result;
  }
}
