
package de.ivu.elect.presentation.gebietsbaum.polygon;

import java.awt.geom.Line2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;

import org.apache.deltaspike.core.api.scope.ViewAccessScoped;
import org.primefaces.event.map.MarkerDragEvent;
import org.primefaces.event.map.OverlaySelectEvent;
import org.primefaces.event.map.PointSelectEvent;
import org.primefaces.model.map.LatLng;
import org.primefaces.model.map.MapModel;
import org.primefaces.model.map.Marker;
import org.primefaces.model.map.Overlay;
import org.primefaces.model.map.Polygon;

import de.ivu.elect.business.gebietsbaum.boundary.GebietExtendedDAO;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.GebietPolygon;
import de.ivu.elect.business.gebietsbaum.entity.GebietPolygonPoint;
import de.ivu.elect.business.gebietsbaum.entity.GebietPolygone;
import de.ivu.elect.business.security.UseCasePermission;
import de.ivu.elect.jee.faces.FacesMessageUtil;
import de.ivu.elect.jee.presentation.redirect.Redirect;
import de.ivu.elect.presentation.View;
import de.ivu.elect.presentation.hilfe.KeineOnlineHilfe;
import de.ivu.elect.presentation.hilfe.OnlineHilfeId;

@OnlineHilfeId(KeineOnlineHilfe.KEINE_ONLINE_HILFE)
@ViewAccessScoped
@Named(GebietGeometrieEditView.COMPONENT_NAME)
@View(viewId = GebietGeometrieEditView.VIEW_ID, useCase = UseCasePermission.GEBIET_GEOMETRIE_BEARBEITEN)
// Deaktiviert
public class GebietGeometrieEditView extends AbstractGebietGeometrieMapEditView {

  private static final long serialVersionUID = 1L;

  public static final String COMPONENT_NAME = "gebietGeometrieEditView";

  public static final String VIEW_ID = "/gebietsbaum/gebiet-geometrie-edit.xhtml";

  @Inject
  private GebietExtendedDAO gebietExtendedDAO;

  private Map<Gebiet, GebietPolygone> nachbarGebiete = new HashMap<>();

  private Map<Overlay, Gebiet> gebietNachOverlay = new HashMap<>();

  private Map<Marker, Polygon> polygonMarkerMap = new HashMap<>();

  public GebietGeometrieEditView() {
    super();
  }

  @Override
  @Redirect
  public String reset() {
    super.reset();
    polygonMarkerMap = new HashMap<>();
    nachbarGebiete = new HashMap<>();
    gebietNachOverlay = new HashMap<>();
    return null;
  }

  @Override
  public void onPointSelectInternal(PointSelectEvent event) {

    LatLng latlng = event.getLatLng();
    Marker neu = new Marker(latlng, "");
    neu.setDraggable(!isReadOnly());
    MapModel geometrieModel = getMapModel();
    geometrieModel.addOverlay(neu);

    if (geometrieModel.getMarkers().size() > 3) {
      // wieder raus und zwischen beide nähesten Marker hängen
      geometrieModel.getMarkers().remove(neu);
      List<Marker> bisherige = new ArrayList<>(geometrieModel.getMarkers());

      double kleinsterAbstand = 0;
      Marker von = null;
      Marker nach = null;
      for (int i = 0; i < bisherige.size() - 1; i++) {
        Marker bisher1 = bisherige.get(i);
        Marker bisher2 = bisherige.get(i + 1);

        if (von == null || berecheneAbstand(bisher1, bisher2, neu) < kleinsterAbstand) {
          kleinsterAbstand = berecheneAbstand(bisher1, bisher2, neu);
          von = bisher1;
          nach = bisher2;
        }
      }

      // TODO bestimmen, ob nach "von", oder nach "nach" eingefügt wreden muss...
      geometrieModel.getMarkers().add(geometrieModel.getMarkers().indexOf(von) + 1, neu);

    }

    Polygon polygon = findeNaechstesPolygon(neu);
    if (polygon == null) {
      polygon = addPolygonDesAktuellenGebietes();
    }
    polygonMarkerMap.put(neu, polygon);

    setChanged();

    refereshPolygon();

  }

  @Nullable
  private Polygon findeNaechstesPolygon(Marker neu) {

    Polygon result = null;
    double abstand = 0d;

    for (Polygon polygon : getPolygoneDesAtuellenGebietes()) {
      for (Map.Entry<Marker, Polygon> entry : polygonMarkerMap.entrySet()) {
        if (entry.getValue().equals(polygon)) {
          double abstandZuNeu = distanceInMeters(neu, entry.getKey());
          if (result == null || abstand > abstandZuNeu) {
            result = polygon;
            abstand = abstandZuNeu;
          }
        }
      }
    }

    return result;
  }

  protected double berecheneAbstand(Marker von, Marker nach, Marker neu) {
    return Math.abs(Line2D.ptLineDist(von.getLatlng().getLat(), von.getLatlng().getLng(), nach.getLatlng().getLat(),
        nach.getLatlng().getLng(), neu.getLatlng().getLat(), neu.getLatlng().getLng()));
  }

  @Override
  public void onMarkerDragInternal(MarkerDragEvent event) {
    Marker movedMarker = event.getMarker();

    LatLng nachbarPunktInNaehe = findeNachbarInNaehe(movedMarker);

    if (nachbarPunktInNaehe != null) {
      movedMarker.setLatlng(nachbarPunktInNaehe);
    } else if (markerZuNahAnAnderem(movedMarker)) {
      getMapModel().getMarkers().remove(movedMarker);
      polygonMarkerMap.remove(movedMarker);
    }
    setChanged();

    refereshPolygon();
  }

  @Nullable
  private LatLng findeNachbarInNaehe(Marker movedMarker) {
    for (GebietPolygone gebietPolygone : nachbarGebiete.values()) {
      for (GebietPolygon gebietPolygon : gebietPolygone.getPolygonList()) {
        for (GebietPolygonPoint gebietPolygonPoint : gebietPolygon.getGebietPolygonPointListReadOnly()) {
          if (distanceInMeters(gebietPolygonPoint.getLatitude(), gebietPolygonPoint.getLongitude(), movedMarker.getLatlng().getLat(),
              movedMarker.getLatlng().getLng()) < REMOVE_DISTANCE_IN_METER) {
            return new LatLng(gebietPolygonPoint.getLatitude(), gebietPolygonPoint.getLongitude());
          }
        }
      }
    }
    return null;
  }

  protected void refereshPolygon() {
    for (Polygon polygon : getPolygoneDesAtuellenGebietes()) {
      polygon.getPaths().clear();
    }

    for (Marker markerInModel : getMapModel().getMarkers()) {
      Polygon polygon = polygonMarkerMap.get(markerInModel);
      polygon.getPaths().add(markerInModel.getLatlng());
    }
  }

  private boolean markerZuNahAnAnderem(Marker movedMarker) {
    for (Marker marker1 : getMapModel().getMarkers()) {
      // Aus gleichem Polygon
      if (polygonMarkerMap.get(movedMarker).equals(polygonMarkerMap.get(marker1))) {
        if (!marker1.equals(movedMarker) && distanceInMeters(movedMarker.getLatlng(), marker1.getLatlng()) < REMOVE_DISTANCE_IN_METER) {
          return true;
        }
      }
    }
    return false;
  }

  private double distanceInMeters(Marker from, Marker to) {
    return distanceInMeters(from.getLatlng(), to.getLatlng());
  }

  private double distanceInMeters(LatLng from, LatLng to) {
    return distanceInMeters(from.getLat(), from.getLng(), to.getLat(), to.getLng());
  }

  private double distanceInMeters(double lat1, double lon1, double lat2, double lon2) {
    double theta = lon1 - lon2;
    double dist = Math.sin(deg2rad(lat1)) * Math.sin(deg2rad(lat2))
        + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(deg2rad(theta));
    dist = Math.acos(dist);
    dist = rad2deg(dist);
    dist = dist * 60.d * 1.1515d;
    dist = dist * 1.609344d * 1000.d;
    return dist;
  }

  private double deg2rad(double deg) {
    return deg * Math.PI / 180.d;
  }

  private double rad2deg(double rad) {
    return rad * 180.d / Math.PI;
  }

  @Override
  @Redirect
  public String save() {

    save(getGebiet());
    return VIEW_ID;
  }

  @Override
  protected void save(Gebiet gebiet) {
    if (isChanged()) {
      gebietGeometrieEdit.save(gebiet, convertPolygonPath(getPolygoneDesAtuellenGebietes()));
      FacesMessageUtil.addInfoMessage("gebiet.geoemetrie.edit.save.gebiet", "gebiet.geoemetrie.edit.save.gebiet", getGebietName(gebiet));
    }
    setChanged(false);
  }

  @Override
  @Redirect
  public String delete() {
    throw new UnsupportedOperationException("This is NOT implemented");
  }

  @Override
  protected String getViewId() {
    return VIEW_ID;
  }

  @Override
  protected MapModel createModel() {
    MapModel geometrieModel = super.createModel();

    List<GebietPolygonPoint> polygoneDesGebietesInsgesamt = new ArrayList<>();

    GebietPolygone gebietPolygone = null;

    nachbarGebiete = new HashMap<>();
    gebietNachOverlay = new HashMap<>();

    Collection<List<GebietPolygonPoint>> polygoneDerNachbargebieteInsgesamt = new ArrayList<>();

    if (getGebiet().hasUntergeordneteGebiete()) {
      nachbarGebiete.putAll(gebietGeometrieEdit.getUntergeordneteGebietPolygons(getGebiet()));
    } else {
      gebietPolygone = gebietGeometrieEdit.getGebietPolygone(getGebiet());
      for (GebietPolygon gebietPolygon : gebietPolygone.getPolygonList()) {
        polygoneDesGebietesInsgesamt.addAll(gebietPolygon.getGebietPolygonPointListReadOnly());
      }
      nachbarGebiete.putAll(gebietGeometrieEdit.getNachbarGebietPolygone(getGebiet()));
    }

    for (GebietPolygone polygone : nachbarGebiete.values()) {
      List<GebietPolygonPoint> nachbarGebietPolygone = new ArrayList<>();
      polygoneDerNachbargebieteInsgesamt.add(nachbarGebietPolygone);
      for (GebietPolygon gebietPolygon : polygone.getPolygonList()) {
        nachbarGebietPolygone.addAll(gebietPolygon.getGebietPolygonPointListReadOnly());
      }
    }

    if (isMindestensEinPolgonVorhanden(polygoneDesGebietesInsgesamt, polygoneDerNachbargebieteInsgesamt)) {

      setCenter(berechneMitte(polygoneDesGebietesInsgesamt, polygoneDerNachbargebieteInsgesamt));

      if (gebietPolygone != null) {
        for (GebietPolygon gebietPolygon : gebietPolygone.getPolygonList()) {
          Polygon polygon = addPolygonDesAktuellenGebietes();
          for (GebietPolygonPoint gebietPolygonPoint : gebietPolygon.getGebietPolygonPointListReadOnly()) {
            Marker marker = new Marker(new LatLng(gebietPolygonPoint.getLatitude(), gebietPolygonPoint.getLongitude()), "");
            marker.setDraggable(!isReadOnly());
            geometrieModel.addOverlay(marker);
            polygonMarkerMap.put(marker, polygon);
            polygon.getPaths().add(marker.getLatlng());
          }
        }
      }

      for (Map.Entry<Gebiet, GebietPolygone> gebietGebietPolygoneEntry : nachbarGebiete.entrySet()) {
        for (GebietPolygon gebietPolygon : gebietGebietPolygoneEntry.getValue().getPolygonList()) {
          if (gebietPolygon.getAnzahlPunkte() > 0) {
            Polygon nachbarPolygon = createNachbarPolygon(getGebietName(gebietGebietPolygoneEntry.getKey()));
            geometrieModel.addOverlay(nachbarPolygon);
            for (GebietPolygonPoint gebietPolygonPoint : gebietPolygon.getGebietPolygonPointListReadOnly()) {
              nachbarPolygon.getPaths().add(new LatLng(gebietPolygonPoint.getLatitude(), gebietPolygonPoint.getLongitude()));
            }
            gebietNachOverlay.put(nachbarPolygon, gebietGebietPolygoneEntry.getKey());
          }
        }
      }

    } else {
      setDefaultCenter();
    }

    return geometrieModel;
  }

  public void onPolygonSelect(OverlaySelectEvent event) {
    Gebiet gebiet = gebietNachOverlay.get(event.getOverlay());
    if (gebiet != null) {
      gebietExtendedDAO.attach(gebiet);

      if (getEinsehenGebietSecurity().isRechtVorhanden(gebiet)) {
        if (isChanged() && isBtnSaveRendered()) {
          save();
        }

        FacesMessageUtil.addInfoMessage("gebiet.geoemetrie.edit.selected", "gebiet.geoemetrie.edit.selected", getGebietName(gebiet));

        gebietSessionSelect.setEntity(gebiet);
      }
    }
  }

  @Override
  public boolean isBtnZuweisenRendered() {
    return hasPermission(UseCasePermission.GEBIET_GEOMETRIE_BEARBEITEN);
  }

  @Override
  public boolean isBtnEditRendered() {
    return false;
  }

  @Override
  public boolean isBtnSVGRendered() {
    return hasPermission(UseCasePermission.GEBIET_GEOMETRIE_ANZEIGEN);
  }

  @Override
  @Redirect
  public String gotoSvg() {
    if (isChanged() && isBtnSaveRendered()) {
      save();
    }
    return super.gotoSvg();
  }

  @Override
  @Redirect
  public String gotoZuweisen() {
    if (isChanged() && isBtnSaveRendered()) {
      save();
    }
    return super.gotoZuweisen();
  }
}
