package be.iminds.ilabt.jfed.experimenter_gui.editor.properties.map;

import be.iminds.ilabt.jfed.experimenter_gui.config.JFedGuiConfig;
import be.iminds.ilabt.jfed.experimenter_gui.util.StringConverters;
import com.lynden.gmapsfx.ClusteredGoogleMapView;
import com.lynden.gmapsfx.MapComponentInitializedListener;
import com.lynden.gmapsfx.javascript.event.MapStateEventType;
import com.lynden.gmapsfx.javascript.event.UIEventType;
import com.lynden.gmapsfx.javascript.object.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
 * User: twalcari
 * Date: 1/8/2015
 * Time: 14:34
 */
public class NodesMapDialog implements MapComponentInitializedListener {
    private static final Logger LOG = LoggerFactory.getLogger(NodesMapDialog.class);
    @FXML
    private ClusteredGoogleMapView mapView;

    @FXML
    private ComboBox<JFedGuiConfig.ComponentInfo> nodesComboBox;

    @FXML
    private Button selectNodeButton;

    private boolean mapViewIsInitialized = false;

    private ClusteredGoogleMap map;

    private ObservableList<JFedGuiConfig.ComponentInfo> nodes = null;
    private final Map<JFedGuiConfig.ComponentInfo, LatLong> nodeLocations = new HashMap<>();

    private FilteredList<JFedGuiConfig.ComponentInfo> nodesWithinBounds;

    private JFedGuiConfig.ComponentInfo result = null;
//    private Marker selectedLocationMarker;


    @FXML
    private void initialize() {
        mapView.addMapInializedListener(this);

        nodesComboBox.setConverter(StringConverters.COMPONENT_INFO_CONVERTER);
        selectNodeButton.disableProperty().bind(nodesComboBox.valueProperty().isNull());

        nodesComboBox.setOnAction(event -> {
            JFedGuiConfig.ComponentInfo node = nodesComboBox.getValue();
            if (node != null) {
                assert (node.getLocation() != null);
                map.panTo(nodeLocations.get(node));
                map.setZoom(18);
            }
        });

    }

    @Override
    public void mapInitialized() {
        mapViewIsInitialized = true;

        tryInitializeMap();
    }

    private static final class ComparableLatLong {
        private final double lat, lon;

        public ComparableLatLong(double lat, double lon) {
            this.lon = lon;
            this.lat = lat;
        }

        public double getLat() {
            return lat;
        }

        public double getLon() {
            return lon;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            ComparableLatLong that = (ComparableLatLong) o;

            if (Double.compare(that.lat, lat) != 0) return false;
            if (Double.compare(that.lon, lon) != 0) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result;
            long temp;
            temp = Double.doubleToLongBits(lat);
            result = (int) (temp ^ (temp >>> 32));
            temp = Double.doubleToLongBits(lon);
            result = 31 * result + (int) (temp ^ (temp >>> 32));
            return result;
        }

        public LatLong toLatLong() {
            return new LatLong(lat, lon);
        }
    }

    private static double GRID_DIST = 0.00005;
    private static ComparableLatLong changeDuplicateToGrid(ComparableLatLong orig, int sameLocationCount) {
        for (int i = 0; i < 20; i++) {
            int square = i*i;
            int nextSquare = (i+1)*(i+1);
            if (sameLocationCount < nextSquare) {
                int x,y;
                if (sameLocationCount < square+i) {
                    x = i;
                    y = sameLocationCount - square;
                } else {
                    y = i;
                    x = sameLocationCount - (square+i);
                }

                LOG.debug("changeDuplicateToGrid(?, "+sameLocationCount+") -> x="+x+" y="+y+"   square="+square+" nextSquare="+nextSquare);
                return new ComparableLatLong(orig.lat + (x*GRID_DIST), orig.lon+(y*GRID_DIST));
            }
        }
        LOG.debug("changeDuplicateToGrid(?, "+sameLocationCount+") -> too much on same location");
        //too much on the same location, so we do something else
        return new ComparableLatLong(
                orig.lat - (sameLocationCount*GRID_DIST),
                orig.lon - (sameLocationCount*GRID_DIST)
        );
    }

    private void tryInitializeMap() {
        if (nodes == null) return;
        if (!mapViewIsInitialized) return;

        Map<ComparableLatLong, Integer> duplicateLocations = new HashMap<>();

        //first setup locations, but try to prevent duplicate locations by shifting them a bit if necessary
        nodes.stream().filter(JFedGuiConfig.ComponentInfo::hasLocation)
                .forEach(node -> {

                            ComparableLatLong ll =
                                    new ComparableLatLong(Double.parseDouble(node.getLocation().getLatitude()),
                                            Double.parseDouble(node.getLocation().getLongitude())
                                    );

                            int sameLocationCount = duplicateLocations.compute(
                                    ll,
                                    (k, v) -> (v == null) ? 0 : v+1
                            );
                            if (sameLocationCount > 0) {
                                ll = changeDuplicateToGrid(ll, sameLocationCount);
//                                assert !duplicateLocations.containsKey(ll);
                            }

                            nodeLocations.put(node, ll.toLatLong());
                        }


                );

        LOG.debug("Initialising nodesMap for "+nodes.size()+" nodes on "+nodeLocations.size()+" locations.");

        MapOptions mapOptions = new MapOptions();

        mapOptions.mapType(MapTypeIdEnum.ROADMAP)
                .center(new LatLong(41.711,26.828))
                .overviewMapControl(true)
                .panControl(true)
                .rotateControl(true)
                .scaleControl(true)
                .streetViewControl(true)
                .zoomControl(true)
                .zoom(3)
                .maxZoom(20); //was 18

        map = mapView.createMap(mapOptions);
        map.getMarkerClusterer().setMaxZoom(17);
        map.getMarkerClusterer().setGridSize(20);

        map.addStateEventHandler(MapStateEventType.bounds_changed, this::onMapBoundsChanged);
        map.addUIEventHandler(UIEventType.click, obj -> clearNodeSelection());

        LatLongBounds bounds = new LatLongBounds();

        for (Map.Entry<JFedGuiConfig.ComponentInfo, LatLong> entry : nodeLocations.entrySet()) {
            MarkerOptions nodeMarkerOptions = new MarkerOptions();
            nodeMarkerOptions.position(entry.getValue());
            nodeMarkerOptions.title(entry.getKey().getUrn().getEncodedResourceName());

            Marker nodeMarker = new Marker(nodeMarkerOptions);

            LOG.trace("Adding marker {} at location {}", entry.getKey().getUrn().getEncodedResourceName(), entry.getValue());
            map.addClusterableMarker(nodeMarker);

            map.addUIEventHandler(nodeMarker, UIEventType.click, obj -> selectNode(entry.getKey()));


            bounds.extend(entry.getValue());
        }

    }

    private void onMapBoundsChanged() {
        final LatLongBounds newBounds = map.getBounds();

        nodesWithinBounds.setPredicate(node -> newBounds.contains(nodeLocations.get(node)));
    }


    private void clearNodeSelection() {
        nodesComboBox.setValue(null);
    }

    private void selectNode(JFedGuiConfig.ComponentInfo node) {
        assert (node.getLocation() != null);
        map.panTo(nodeLocations.get(node));

        nodesComboBox.setValue(node);
    }

    public void setNodes(List<JFedGuiConfig.ComponentInfo> nodes) {
        if (this.nodes != null)
            throw new IllegalStateException("Already initialized");

        this.nodes = FXCollections.observableList(nodes);
        this.nodesWithinBounds = new FilteredList<>(this.nodes);

        nodesComboBox.setItems(nodesWithinBounds);

        tryInitializeMap();
    }

    @FXML
    private void selectNode() {
        result = nodesComboBox.getValue();
        ((Stage) selectNodeButton.getScene().getWindow()).close();
    }

    public JFedGuiConfig.ComponentInfo getResult() {
        return result;
    }
}
