import { Injectable } from '@angular/core';

import { Store, Action, createFeatureSelector } from '@ngrx/store';
import { ROUTER_NAVIGATED, RouterReducerState } from '@ngrx/router-store';
import * as fromRouter from '@ngrx/router-store';
import { createEffect, Actions, ofType } from '@ngrx/effects';

import { EMPTY, Observable, of } from 'rxjs';
import { withLatestFrom, switchMap, map, catchError, mergeMap } from 'rxjs/operators';

import { GeoJSON, WKT } from 'ol/format';
import { Feature } from 'ol';
import { getCenter, createEmpty, extend } from 'ol/extent';
import { Point, Geometry } from 'ol/geom'


import * as mapActions from '../actions/map.actions';
import * as mapReducers from '../reducers/map.reducer';
import { commonReducers } from '@farmmaps/common';

import { commonActions } from '@farmmaps/common';

import { IItem } from '@farmmaps/common';
import { FolderService, ItemService } from '@farmmaps/common';
import { tassign } from 'tassign';

import { FeatureIconService } from '../services/feature-icon.service';

import * as style from 'ol/style';

import { ItemTypeService, IQueryState } from '@farmmaps/common';
import { TemporalItemLayer } from '../models/item.layer'

export const getRouterState = createFeatureSelector<RouterReducerState>('router');

export const {
  selectCurrentRoute,   // select the current route
  selectQueryParams,    // select the current route query params
  selectQueryParam,     // factory function to select a query param
  selectRouteParams,    // select the current route params
  selectRouteParam,     // factory function to select a route param
  selectRouteData,      // select the current route data
  selectUrl,            // select the current url
} = fromRouter.getRouterSelectors(getRouterState);


@Injectable()
export class MapEffects {
  private _geojsonFormat: GeoJSON;
  private _wktFormat: WKT;
  private overrideSelectedItemLayer = false;

  private updateFeatureGeometry(feature: Feature<Geometry>, updateEvent: commonActions.DeviceUpdateEvent): Feature<Geometry> {
    const newFeature = feature.clone();
    const f = this._wktFormat.readFeature(updateEvent.attributes["geometry"], {
      dataProjection: 'EPSG:4326',
      featureProjection: 'EPSG:3857'
    });
    const centroid = getCenter(f.getGeometry().getExtent());
    newFeature.setId(feature.getId());
    newFeature.setGeometry(new Point(centroid));
    return newFeature;
  }

  init$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.INIT),
    withLatestFrom(this.store$.select(commonReducers.selectGetRootItems)),
    switchMap(([action, rootItems]) => {
      const actions = [];
      for (const rootItem of rootItems) {
        if (rootItem.itemType == "UPLOADS_FOLDER") actions.push(new mapActions.SetParent(rootItem.code));
      }
      // initialize default feature styles
      actions.push(new mapActions.SetStyle('file', new style.Style({
        image: new style.Icon({
          anchor: [0.5, 1],
          scale: 0.05,
          src: this.featureIconService$.getIconImageDataUrl("fal fa-file")
        }),
        stroke: new style.Stroke({
          color: 'red',
          width: 1
        }),
        fill: new style.Fill({
          color: 'rgba(0, 0, 0,0)'
        })
      })));
      actions.push(new mapActions.SetStyle('selected', new style.Style({
        image: new style.Icon({
          anchor: [0.5, 1],
          scale: 0.08,
          src: this.featureIconService$.getIconImageDataUrl(null)
        }),
        stroke: new style.Stroke({
          color: 'red',
          width: 3
        }),
        fill: new style.Fill({
          color: 'rgba(0, 0, 0, 0)'
        })
      })));

      return actions;
    }
    )));

  initBaseLayers$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.INIT),
    withLatestFrom(this.store$.select(mapReducers.selectGetProjection)),
    map(([action, projection]) => new mapActions.LoadBaseLayers(projection)))
  );

  loadBaseLayers$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.LOADBASELAYERS),
    switchMap((action: mapActions.LoadBaseLayers) => {
      return this.itemService$.getItemList("vnd.farmmaps.itemtype.layer", { "isBaseLayer": true }).pipe(
        map((items: IItem[]) => new mapActions.LoadBaseLayersSuccess(items)),
        catchError(error => of(new commonActions.Fail(error))));
    })));

  startSearch$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.STARTSEARCH),
    switchMap((action) => {
      const a = action as mapActions.StartSearch;
      const startDate = a.queryState.startDate;
      const endDate = a.queryState.endDate;
      let newAction: Observable<Action>;
      if (a.queryState.itemCode || a.queryState.parentCode || a.queryState.itemType || a.queryState.query || a.queryState.tags) {
        newAction = this.itemService$.getFeatures(a.queryState.bbox, "EPSG:3857", a.queryState.query, a.queryState.tags, startDate, endDate, a.queryState.itemType, a.queryState.parentCode, a.queryState.dataFilter, a.queryState.level).pipe(
          switchMap((features: any) => {
            for (const f of features.features) {
              if (f.properties && f.properties["code"]) {
                f.id = f.properties["code"];
              }
            }
            return of(new mapActions.StartSearchSuccess(this._geojsonFormat.readFeatures(features).filter(feature => feature instanceof Feature) as Feature[], a.queryState));
          }
          ),
          catchError(error => of(new commonActions.Fail(error))));
      } else {
        return [];
      }
      return newAction;
    })));


  zoomToExtent$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.STARTSEARCHSUCCESS),
    mergeMap((action: mapActions.StartSearchSuccess) => {
      const actions = [];
      actions.push(new commonActions.SetMenuVisible(false));
      const extent = createEmpty();
      if (!action.query.bboxFilter) {
        if (extent) {
          for (const f of action.features) {
            extend(extent, (f as Feature<Geometry>).getGeometry().getExtent());
          }
          if (action.features && action.features.length > 0) {
            actions.push(new mapActions.SetExtent(extent));
          }
        }
      }
      return actions;
    })));

  zoomToExtent2$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.SETFEATURES),
    switchMap((action: mapActions.SetFeatures) => {
      if (action.zoomToExtent) {
        const extent = createEmpty();
        if (extent) {
          for (const f of action.features) {
            extend(extent, (f as Feature<Geometry>).getGeometry().getExtent());
          }
          if (action.features.length > 0) return of(new mapActions.SetExtent(extent));
        }
      }
      return EMPTY;
    })));

  hideMenu$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.STARTSEARCHSUCCESS),
    mergeMap((action: mapActions.StartSearchSuccess) => {
      return of(new commonActions.SetMenuVisible(false));
    })));

  selectItem$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.SELECTITEM),
    withLatestFrom(this.store$.select(mapReducers.selectGetSelectedItem)),
    switchMap(([action, selectedItem]) => {
      const a = action as mapActions.SelectItem;
      const itemCode = selectedItem ? selectedItem.code : "";
      if (a.itemCode != itemCode) {
        return this.itemService$.getItem(a.itemCode).pipe(
          switchMap(child => {
            return this.itemService$.getItem(child.parentCode)
              .pipe(map(parent => {
                return { child, parent };
              }), catchError(() => { const parent: IItem = null; return of({ child, parent }) })
              );
          }),
          map(data => new mapActions.SelectItemSuccess(data.child, data.parent)),
          catchError(error => of(new commonActions.Fail(error))))
      } else {
        return [];
      }
    }
    )));

  selectItemSuccessSetLayer$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.SELECTITEMSUCCESS),
    map((action: mapActions.SelectItemSuccess) =>
      new mapActions.SetSelectedItemLayer(action.item)
    )
  ));

  selectItemSuccess$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.SELECTITEMSUCCESS),
    switchMap((action: mapActions.SelectItemSuccess) => {
      if (!this.overrideSelectedItemLayer) {
        return this.itemService$.getFeature(action.item.code, "EPSG:3857").pipe(
          map((feature: any) => {
            const f = this._geojsonFormat.readFeature(feature) as Feature;
            f.setId(action.item.code);
            return new mapActions.AddFeatureSuccess(f);
          }),
          catchError(error => of(new commonActions.Fail(error))));
      } else {
        return EMPTY;
      }
    }
    )));

  selectItemSuccessTemporal$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.SELECTITEMSUCCESS),
    switchMap((action: mapActions.SelectItemSuccess) => {
      if (action.item.itemType == "vnd.farmmaps.itemtype.temporal") {
        return this.itemService$.getChildItemList(action.item.code, null).pipe(
          map(items => new mapActions.SelectTemporalItemsSuccess(
            items.sort((a, b) =>
              -(Date.parse(b.dataDate) - Date.parse(a.dataDate))
            )
          )),
          catchError(error => of(new commonActions.Fail(error))));
      } else {
        return [];
      }
    }
    )));

  uploadedItemClick$ = createEffect(() => this.actions$.pipe(
    ofType(commonActions.UPLOADEDFILECLICK),
    switchMap((action: commonActions.UploadedFileClick) => of(new mapActions.DoQuery(tassign(mapReducers.initialState.query.querystate, { itemCode: action.itemCode })))
    )));

  featureUpdate$ = createEffect(() => this.actions$.pipe(
    ofType(commonActions.DEVICEUPDATEEVENT),
    withLatestFrom(this.store$.select(mapReducers.selectGetFeatures)),
    mergeMap(([action, features]) => {
      const deviceUpdateEventAction = action as commonActions.DeviceUpdateEvent;
      let feature: Feature<Geometry> = null;
      for (const f of features) {
        if (f.getId() == deviceUpdateEventAction.itemCode) {
          feature = f;
          break;
        }
      }
      if (feature) {
        return of(new mapActions.UpdateFeatureSuccess(this.updateFeatureGeometry(feature, deviceUpdateEventAction)));
      } else {
        return [];
      }
    })));

  itemUpdate$ = createEffect(() => this.actions$.pipe(
    ofType(commonActions.ITEMCHANGEDEVENT),
    withLatestFrom(this.store$.select(mapReducers.selectGetSelectedItem)),
    mergeMap(([action, selectedItem]) => {
      const itemChangedAction = action as commonActions.ItemChangedEvent;
      if (selectedItem && selectedItem.code == itemChangedAction.itemCode) {
        return this.itemService$.getItem(itemChangedAction.itemCode).pipe(
          switchMap(child => {
            return this.itemService$.getItem(child.parentCode)
              .pipe(map(parent => {
                return { child, parent };
              })
              );
          }),
          map(data => new mapActions.SelectItemSuccess(data.child, data.parent)),
          catchError(error => of(new commonActions.Fail(error))));
      } else {
        return [];
      }
    })));

  getActionFromQueryState(queryState: IQueryState, inSearch: boolean): Observable<Action> | [] {
    if (!inSearch && (queryState.itemType || queryState.parentCode || queryState.itemCode || queryState.query || queryState.tags)) {
      var newAction: Action;
      if (queryState.itemCode && queryState.itemCode != "") {
        newAction = new mapActions.SelectItem(queryState.itemCode);
      } else {
        newAction = new mapActions.StartSearch(queryState);
      }

    } else {
      newAction = new mapActions.Clear();
    }
    return of(newAction);
  }

  getLayerValue$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.GETLAYERVALUE),
    mergeMap((action: mapActions.GetLayerValue) => {
      const l = action.itemLayer.item.data["layers"][action.itemLayer.layerIndex];
      const scale = l.scale ? l.scale : 1;
      return this.itemService$.getLayerValue(action.itemLayer.item.code, action.itemLayer.layerIndex, action.x, action.y).pipe(
        mergeMap((v: number) => {
          const a = [];
          if (v !== null) {
            if (l.renderer && l.renderer.colorMap && l.renderer.colorMap.colormapType == "manual") {
              l.renderer.colorMap.entries.forEach((e) => {
                if (e.value == v && e.label) {
                  v = e.label;
                  return;
                }
              });
              a.push(new mapActions.GetLayerValueSuccess({ date: action.itemLayer.item.dataDate, value: v, layerName: l.name, quantity: "", unit: l.unit, scale: l.scale }));
            } else {
              a.push(new mapActions.GetLayerValueSuccess({ date: action.itemLayer.item.dataDate, value: v * scale, layerName: l.name, quantity: l.quantity, unit: l.unit, scale: l.scale }));
            }
          }
          return a;
        }))
    }
    )));

  updateLayerValuesOnLayerAddedOrRemoved$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.ADDLAYER, mapActions.REMOVELAYER, mapActions.SELECTITEMSUCCESS, mapActions.SELECTTEMPORALITEMSSUCCESS, mapActions.NEXTTEMPORAL, mapActions.PREVIOUSTEMPORAL, mapActions.TOGGLELAYERVALUESENABLED, mapActions.SETLAYERINDEX, mapActions.SETSELECTEDITEMLAYER),
    withLatestFrom(this.store$.select(mapReducers.selectGetLayerValuesX)),
    withLatestFrom(this.store$.select(mapReducers.selectGetLayerValuesY)),
    map(([[action, x], y]) => new mapActions.SetLayerValuesLocation(x, y))
  ));


  getLayerValues$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.SETLAYERVALUESLOCATION),
    withLatestFrom(this.store$.select(mapReducers.selectGetSelectedItemLayer)),
    withLatestFrom(this.store$.select(mapReducers.selectGetLayerValuesEnabled)),
    withLatestFrom(this.store$.select(mapReducers.selectGetOverlayLayers)),
    mergeMap(([[[action, selected], enabled], overlayLayers]) => {
      const layers = [];
      if (selected) {
        if (selected && (selected as TemporalItemLayer).selectedItemLayer) {
          selected = (selected as TemporalItemLayer).selectedItemLayer;
        }
        layers.push(selected);
      }
      overlayLayers.forEach((ol) => {
        if (ol != selected) layers.push(ol);
      });
      const a = action as mapActions.SetLayerValuesLocation;
      const actions = [];
      if (enabled) {
        layers.forEach((ol) => {
          if ("vnd.farmmaps.itemtype.shape.processed,vnd.farmmaps.itemtype.geotiff.processed".indexOf(ol.item.itemType) >= 0) {
            actions.push(new mapActions.GetLayerValue(ol, a.x, a.y));
          }
        });
      }
      return actions;
    })));


  setState$ = createEffect(() => this.actions$.pipe(
    ofType(mapActions.SETSTATE),
    withLatestFrom(this.store$.select(mapReducers.selectGetInSearch)),
    switchMap(([action, inSearch]) => {
      const a = action as mapActions.SetState;
      return this.getActionFromQueryState(a.queryState, inSearch);
    })));

  escape$ = createEffect(() => this.actions$.pipe(
    ofType(commonActions.ESCAPE),
    switchMap((action) => {
      const a = action as commonActions.Escape;
      if (a.escapeKey) {
        return of(new mapActions.Clear());
      } else {
        return EMPTY;
      }
    })));

  setOverride$ = createEffect(() => this.actions$.pipe(
    ofType(ROUTER_NAVIGATED),
    switchMap(() => this.store$.select(selectRouteData as any)),
    switchMap((data: any) => {
      if (data && data["fm-map-map"]) {
        const params = data["fm-map-map"];
        this.overrideSelectedItemLayer = params["overrideSelectedItemlayer"] ? params["overrideSelectedItemlayer"] : false;
      } else {
        this.overrideSelectedItemLayer = false;
      }
      return [];
    })
  ));

  constructor(private actions$: Actions, private store$: Store<mapReducers.State>, private folderService$: FolderService, private itemService$: ItemService, private featureIconService$: FeatureIconService, private itemTypeService$: ItemTypeService) {
    this._geojsonFormat = new GeoJSON();
    this._wktFormat = new WKT();
  }
}
