package cgeo.geocaching.maps.routing;

import cgeo.geocaching.CgeoApplication;
import cgeo.geocaching.location.Geopoint;
import cgeo.geocaching.settings.Settings;
import cgeo.geocaching.utils.Log;

import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Xml;

import java.util.LinkedList;

import org.apache.commons.lang3.ArrayUtils;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public final class Routing {
    private static final double UPDATE_MIN_DISTANCE_KILOMETERS = 0.005;
    private static final double MAX_ROUTING_DISTANCE_KILOMETERS = 10.0;
    private static final double MIN_ROUTING_DISTANCE_KILOMETERS = 0.04;
    private static final int UPDATE_MIN_DELAY_SECONDS = 3;
    private static BRouterServiceConnection brouter;
    private static Geopoint lastDirectionUpdatePoint;
    @NonNull private static Geopoint[] lastRoutingPoints = {};
    private static Geopoint lastDestination;
    private static long timeLastUpdate;

    private Routing() {
        // utility class
    }

    public static void connect() {
        if (brouter != null && brouter.isConnected()) {
            //already connected
            return;
        }

        brouter = new BRouterServiceConnection();
        final Intent intent = new Intent();
        intent.setClassName("btools.routingapp", "btools.routingapp.BRouterService");

        if (!getContext().bindService(intent, brouter, Context.BIND_AUTO_CREATE)) {
            brouter = null;
        }
    }

    private static ContextWrapper getContext() {
        return CgeoApplication.getInstance();
    }

    public static void disconnect() {
        if (brouter != null && brouter.isConnected()) {
            getContext().unbindService(brouter);
            brouter = null;
        }
    }

    @NonNull
    public static Geopoint[] getTrack(final Geopoint start, final Geopoint destination) {
        if (brouter == null || Settings.getRoutingMode() == RoutingMode.STRAIGHT) {
            return defaultTrack(start, destination);
        }

        // avoid updating to frequently
        final long timeNow = System.currentTimeMillis();
        if ((timeNow - timeLastUpdate) < 1000 * UPDATE_MIN_DELAY_SECONDS) {
            return lastRoutingPoints;
        }

        // Disable routing for huge distances
        final float targetDistance = start.distanceTo(destination);
        if (targetDistance > MAX_ROUTING_DISTANCE_KILOMETERS) {
            return defaultTrack(start, destination);
        }

        // disable routing when near the target
        if (targetDistance < MIN_ROUTING_DISTANCE_KILOMETERS) {
            return defaultTrack(start, destination);
        }

        // Use cached route if current position has not changed more than 5m
        // TODO: Maybe adjust this to current zoomlevel
        if (lastDirectionUpdatePoint != null && destination == lastDestination && start.distanceTo(lastDirectionUpdatePoint) < UPDATE_MIN_DISTANCE_KILOMETERS) {
            return lastRoutingPoints;
        }

        // now really calculate a new route
        lastDestination = destination;
        lastRoutingPoints = ArrayUtils.nullToEmpty(calculateRouting(start, destination), Geopoint[].class);
        lastDirectionUpdatePoint = start;
        timeLastUpdate = timeNow;
        return lastRoutingPoints;
    }

    private static Geopoint[] defaultTrack(final Geopoint start, final Geopoint destination) {
        return new Geopoint[] { start, destination };
    }

    @Nullable
    private static Geopoint[] calculateRouting(final Geopoint start, final Geopoint dest) {
        final Bundle params = new Bundle();
        params.putString("trackFormat", "gpx");
        params.putDoubleArray("lats", new double[]{start.getLatitude(), dest.getLatitude()});
        params.putDoubleArray("lons", new double[]{start.getLongitude(), dest.getLongitude()});
        params.putString("v", Settings.getRoutingMode().parameterValue);

        final String gpx = brouter.getTrackFromParams(params);

        return parseGpxTrack(gpx, dest);
    }

    @Nullable
    private static Geopoint[] parseGpxTrack(final String gpx, final Geopoint destination) {
        try {
            final LinkedList<Geopoint> result = new LinkedList<>();
            Xml.parse(gpx, new DefaultHandler() {
                @Override
                public void startElement(final String uri, final String localName, final String qName, final Attributes atts) throws SAXException {
                    if (qName.equalsIgnoreCase("trkpt")) {
                        final String lat = atts.getValue("lat");
                        if (lat != null) {
                            final String lon = atts.getValue("lon");
                            if (lon != null) {
                                result.add(new Geopoint(lat, lon));
                            }
                        }
                    }
                }
            });

            // artificial straight line from track to target
            result.add(destination);

            return result.toArray(new Geopoint[result.size()]);

        } catch (final SAXException e) {
            Log.e("cannot parse brouter output", e);
        }
        return null;
    }

    public static void invalidateRouting() {
        lastDirectionUpdatePoint = null;
        timeLastUpdate = 0;
    }

    public static boolean isAvailable() {
        return brouter != null;
    }
}

@@ -1031,22 +1031,23 @@
 ;%0A    @N
-onN
 ull
+able
  private
@@ -1089,10 +1089,12 @@
 s = 
-%7B%7D
+null
 ;%0A  
@@ -1983,24 +1983,450 @@
    %7D%0A    %7D%0A%0A
+    /**%0A     * Return a valid track (with at least two points, including the start and destination).%0A     * In some cases (e.g., destination is too close or too far, path could not be found),%0A     * a straight line will be returned.%0A     *%0A     * @param start the starting point%0A     * @param destination the destination point%0A     * @return a track with at least two points including the start and destination points%0A     */%0A
     @NonNull
@@ -2848,32 +2848,44 @@
          return 
+ensureTrack(
 lastRoutingPoint
@@ -2877,32 +2877,53 @@
 astRoutingPoints
+, start, destination)
 ;%0A        %7D%0A%0A   
@@ -3413,16 +3413,35 @@
  than 5m
+ and we had a route
 %0A       
@@ -3501,16 +3501,45 @@
     if (
+lastRoutingPoints != null && 
 lastDire
@@ -3599,16 +3599,32 @@
 ation &&
+%0A               
  start.d
@@ -4018,32 +4018,44 @@
 %0A        return 
+ensureTrack(
 lastRoutingPoint
@@ -4055,24 +4055,303 @@
 ngPoints
-;%0A    %7D%0A
+, start, destination);%0A    %7D%0A%0A    @NonNull%0A    private static Geopoint%5B%5D ensureTrack(@Nullable final Geopoint%5B%5D routingPoints, final Geopoint start, final Geopoint destination) %7B%0A        return routingPoints != null ? routingPoints : defaultTrack(start, destination);%0A    %7D%0A%0A    @NonNull
 %0A    pri

fix #6241: always include start and destination in path