package com.ctpodp;

import static com.ctpodp.PathUtilities.getMapFromPath;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;

import androidx.fragment.app.Fragment;

import android.provider.CalendarContract;
import android.provider.MediaStore;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Toast;

import com.ctpodp.objects.MapTapEvent;
import com.ctpodp.objects.NavigationPathGenerated;
import com.ctpodp.objects.PathAndWaypoints;
import com.ctpodp.objects.ProcessedInstruction;
import com.ctpodp.objects.fromReact.CenterMapOnPathObj;
import com.ctpodp.objects.fromReact.CenterMapOnPoint;
import com.ctpodp.objects.fromReact.CenterMapOnUserObj;
import com.ctpodp.objects.fromReact.DrawNavigationPathObj;
import com.ctpodp.objects.fromReact.Geojson.GeojsonPathObj;
import com.ctpodp.objects.fromReact.HighlightUnitObj;
import com.ctpodp.objects.fromReact.MapCredentials;
import com.ctpodp.objects.fromReact.MapImageObj;
import com.ctpodp.objects.fromReact.ObjectStyle;
import com.ctpodp.objects.fromReact.SetFollowBlueDotObj;
import com.ctpodp.objects.fromReact.SetMapVenueObj;
import com.ctpodp.objects.fromReact.SetMapViewBoundsObj;
import com.ctpodp.objects.fromReact.SetUserPositionObj;
import com.ctpodp.objects.fromReact.WayfindBetweenWaypointsObj;
import com.ctpodp.objects.fromReact.WayfindToWaypointObj;
import com.ctpodp.objects.fromReact.mapOptions.MapOptionsNavigationObj;
import com.ctpodp.objects.fromReact.mapOptions.MapOptionsObj;
import com.ctpodp.objects.fromReact.mapOptions.MapOptionsSnapObj;
import com.ctpodp.objects.fromReact.mapOptions.MapOptionsUserCircleObj;
import com.ctpodp.objects.fromReact.mapOptions.MapOptionsUserObj;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.jibestream.geofencekit.Geofence;
import com.jibestream.geofencekit.GeofenceCollection;
import com.jibestream.geofencekit.GeofenceInstance;
import com.jibestream.geofencekit.GeofenceKit;
import com.jibestream.jmapandroidsdk.astar.ASNode;
import com.jibestream.jmapandroidsdk.astar.Neighbor;
import com.jibestream.jmapandroidsdk.astar.PathPerFloor;
import com.jibestream.jmapandroidsdk.collections.WaypointCollection;
import com.jibestream.jmapandroidsdk.components.Associations;
import com.jibestream.jmapandroidsdk.components.Destination;
import com.jibestream.jmapandroidsdk.components.Waypoint;
import com.jibestream.jmapandroidsdk.jcontroller.JController;
import com.jibestream.jmapandroidsdk.jcore.JCore;
import com.jibestream.jmapandroidsdk.jcore.JCoreOptions;
import com.jibestream.jmapandroidsdk.jmap.JMap;
import com.jibestream.jmapandroidsdk.jmap.JMapOptions;
import com.jibestream.jmapandroidsdk.main.PointD;
import com.jibestream.jmapandroidsdk.main.Utilities;
import com.jibestream.jmapandroidsdk.oauth.JAuth;
import com.jibestream.jmapandroidsdk.rendering_engine.MapLayer;
import com.jibestream.jmapandroidsdk.rendering_engine.MapView;
import com.jibestream.jmapandroidsdk.rendering_engine.Stage;
import com.jibestream.jmapandroidsdk.rendering_engine.Transform;
import com.jibestream.jmapandroidsdk.rendering_engine.drawables.JBitmapDrawable;
import com.jibestream.jmapandroidsdk.rendering_engine.drawables.JIconDrawable;
import com.jibestream.jmapandroidsdk.rendering_engine.drawables.JShapeDrawable;
import com.jibestream.jmapandroidsdk.rendering_engine.moving_objects.MovingObject;
import com.jibestream.jmapandroidsdk.rendering_engine.moving_objects.UserLocation;
import com.jibestream.jmapandroidsdk.styles.JStyle;
import com.jibestream.navigationkit.NavigationKit;
import com.jibestream.navigationkit.instructionfactory.Instruction;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

public class MapFragment extends Fragment {

  ThemedReactContext _context;
  Stage stage;
  JController controller;
  NavigationKit navigationKit;
  GeofenceKit geofenceKit;
  boolean isNavigating = false;
  String currentPathId;
  DrawNavigationPathObj currDrawNavPathObj;
  Dictionary<String, PathPerFloor[]> paths = new Hashtable<>();
  private SetFollowBlueDotObj followBlueDot = new SetFollowBlueDotObj(false, false, 500);
  private long lastBlueDotFollow = 0;
  private MapOptionsObj mapOptions;
  MapCredentials mapCredentials;
  JBitmapDrawable pathStartIcon;
  JBitmapDrawable pathEndIcon;
  double walkingSpeedMps = 1.18; //walking speed to estimate duration, in m/s

  public MapFragment(ThemedReactContext context) {
    _context = context;
    // Required empty public constructor
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                           Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View view = inflater.inflate(R.layout.fragment_map, container, false);
    return view;
  }

  @Override
  public void onViewCreated(View view, Bundle savedInstanceState) {
    stage = view.findViewById(R.id.stage);
    Toast.makeText(_context, "View Created", Toast.LENGTH_SHORT).show();
  }

  public void sendEvent(String topic, Object data) {
    Log.d("EVENT", "Sending Event To " + topic + ", " + data.toString());
    _context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
      .emit(topic, data);
  }

  public void removeMap() {
    Log.d("STATE_CHANGE", "removeMap");
    try {
    stage.getMapView().pauseMap();
      Log.d("STATE_CHANGE", "pauseMap");
    } catch (InterruptedException e) {
      Log.d("STATE_CHANGE", "exception pausing map");
    }
    stage.removeAllViews();
    Log.d("STATE_CHANGE", "removeAllViews");
  }

  public void initializeMap(SetMapVenueObj obj) {
    MapCredentials credentials = obj.credentials;
    mapCredentials = credentials;

    String locale = credentials.locale;
    setMapOptions(obj.options);

    String CLIENT_ID = credentials.clientId;
    String CLIENT_SECRET = credentials.clientSecret;
    int CUSTOMER_ID = credentials.customerId;
    int VENUE_ID = credentials.venueId;
    String HOST = credentials.host;



    JAuth jAuth = new JAuth(CLIENT_ID, CLIENT_SECRET);

    JMapOptions mapOptions = new JMapOptions(jAuth, CUSTOMER_ID, VENUE_ID, HOST, stage);

    JCoreOptions jCoreOptions = new JCoreOptions();
    jCoreOptions.setLocaleCode(locale);

    mapOptions.setCoreOptions(jCoreOptions);

    mapOptions.shouldParseAllMaps(true);
    mapOptions.shouldApplyDisplayMode(true);

    final JMap map = new JMap(_context, mapOptions);
    JCore core = new JCore(jAuth, _context, CUSTOMER_ID, HOST, jCoreOptions);

    map.init(
      new JMap.OnMapInitCallback() {
        @Override
        public void onReady() {

          controller = map.getController();

          controller.setOnTapListener(new MapView.OnTapListener() {
            @Override
            public void onTap(MotionEvent motionEvent, Object drawable) {
              onMapTap(motionEvent, drawable);
            }
          });
          controller.setOnRotateListener(new MapView.OnRotateListener() {
            @Override
            public void onRotateChanged(float v) {
              onMapRotated(v);
            }
          });

          controller.enableLayerInteractivity(MapLayer.Layer_Units);

          navigationKit = new NavigationKit(controller);
          geofenceKit = new GeofenceKit(core, controller);


          geofenceKit.getGeofences(new GeofenceKit.OnGeofencesReadyCallback() {
            @Override
            public void onSuccess(GeofenceCollection geofenceCollection) {
              initializeGeofenceListener();
            }

            @Override
            public void onError(String s) {
              sendEvent("ERROR", "Error retrieving geofences: " + s);
            }
          });

          final PointF centerOfMap = new PointF(controller.getCurrentMap().getWidth()/2, controller.getCurrentMap().getHeight()/2); //
          UserLocation.getInstance().setPosition(centerOfMap);

          WritableArray searchDestinations = ReactUtilities.classToArray(controller.getActiveVenue().getDestinations().getAll());
          sendEvent("GENERATED_SEARCH_DESTINATIONS", searchDestinations);

          Waypoint userLocationWaypoint = controller.getActiveVenue().getClosestWaypointToCoordinatesOnMap(centerOfMap, controller.getCurrentMap());

          controller.addComponent(UserLocation.getInstance(), controller.getCurrentMap(), new PointF(userLocationWaypoint.getCoordinates()[0].floatValue(), userLocationWaypoint.getCoordinates()[1].floatValue()));

          WritableMap params = Arguments.createMap();
          params.putInt("venue_id", VENUE_ID);
          sendEvent("VENUE_LOADED", params);
        }

        @Override
        public void onError(String s) {
          sendEvent("ERROR", "Error initializing map: " + s);
        }
      }
    );
  }

  private void printXYFromLatLng(float lat, float lng) {
    PointF p1_latlng = new PointF(lat, lng);
    PointF p1_xy = convertLatLngToLocal(p1_latlng.x, p1_latlng.y);
    PointF p1_latlng_val = convertLocalToLatLng(p1_xy.x, p1_xy.y);
    Log.d("LAT_LNG_CONVERSION", "Point 1 XY:" + p1_xy.x + ", " + p1_xy.y);
    Log.d("LAT_LNG_1", "Point 1 Validation, lat: " + p1_latlng.x + ":" + p1_latlng_val.x + ", " + p1_latlng.y + ":" + p1_latlng_val.y);
  }

  private void printLatLngFromXY(float x, float y) {
    PointF p1_xy = new PointF(x, y);
    PointF p1_latlng = convertLocalToLatLng(p1_xy.x, p1_xy.y);
    PointF p1_xy_val = convertLatLngToLocal(p1_latlng.x, p1_latlng.y);
    Log.d("LAT_LNG_CONVERSION", "Point 1 LatLng:" + p1_latlng.x + ", " + p1_latlng.y);
    Log.d("LAT_LNG_1", "Point 1 Validation, x: " + p1_xy.x + ":" + p1_xy_val.x + ", y: " + p1_xy.y + ":" + p1_xy_val.y);
  }

  private void setMapOptions(MapOptionsObj obj) {
    mapOptions = obj;
    UserLocation userLocation = UserLocation.getInstance();
    if (obj.snap != null) {
      MapOptionsSnapObj snapObj = obj.snap;
      if (snapObj.grid != null) {
        userLocation.setSnapToGrid(snapObj.grid);
      }
      if (snapObj.waypoint != null) {
        userLocation.setSnapToWaypoint(snapObj.waypoint);
      }
      if (snapObj.path != null) {
        if (snapObj.path && snapObj.pathThreshold != null) {
          userLocation.snapToWayfindingPathWithThreshold(snapObj.pathThreshold);
        } else {
          // TODO: Stop snapping to path
        }
      }
    }
    if (obj.user != null) {
      MapOptionsUserObj userObj = obj.user;
      if (userObj.maxSpeed != null) {
        userLocation.setSpeed(userObj.maxSpeed);
      }
      if (userObj.iconUrl != null) {
          Thread thread = new Thread() {
            public void run() {
              try {
                InputStream inputStream = new java.net.URL(userObj.iconUrl).openStream();
                Bitmap imageBitmap = BitmapFactory.decodeStream(inputStream);
                imageBitmap = Bitmap.createScaledBitmap(imageBitmap, 25, 25, true);
                userLocation.setBitmap(imageBitmap);

              } catch (IOException e) {
                sendEvent("ERROR", "Unable to retrieve bitmap from image url: " + e);
              }
            }
          };
          thread.start();
      }
      if (userObj.walkingSpeed != null) {
        walkingSpeedMps = userObj.walkingSpeed;
      }
    }
    if (obj.userCircle != null) {
      MapOptionsUserCircleObj circleObj = obj.userCircle;
      if (circleObj.delayBetweenPulses != null) {
        userLocation.setDelayBetweenPulseCycles(Math.round(circleObj.delayBetweenPulses * 1000));
      }
      if (circleObj.pulseAnimationDuration != null) {
        userLocation.setPulseAnimationDuration(Math.round(circleObj.pulseAnimationDuration * 1000));
      }
      if (circleObj.confidenceColor != null) {
        userLocation.setConfidenceColor(circleObj.confidenceColor);
        userLocation.setPulseColor(circleObj.confidenceColor);
      }
      if (circleObj.confidenceOpacity != null) {
        userLocation.setConfidenceAlpha(circleObj.confidenceOpacity);
      }
      if (circleObj.confidenceRadius != null) {
        userLocation.setConfidenceRadius(Math.round(circleObj.confidenceRadius));
      }
    }
    if (obj.navigation != null) {
      MapOptionsNavigationObj navigationObj = obj.navigation;
    }
  }

  private float getDistanceBetweenPoints(PointF startPoint, PointF endPoint) {
    return (float) Math.sqrt(Math.pow(startPoint.x - endPoint.x, 2) + Math.pow(startPoint.y - endPoint.y, 2));
  }

  public void setUserPosition(SetUserPositionObj obj) {
    UserLocation prevUserLocation = UserLocation.getInstance();
    PointF newUserLocation = null;
    if (obj.format == null) {
      PointD convertPoint = new PointD(obj.y, obj.x);
      Utilities.convertEpsg4326ToLocal(convertPoint, controller.getActiveVenue());
      newUserLocation = new PointF((float) convertPoint.x, (float) convertPoint.y);
    } else if (obj.format.equals("xy")) {
      newUserLocation = new PointF((float) obj.x, (float) obj.y);
    }
    float heading = (float) obj.heading;
    float distance = getDistanceBetweenPoints(prevUserLocation.getPosition(), newUserLocation);
    controller.updateUserLocationPosition(newUserLocation, (int) heading, (int) obj.confidence, controller.getCurrentMap());

    if (followBlueDot.enable) {
      long currTime = System.currentTimeMillis();
      double rotate_heading = 0;

      int animationDuration = (int) (1000 * distance / prevUserLocation.getSpeed());
      if (followBlueDot.rotate) rotate_heading = heading;
      if (currTime - lastBlueDotFollow > followBlueDot.interval) {
        CenterMapOnPoint centerMapObj = new CenterMapOnPoint(new float[]{newUserLocation.x, newUserLocation.y}, 5f, (float) rotate_heading, animationDuration, "");
        centerMapOnPoint(centerMapObj);
      }
    }
    Log.d("AUTO_REROUTE", isNavigating + ", " + mapOptions.navigation.autoReroute);
    if (isNavigating) {
      if (mapOptions != null && mapOptions.navigation != null && mapOptions.navigation.autoReroute) {
        float threshold = mapOptions.navigation.autoRerouteThreshold == null ? 50 : mapOptions.navigation.autoRerouteThreshold;
        PathPerFloor[] path = paths.get(currentPathId);
        Log.d("AUTO_REROUTE", String.valueOf(navigationKit.hasUserVeeredOffRoute(path, threshold)));
        if (navigationKit.hasUserVeeredOffRoute(path, threshold)) {
          int lastPathIndex = path.length - 1;
          int lastPointIndex = path[lastPathIndex].points.length - 1;
          ASNode node = path[lastPathIndex].points[lastPointIndex];
          Waypoint destinationWaypoint = controller.getActiveVenue().getClosestWaypointToCoordinatesOnMap(new PointF((float) node.x, (float) node.y), controller.getCurrentMap());
          WayfindToWaypointObj wayfindObj = new WayfindToWaypointObj(destinationWaypoint.id);
          wayfindObj.doNotSendEvent = true;
          wayfindToWaypoint(wayfindObj);
          PathAndWaypoints pawObj = getPathToDestination(destinationWaypoint.id);
          if (pawObj.path != null && pawObj.path.length != 0) {
            String pathId = pawObj.origin.id + "_" + pawObj.destination.id;
            paths.put(pathId, pawObj.path);

            Instruction[] instructions = navigationKit.createInstructionsFromPaths(pawObj.path);

            ProcessedInstruction[] processedInstructions = PathUtilities.processInstructions(instructions, controller.getActiveVenue(), "FEET", controller.getCurrentMap().getMmPerPixel());

            WritableMap navPath = ReactUtilities.classToMap(new NavigationPathGenerated(pathId, processedInstructions, pawObj.origin, pawObj.destination, pawObj.path));
            navPath.putArray("jibestreamPath", getMapFromPath(controller, pawObj.path));

            sendEvent("REROUTE_NAVIGATION_PATH_GENERATED", navPath);

            DrawNavigationPathObj drawPathObj = currDrawNavPathObj;
            drawPathObj.pathId = pathId;
            drawPathObj.shouldFocus = false;
            clearWayfindingPath();
            drawWayfindingPath(drawPathObj);
          }
          else {
            sendEvent("REROUTE_NAVIGATION_PATH_GENERATED", Arguments.createMap());
            Log.w("AUTO_REROUTE", "Failed to auto reroute, no valid navigation paths were found");
          }
        }
      }
    }

    WritableArray data = Arguments.createArray();
    data.pushDouble(newUserLocation.x);
    data.pushDouble(newUserLocation.y);
    sendEvent("USER_POSITION_UPDATED", data);
  }

  public static WritableArray addValuesToPath(JController controller, PathPerFloor[] pathPerFloors) {
    WritableArray pathArr = ReactUtilities.classToArray(pathPerFloors);
    WritableArray pathArrW = Arguments.createArray();
    for (int i = 0; i < pathArr.size(); i++) {
      ReadableMap path = pathArr.getMap(i);

      WritableMap pathW = Arguments.createMap();
      pathW.putInt("mapId", path.getInt("mapId"));
      pathW.putMap("mover", path.getMap("mover"));
      WritableArray pointsW = Arguments.createArray();
      pathW.putDouble("cost", path.getDouble("cost"));
      pathW.putDouble("pixelDistance", path.getDouble("pixelDistance"));
      pathW.putDouble("mmDistance", path.getDouble("mmDistance"));

      for (int j = 0; j < path.getArray("points").size(); j++) {

        ReadableMap point = path.getArray("points").getMap(j);
        WritableMap pointW = Arguments.createMap();

        pointW.putInt("id", point.getInt("id"));
        pointW.putInt("mapId", point.getInt("mapId"));
        pointW.putDouble("x", point.getDouble("x"));
        pointW.putDouble("y", point.getDouble("y"));
        pointW.putDouble("z", point.getDouble("z"));

        int id = point.getInt("id");
        Waypoint waypoint = Arrays.stream(controller.getActiveVenue().getMaps().getAllWaypoints()).filter(wp -> wp.id == id).findFirst().get();
        Associations associations = waypoint.getAssociations();
        WritableMap assocaitionsMap = ReactUtilities.classToMap(associations);

        pointW.putMap("associations", assocaitionsMap);
        pointsW.pushMap(pointW);
      }

      pathW.putArray("points", pointsW);
      pathArrW.pushMap(pathW);
    }
    return pathArrW;
  }

  public PointF convertLocalToLatLng(float x, float y) {
    PointD convertPoint = new PointD(x, y);
    Utilities.convertLocalToEpsg4326(convertPoint, controller.getActiveVenue());
    return new PointF((float) convertPoint.y, (float) convertPoint.x);
  }

  public PointF convertLatLngToLocal(float lat, float lng) {
    PointD convertPoint = new PointD(lng, lat);
    Utilities.convertEpsg4326ToLocal(convertPoint, controller.getActiveVenue());
    return new PointF((float) convertPoint.x, (float) convertPoint.y);
  }

  public void centerMapOnPoint(CenterMapOnPoint centerMapObj) {
    PointF point = new PointF(centerMapObj.point[0], centerMapObj.point[1]);
    if (centerMapObj.format.equals("LAT_LONG")) {
      point = convertLatLngToLocal(point.x, point.y);
    }

    float zoom = centerMapObj.zoom != null ? centerMapObj.zoom : controller.getMapView().getCurrentScale();
    float orientation = centerMapObj.rotation != null ? -centerMapObj.rotation : controller.getMapView().getCurrentRotation();
    int animationDuration = centerMapObj.animationDuration;
    controller.setMapTransform(new Transform(zoom, point, orientation), animationDuration,
      new MapView.OnAnimationCallback() {@Override public void onAnimationComplete() {} @Override public void onAnimationInterrupted() {}});
  }

  public void centerMapOnUser(CenterMapOnUserObj obj) {
    int animationDuration = obj.animationDuration;
    float zoom = obj.zoom != null ? obj.zoom : controller.getMapView().getCurrentScale();
    double orientation = obj.rotation != null ? -obj.rotation : controller.getMapView().getCurrentRotation();
    controller.setMapTransform(new Transform(zoom, UserLocation.getInstance().getPosition(), (float) orientation), animationDuration,
      new MapView.OnAnimationCallback() {@Override public void onAnimationComplete() {} @Override public void onAnimationInterrupted() {}});
  }

  public void setMapViewBounds(SetMapViewBoundsObj obj) {
    RectF bounds = new RectF((float) obj.topLeft[0], (float) obj.topLeft[1], (float) obj.bottomRight[0], (float) obj.bottomRight[1]);
    if (obj.format.equals("LAT_LONG")) {
      PointF topLeft = convertLatLngToLocal((float) obj.topLeft[0], (float) obj.topLeft[1]);
      PointF bottomRight = convertLatLngToLocal((float) obj.bottomRight[0], (float) obj.bottomRight[1]);
      bounds = new RectF(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
    }
    zoomMapToBounds(bounds, (float) obj.rotation, obj.animationDuration);
  }

  public void centerMapOnPath(CenterMapOnPathObj obj) {
    RectF bounds = addPaddingToBounds(controller.getBoundsOfPathOnMap(controller.getCurrentMap()), (float) obj.margin);
    zoomMapToBounds(bounds, (float) obj.rotation, obj.animationDuration);
  }

  public void zoomMapToBounds(RectF bounds, float orientation, int animationDuration) {
    float maxDim = Math.max(bounds.width(), bounds.height());
    float zoom = Math.min(stage.getWidth(), stage.getHeight())/maxDim/1.75f;
    controller.setMapTransform(new Transform(zoom, new PointF(bounds.centerX(), bounds.centerY()), -(float) orientation), animationDuration,
      new MapView.OnAnimationCallback() {@Override public void onAnimationComplete() {} @Override public void onAnimationInterrupted() {}});
  }

  public RectF addPaddingToBounds(RectF bounds, float padding) {
    float cx = bounds.centerX();
    float cy = bounds.centerY();

    float scalingFactor = 1 + padding;

    float widthNew = bounds.width() * scalingFactor / 2;
    float heightNew = bounds.height() * scalingFactor / 2;

    return new RectF(
      cx - widthNew,
      cy + heightNew,
      cx + widthNew,
      cy - heightNew
      );
  }

  public void styleUnitForDestination(HighlightUnitObj obj) {
    JShapeDrawable[] units = controller.getUnitsFromDestination(controller.getActiveVenue().getDestinations().getById(obj.destinationId));
    if (obj.style != null) {
      controller.styleShapes(units, ObjectStyle.getJStyle(obj.style));
    } else {
      controller.resetShapeStyles(units);
    }
  }

  public void setFollowBlueDot(SetFollowBlueDotObj obj) {
    followBlueDot = obj;
    centerMapOnUser(new CenterMapOnUserObj((double) UserLocation.getInstance().getRotation(), 5, 1500));
  }

  public void clearWayfindingPath() {
    currDrawNavPathObj = null;
    isNavigating = false;
    currentPathId = null;
    controller.clearWayfindingPath();
    Log.d("REMOVING_ICONS", String.valueOf(pathStartIcon == null) + ", " + String.valueOf(pathEndIcon == null));
    if (pathStartIcon != null) {
      controller.removeComponent(pathStartIcon, controller.getCurrentMap());
    }
    if (pathEndIcon != null) {
      controller.removeComponent(pathEndIcon, controller.getCurrentMap());
    }
  }

  public void drawWayfindingPath(DrawNavigationPathObj obj) {
    Log.d("PATH_ICON_ORIGIN", obj.pathId);
    String pathId = obj.pathId;
    Boolean shouldFocus = obj.shouldFocus;
    JStyle style = ObjectStyle.getJStyle(obj.style);
    if (obj.style.color == null && obj.style.stroke != null) {
      try {
        style.setColor(Color.parseColor(obj.style.stroke));
      }
      catch (IllegalArgumentException ignored) {}
    }

    clearWayfindingPath();
    currDrawNavPathObj = obj;
    isNavigating = true;
    currentPathId = pathId;
    PathPerFloor[] path = paths.get(pathId);
    controller.drawWayfindingPath(path, style);
    if (obj.originIcon != null) {
        Thread thread = new Thread() {
          public void run() {
            pathStartIcon = createBitmap(obj.originIcon);
            Log.d("PATH_ICON_ORIGIN", path[0].points[0].x + ", " + path[0].points[0].y);
            controller.addComponent(pathStartIcon, controller.getCurrentMap(), new PointF((float) path[0].points[0].x, (float) path[0].points[0].y));
          }
        };
        thread.start();
    }
    if (obj.destinationIcon != null) {
      Thread thread = new Thread() {
        public void run() {
          pathEndIcon = createBitmap(obj.destinationIcon);
          int lastIndex = path[0].points.length - 1;
          Log.d("PATH_ICON_DESTINATION", path[0].points[lastIndex].x + ", " + path[0].points[lastIndex].y);
          controller.addComponent(pathEndIcon, controller.getCurrentMap(), new PointF((float) path[0].points[lastIndex].x, (float) path[0].points[lastIndex].y));
        }
      };
      thread.start();
    }
    if (shouldFocus) {
      Timer timer = new Timer();
      TimerTask task = new TimerTask() {
        @Override
        public void run() {
          centerMapOnPath(new CenterMapOnPathObj(0, 0.2, 1000));
        }
      };
      timer.schedule(task,500);
    }
  }

  JBitmapDrawable createBitmap(MapImageObj obj) {
    try {
      InputStream inputStream = new java.net.URL(obj.iconUrl).openStream();
      Bitmap imageBitmap = BitmapFactory.decodeStream(inputStream);
      imageBitmap = Bitmap.createScaledBitmap(imageBitmap, Math.round(obj.iconWidth), Math.round(obj.iconHeight), true);

      Bitmap bitmap = Bitmap.createBitmap(Math.round(obj.iconWidth*2), Math.round(obj.iconHeight*2), Bitmap.Config.ARGB_4444);
      Canvas canvas = new Canvas(bitmap);
      canvas.drawBitmap(imageBitmap, Math.round(obj.iconWidth/2) + obj.iconOffsetX, Math.round(obj.iconHeight/2) + obj.iconOffsetY, null);

      JBitmapDrawable startDrawable = new JBitmapDrawable(bitmap);
      startDrawable.setHeadsUp(true);
      return startDrawable;
    } catch (IOException e) {
      return null;
    }
  }

  public void wayfindToWaypoint(WayfindToWaypointObj obj) {
    int waypointId = obj.waypointId;
    PathAndWaypoints pathAndWaypoints = getPathToDestination(waypointId);
    if (pathAndWaypoints == null) {
      if (obj.doNotSendEvent == null || !obj.doNotSendEvent)
        sendEvent("NAVIGATION_PATH_GENERATED", Arguments.createMap());
      return;
    }
    Instruction[] instructions = navigationKit.createInstructionsFromPaths(pathAndWaypoints.path);
    ProcessedInstruction[] processedInstructions = PathUtilities.processInstructions(instructions, controller.getActiveVenue(), "FEET", controller.getCurrentMap().getMmPerPixel());

    String path_id = pathAndWaypoints.origin.id + "_" + pathAndWaypoints.destination.id;
    paths.put(path_id, pathAndWaypoints.path);

    WritableMap navPath = ReactUtilities.classToMap(new NavigationPathGenerated(path_id, processedInstructions, pathAndWaypoints.origin, pathAndWaypoints.destination, pathAndWaypoints.path));
    navPath.putArray("jibestreamPath", getMapFromPath(controller, pathAndWaypoints.path));

    if (obj.doNotSendEvent == null || !obj.doNotSendEvent) {
      sendEvent("NAVIGATION_PATH_GENERATED", navPath);
    }
  }

  public void wayfindBetweenWaypoints(WayfindBetweenWaypointsObj obj) {
    int originId = obj.originId;
    int destinationId = obj.destinationId;
    PathAndWaypoints pathAndWaypoints = getPathBetweenWaypoints(originId, destinationId);
    Instruction[] instructions = navigationKit.createInstructionsFromPaths(pathAndWaypoints.path);
    ProcessedInstruction[] processedInstructions = PathUtilities.processInstructions(instructions, controller.getActiveVenue(), "FEET", controller.getCurrentMap().getMmPerPixel());

    String path_id = pathAndWaypoints.origin.id + "_" + pathAndWaypoints.destination.id;
    paths.put(path_id, pathAndWaypoints.path);

    WritableMap navPath = ReactUtilities.classToMap(new NavigationPathGenerated(path_id, processedInstructions, pathAndWaypoints.origin, pathAndWaypoints.destination, pathAndWaypoints.path));
    navPath.putArray("jibestreamPath", getMapFromPath(controller, pathAndWaypoints.path));
    sendEvent("NAVIGATION_PATH_GENERATED", navPath);
  }

  private PathAndWaypoints getPathToDestination(int destinationId) {
    int numAttempts = 3;
    Waypoint[] sortedWaypoints = sortWaypointsByDistance(UserLocation.getInstance().getPosition(), controller.getCurrentMap().getWaypoints().getAll());

    for (int i = 0; i < Math.min(numAttempts, sortedWaypoints.length - 1); i++) {
      Waypoint userLocationWaypoint = controller.getActiveVenue().getClosestWaypointToCoordinatesOnMap(UserLocation.getInstance().getPosition(), controller.getCurrentMap());
      PathAndWaypoints path = getPathBetweenWaypoints(userLocationWaypoint.id, destinationId);
      if (path.path != null && path.path.length != 0) {
        return path;
      }
    }

    return null;
  }

  private Waypoint[] sortWaypointsByDistance(PointF center, Waypoint[] waypoints) {
    Arrays.sort(waypoints, new Comparator<Waypoint>() {
      @Override
      public int compare(Waypoint w1, Waypoint w2) {
        float distance1 = getDistanceBetweenPoints(center, new PointF(w1.getCoordinates()[0].floatValue(), w1.getCoordinates()[1].floatValue()));
        float distance2 = getDistanceBetweenPoints(center, new PointF(w2.getCoordinates()[0].floatValue(), w2.getCoordinates()[1].floatValue()));
        return Float.compare(distance1, distance2);
      }
    });
    return waypoints;
  }

  private PathAndWaypoints getPathBetweenWaypoints(int sourceId, int destinationId) {
    Waypoint sourceWaypoint = Arrays.stream(controller.getActiveVenue().getMaps().getAllWaypoints()).filter(wp -> wp.id == sourceId).findFirst().get();
    Waypoint destinationWaypoint = Arrays.stream(controller.getActiveVenue().getMaps().getAllWaypoints()).filter(wp -> wp.id == destinationId).findFirst().get();
    PathPerFloor[] pathPerFloors = controller.wayfindBetweenWaypoints(sourceWaypoint, destinationWaypoint);
    return new PathAndWaypoints(sourceWaypoint, destinationWaypoint, pathPerFloors);
  }

  private void initializeGeofenceListener() {
    geofenceKit.watchMovingObject(UserLocation.getInstance(), new GeofenceKit.OnMovingObjectListener() {
      @Override
      public void onEnteredGeofence(MovingObject movingObject, GeofenceInstance[] geofenceInstances) {
        WritableMap obj = Arguments.createMap();
        WritableArray arr = Arguments.createArray();
        for (GeofenceInstance geofenceInstance : geofenceInstances) {
          WritableMap geofence = Arguments.createMap();
          geofence.putString("name", geofenceInstance.getName());
          geofence.putInt("id", geofenceInstance.getId());
          WritableArray pointsArr = Arguments.createArray();
          for (double[] point : geofenceInstance.getCoordinates()) {
            WritableArray pointArr = Arguments.createArray();
            for (double p : point) {
              pointArr.pushDouble(p);
            }
            pointsArr.pushArray(pointArr);
          }
          geofence.putArray("coordinates", pointsArr);
          arr.pushMap(geofence);
        }
        obj.putBoolean("entered", true);
        obj.putArray("geofences", arr);
        sendEvent("GEOFENCES_CHANGED", obj);
      }

      @Override
      public void onExitedGeofence(MovingObject movingObject, GeofenceInstance[] geofenceInstances) {
        WritableMap obj = Arguments.createMap();
        WritableArray arr = Arguments.createArray();
        for (GeofenceInstance geofenceInstance : geofenceInstances) {
          WritableMap geofence = Arguments.createMap();
          geofence.putString("name", geofenceInstance.getName());
          geofence.putInt("id", geofenceInstance.getId());
          WritableArray pointsArr = Arguments.createArray();
          for (double[] point : geofenceInstance.getCoordinates()) {
            WritableArray pointArr = Arguments.createArray();
            for (double p : point) {
              pointArr.pushDouble(p);
            }
            pointsArr.pushArray(pointArr);
          }
          geofence.putArray("coordinates", pointsArr);
          arr.pushMap(geofence);
        }
        obj.putBoolean("entered", false);
        obj.putArray("geofences", arr);
        sendEvent("GEOFENCES_CHANGED", obj);
      }
    });
  }

  @Override
  public void onResume() {
    super.onResume();
    stage.getMapView().resumeMap();
  }

  @Override
  public void onPause() {
    super.onPause();
    try {
      stage.getMapView().pauseMap();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  public void onMapTap(MotionEvent motionEvent, Object drawable) {

    MapTapEvent tapEvent = new MapTapEvent(motionEvent.getX(), motionEvent.getY(), null);
    if (drawable == null) {
    } else if (drawable instanceof JShapeDrawable) {
      //cast shape drawable
      JShapeDrawable shapeDrawable = (JShapeDrawable) drawable;

      //extract waypoint meta data from unit shape
      Object[] destinationIds = shapeDrawable.getMetaData("data-unit");

      if (destinationIds != null && destinationIds.length > 0) {
        //get first destination from ids
        Destination destination = controller.getActiveVenue().getDestinations().getById((int) destinationIds[0]);

        tapEvent = new MapTapEvent(motionEvent.getX(), motionEvent.getY(), destination);
      }
    }
    sendEvent("MAP_TAPPED_GENERIC", ReactUtilities.classToMap(tapEvent));
  }

  public void onMapRotated(float angle) {
    WritableMap params = Arguments.createMap();
    params.putDouble("degrees", angle);
    sendEvent("SCREEN_ROTATED", params);
  }
}
