import { publish, tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { MapEventsManagerService } from '../../../../angular-cesium/services/map-events-mananger/map-events-manager';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { CesiumEvent } from '../../../../angular-cesium/services/map-events-mananger/consts/cesium-event.enum';
import { PickOptions } from '../../../../angular-cesium/services/map-events-mananger/consts/pickOptions.enum';
import { EditModes } from '../../../models/edit-mode.enum';
import { EditActions } from '../../../models/edit-actions.enum';
import { DisposableObservable } from '../../../../angular-cesium/services/map-events-mananger/disposable-observable';
import { CoordinateConverter } from '../../../../angular-cesium/services/coordinate-converter/coordinate-converter.service';
import { EditPoint } from '../../../models/edit-point';
import { CameraService } from '../../../../angular-cesium/services/camera/camera.service';
import { Cartesian3 } from '../../../../angular-cesium/models/cartesian3';
import { PointsManagerService } from './points-manager.service';
import { LabelProps } from '../../../models/label-props';
import { generateKey } from '../../utils';
import { CesiumService } from '../../../../angular-cesium';
import { PointEditOptions, PointProps } from '../../../models/point-edit-options';
import { PointEditUpdate } from '../../../models/point-edit-update';
import { PointEditorObservable } from '../../../models/point-editor-observable';
export const DEFAULT_POINT_OPTIONS: PointEditOptions = {
  addLastPointEvent: CesiumEvent.LEFT_CLICK,
  removePointEvent: CesiumEvent.RIGHT_CLICK,
  dragPointEvent: CesiumEvent.LEFT_CLICK_DRAG,
  allowDrag: true,
  pointProps: {
    color: Cesium.Color.WHITE.withAlpha(0.95),
    outlineColor: Cesium.Color.BLACK.withAlpha(0.5),
    outlineWidth: 1,
    pixelSize: 10,
    show: true,
    disableDepthTestDistance: Number.POSITIVE_INFINITY,
  },
};
/**
 * Service for creating editable point
 *
 *  * You must provide `PointsEditorService` yourself.
 * PolygonsEditorService works together with `<points-editor>` component. Therefor you need to create `<points-editor>`
 * for each `PointsEditorService`, And of course somewhere under `<ac-map>`/
 *
 * + `create` for starting a creation of the shape over the map. Returns a extension of `PointEditorObservable`.
 * + `edit` for editing shape over the map starting from a given positions. Returns an extension of `PointEditorObservable`.
 * + To stop editing call `dsipose()` from the `PointEditorObservable` you get back from `create()` \ `edit()`.
 *
 * **Labels over editted shapes**
 * Angular Cesium allows you to draw labels over a shape that is being edited with one of the editors.
 * To add label drawing logic to your editor use the function `setLabelsRenderFn()` that is defined on the
 * `PointEditorObservable` that is returned from calling `create()` \ `edit()` of one of the editor services.
 * `setLabelsRenderFn()` - receives a callback that is called every time the shape is redrawn
 * (except when the shape is being dragged). The callback is called with the last shape state and with an array of the current labels.
 * The callback should return type `LabelProps[]`.
 * You can also use `updateLabels()` to pass an array of labels of type `LabelProps[]` to be drawn.
 *
 * usage:
 * ```typescript
 *  // Start creating point
 *  const editing$ = pointEditorService.create();
 *  this.editing$.subscribe(editResult => {
 *				console.log(editResult.positions);
 *		});
 *
 *  // Or edit point from existing point cartesian3 positions
 *  const editing$ = this.pointEditor.edit(initialPos);
 *
 * ```
 */
@Injectable()
export class PointsEditorService {
  private mapEventsManager: MapEventsManagerService;
  private updateSubject = new Subject<PointEditUpdate>();
  private updatePublisher = publish<PointEditUpdate>()(this.updateSubject); // TODO maybe not needed
  private coordinateConverter: CoordinateConverter;
  private cameraService: CameraService;
  private pointManager: PointsManagerService;
  private observablesMap = new Map<string, DisposableObservable<any>[]>();
  private cesiumScene;
  init(mapEventsManager: MapEventsManagerService,
       coordinateConverter: CoordinateConverter,
       cameraService: CameraService,
       pointManager: PointsManagerService,
       cesiumViewer: CesiumService) {
    this.mapEventsManager = mapEventsManager;
    this.coordinateConverter = coordinateConverter;
    this.cameraService = cameraService;
    this.pointManager = pointManager;
    this.updatePublisher.connect();
    this.cesiumScene = cesiumViewer.getScene();
  }
  onUpdate(): Observable<PointEditUpdate> {
    return this.updatePublisher;
  }
  private screenToPosition(cartesian2) {
    const cartesian3 = this.coordinateConverter.screenToCartesian3(cartesian2);
    // If cartesian3 is undefined then the point inst on the globe
    if (cartesian3) {
      const ray = this.cameraService.getCamera().getPickRay(cartesian2);
      return this.cesiumScene.globe.pick(ray, this.cesiumScene);
    }
    return cartesian3;
  }
  create(options = DEFAULT_POINT_OPTIONS, eventPriority = 100): PointEditorObservable {
    const id = generateKey();
    const pointOptions = this.setOptions(options);
    const clientEditSubject = new BehaviorSubject<PointEditUpdate>({
      id,
      editAction: null,
      editMode: EditModes.CREATE
    });
    let finishedCreate = false;
    this.updateSubject.next({
      id,
      editMode: EditModes.CREATE,
      editAction: EditActions.INIT,
      pointOptions: pointOptions,
    });
    const finishCreation = (position: Cartesian3) => {
      return this.switchToEditMode(
        id,
        clientEditSubject,
        position,
        eventPriority,
        pointOptions,
        editorObservable,
        true
      );
    };
    const mouseMoveRegistration = this.mapEventsManager.register({
      event: CesiumEvent.MOUSE_MOVE,
      pick: PickOptions.NO_PICK,
      priority: eventPriority,
      pickConfig: options.pickConfiguration,
    });
    const addLastPointRegistration = this.mapEventsManager.register({
      event: pointOptions.addLastPointEvent,
      modifier: pointOptions.addLastPointModifier,
      pick: PickOptions.NO_PICK,
      priority: eventPriority,
      pickConfig: options.pickConfiguration,
    });
    this.observablesMap.set(id, [mouseMoveRegistration, addLastPointRegistration]);
    const editorObservable = this.createEditorObservable(clientEditSubject, id, finishCreation);
    mouseMoveRegistration.subscribe(({ movement: { endPosition } }) => {
      const position = this.screenToPosition(endPosition);
      if (position) {
        this.updateSubject.next({
          id,
          position,
          editMode: EditModes.CREATE,
          updatedPosition: position,
          editAction: EditActions.MOUSE_MOVE,
        });
      }
    });
    addLastPointRegistration.subscribe(({ movement: { endPosition } }) => {
      const position = this.screenToPosition(endPosition);
      finishedCreate = finishCreation(position);
    });
    return editorObservable;
  }
  private switchToEditMode(id,
                           clientEditSubject,
                           position: Cartesian3,
                           eventPriority,
                           pointOptions,
                           editorObservable,
                           finishedCreate: boolean) {
    const update = {
      id,
      position: position,
      editMode: EditModes.CREATE_OR_EDIT,
      updatedPosition: position,
      editAction: EditActions.ADD_LAST_POINT,
    };
    this.updateSubject.next(update);
    clientEditSubject.next({
      ...update,
      position: position,
      point: this.getPoint(id),
    });
    const changeMode = {
      id,
      editMode: EditModes.CREATE,
      editAction: EditActions.CHANGE_TO_EDIT,
    };
    this.updateSubject.next(changeMode);
    clientEditSubject.next(changeMode);
    if (this.observablesMap.has(id)) {
      this.observablesMap.get(id).forEach(registration => registration.dispose());
    }
    this.observablesMap.delete(id);
    this.editPoint(id, position, eventPriority, clientEditSubject, pointOptions, editorObservable);
    finishedCreate = true;
    return finishedCreate;
  }
  edit(position: Cartesian3, options = DEFAULT_POINT_OPTIONS, priority = 100): PointEditorObservable {
    const id = generateKey();
    const pointOptions = this.setOptions(options);
    const editSubject = new BehaviorSubject<PointEditUpdate>({
      id,
      editAction: null,
      editMode: EditModes.EDIT
    });
    const update = {
      id,
      position: position,
      editMode: EditModes.EDIT,
      editAction: EditActions.INIT,
      pointOptions: pointOptions,
    };
    this.updateSubject.next(update);
    editSubject.next({
      ...update,
      position: position,
      point: this.getPoint(id),
    });
    return this.editPoint(
      id,
      position,
      priority,
      editSubject,
      pointOptions
    );
  }
  private editPoint(id: string,
                       position: Cartesian3,
                       priority: number,
                       editSubject: Subject<PointEditUpdate>,
                       options: PointEditOptions,
                       editObservable?: PointEditorObservable) {
    const pointDragRegistration = this.mapEventsManager.register({
      event: options.dragPointEvent,
      entityType: EditPoint,
      pick: PickOptions.PICK_FIRST,
      pickConfig: options.pickConfiguration,
      priority,
      pickFilter: entity => id === entity.editedEntityId,
    });
    const pointRemoveRegistration = this.mapEventsManager.register({
      event: options.removePointEvent,
      modifier: options.removePointModifier,
      entityType: EditPoint,
      pick: PickOptions.PICK_FIRST,
      pickConfig: options.pickConfiguration,
      priority,
      pickFilter: entity => id === entity.editedEntityId,
    });
    pointDragRegistration.pipe(
      tap(({ movement: { drop } }) => this.cameraService.enableInputs(drop)))
      .subscribe(({ movement: { endPosition, drop }, entities }) => {
        const updatedPosition = this.screenToPosition(endPosition);
        if (!updatedPosition) {
          return;
        }
        const update = {
          id,
          editMode: EditModes.EDIT,
          updatedPosition,
          editAction: drop ? EditActions.DRAG_POINT_FINISH : EditActions.DRAG_POINT,
        };
        this.updateSubject.next(update);
        editSubject.next({
          ...update,
          position: updatedPosition,
          point: this.getPoint(id),
        });
      });
    const observables = [pointDragRegistration, pointRemoveRegistration];
    this.observablesMap.set(id, observables);
    return this.createEditorObservable(editSubject, id);
  }
  private setOptions(options: PointEditOptions) {
    const defaultClone = JSON.parse(JSON.stringify(DEFAULT_POINT_OPTIONS));
    const pointOptions: PointEditOptions = Object.assign(defaultClone, options);
    pointOptions.pointProps = {...DEFAULT_POINT_OPTIONS.pointProps, ...options.pointProps};
    pointOptions.pointProps = {...DEFAULT_POINT_OPTIONS.pointProps, ...options.pointProps};
    return pointOptions;
  }
  private createEditorObservable(observableToExtend: any, id: string, finishCreation?: (position: Cartesian3) => boolean)
                                                                                                    : PointEditorObservable {
    observableToExtend.dispose = () => {
      const observables = this.observablesMap.get(id);
      if (observables) {
        observables.forEach(obs => obs.dispose());
      }
      this.observablesMap.delete(id);
      this.updateSubject.next({
        id,
        editMode: EditModes.CREATE_OR_EDIT,
        editAction: EditActions.DISPOSE,
      });
    };
    observableToExtend.enable = () => {
      this.updateSubject.next({
        id,
        position: this.getPosition(id),
        editMode: EditModes.EDIT,
        editAction: EditActions.ENABLE,
      });
    };
    observableToExtend.disable = () => {
      this.updateSubject.next({
        id,
        position: this.getPosition(id),
        editMode: EditModes.EDIT,
        editAction: EditActions.DISABLE,
      });
    };
    observableToExtend.setManually = (point: {
      position: Cartesian3,
      pointProp?: PointProps
    } | Cartesian3, pointProps?: PointProps) => {
      const newPoint = this.pointManager.get(id);
      newPoint.setManually(point, pointProps);
      this.updateSubject.next({
        id,
        editMode: EditModes.CREATE_OR_EDIT,
        editAction: EditActions.SET_MANUALLY,
      });
    };
    observableToExtend.setLabelsRenderFn = (callback: any) => {
      this.updateSubject.next({
        id,
        editMode: EditModes.CREATE_OR_EDIT,
        editAction: EditActions.SET_EDIT_LABELS_RENDER_CALLBACK,
        labelsRenderFn: callback,
      });
    };
    observableToExtend.updateLabels = (labels: LabelProps[]) => {
      this.updateSubject.next({
        id,
        editMode: EditModes.CREATE_OR_EDIT,
        editAction: EditActions.UPDATE_EDIT_LABELS,
        updateLabels: labels,
      });
    };
    observableToExtend.finishCreation = () => {
      if (!finishCreation) {
        throw new Error('Points editor error edit(): cannot call finishCreation() on edit');
      }
      return finishCreation(null);
    };
    observableToExtend.getCurrentPoint = () => this.getPoint(id);
    observableToExtend.getEditValue = () => observableToExtend.getValue();
    observableToExtend.getLabels = (): LabelProps[] => this.pointManager.get(id).labels;
    return observableToExtend as PointEditorObservable;
  }
  private getPosition(id: string) {
    const point = this.pointManager.get(id);
    return point.getPosition();
  }
  private getPoint(id: string) {
    const point = this.pointManager.get(id);
    if (point) {
      return point.getCurrentPoint();
    }
  }
}