package com.mapbox.geojson.utils;

import androidx.annotation.NonNull;
import com.mapbox.geojson.Point;

import java.util.ArrayList;
import java.util.List;

/**
 * Polyline utils class contains method that can decode/encode a polyline, simplify a line, and
 * more.
 *
 * @since 1.0.0
 */
public final class PolylineUtils {

  private PolylineUtils() {
    // Prevent initialization of this class
  }

  // 1 by default (in the same metric as the point coordinates)
  private static final double SIMPLIFY_DEFAULT_TOLERANCE = 1;

  // False by default (excludes distance-based preprocessing step which leads to highest quality
  // simplification but runs slower)
  private static final boolean SIMPLIFY_DEFAULT_HIGHEST_QUALITY = false;

  /**
   * Decodes an encoded path string into a sequence of {@link Point}.
   *
   * @param encodedPath a String representing an encoded path string
   * @param precision   OSRMv4 uses 6, OSRMv5 and Google uses 5
   * @return list of {@link Point} making up the line
   * @see <a href="https://github.com/mapbox/polyline/blob/master/src/polyline.js">Part of algorithm came from this source</a>
   * @see <a href="https://github.com/googlemaps/android-maps-utils/blob/master/library/src/com/google/maps/android/PolyUtil.java">Part of algorithm came from this source.</a>
   * @since 1.0.0
   */
  @NonNull
  public static List<Point> decode(@NonNull final String encodedPath, int precision) {
    int len = encodedPath.length();

    // OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5
    double factor = Math.pow(10, precision);

    // For speed we preallocate to an upper bound on the final length, then
    // truncate the array before returning.
    final List<Point> path = new ArrayList<>();
    int index = 0;
    int lat = 0;
    int lng = 0;

    while (index < len) {
      int result = 1;
      int shift = 0;
      int temp;
      do {
        temp = encodedPath.charAt(index++) - 63 - 1;
        result += temp << shift;
        shift += 5;
      }
      while (temp >= 0x1f);
      lat += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);

      result = 1;
      shift = 0;
      do {
        temp = encodedPath.charAt(index++) - 63 - 1;
        result += temp << shift;
        shift += 5;
      }
      while (temp >= 0x1f);
      lng += (result & 1) != 0 ? ~(result >> 1) : (result >> 1);

      path.add(Point.fromLngLat(lng / factor, lat / factor));
    }

    return path;
  }

  /**
   * Encodes a sequence of Points into an encoded path string.
   *
   * @param path      list of {@link Point}s making up the line
   * @param precision OSRMv4 uses 6, OSRMv5 and Google uses 5
   * @return a String representing a path string
   * @since 1.0.0
   */
  @NonNull
  public static String encode(@NonNull final List<Point> path, int precision) {
    long lastLat = 0;
    long lastLng = 0;

    final StringBuilder result = new StringBuilder();

    // OSRM uses precision=6, the default Polyline spec divides by 1E5, capping at precision=5
    double factor = Math.pow(10, precision);

    for (final Point point : path) {
      long lat = Math.round(point.latitude() * factor);
      long lng = Math.round(point.longitude() * factor);

      long varLat = lat - lastLat;
      long varLng = lng - lastLng;

      encode(varLat, result);
      encode(varLng, result);

      lastLat = lat;
      lastLng = lng;
    }
    return result.toString();
  }

  private static void encode(long variable, StringBuilder result) {
    variable = variable < 0 ? ~(variable << 1) : variable << 1;
    while (variable >= 0x20) {
      result.append(Character.toChars((int) ((0x20 | (variable & 0x1f)) + 63)));
      variable >>= 5;
    }
    result.append(Character.toChars((int) (variable + 63)));
  }

  /*
   * Polyline simplification method. It's a direct port of simplify.js to Java.
   * See: https://github.com/mourner/simplify-js/blob/master/simplify.js
   */

  /**
   * Reduces the number of points in a polyline while retaining its shape, giving a performance
   * boost when processing it and also reducing visual noise.
   *
   * @param points an array of points
   * @return an array of simplified points
   * @see <a href="http://mourner.github.io/simplify-js/">JavaScript implementation</a>
   * @since 1.2.0
   */
  @NonNull
  public static List<Point> simplify(@NonNull List<Point> points) {
    return simplify(points, SIMPLIFY_DEFAULT_TOLERANCE, SIMPLIFY_DEFAULT_HIGHEST_QUALITY);
  }

  /**
   * Reduces the number of points in a polyline while retaining its shape, giving a performance
   * boost when processing it and also reducing visual noise.
   *
   * @param points    an array of points
   * @param tolerance affects the amount of simplification (in the same metric as the point
   *                  coordinates)
   * @return an array of simplified points
   * @see <a href="http://mourner.github.io/simplify-js/">JavaScript implementation</a>
   * @since 1.2.0
   */
  @NonNull
  public static List<Point> simplify(@NonNull List<Point> points, double tolerance) {
    return simplify(points, tolerance, SIMPLIFY_DEFAULT_HIGHEST_QUALITY);
  }

  /**
   * Reduces the number of points in a polyline while retaining its shape, giving a performance
   * boost when processing it and also reducing visual noise.
   *
   * @param points         an array of points
   * @param highestQuality excludes distance-based preprocessing step which leads to highest quality
   *                       simplification
   * @return an array of simplified points
   * @see <a href="http://mourner.github.io/simplify-js/">JavaScript implementation</a>
   * @since 1.2.0
   */
  @NonNull
  public static List<Point> simplify(@NonNull List<Point> points, boolean highestQuality) {
    return simplify(points, SIMPLIFY_DEFAULT_TOLERANCE, highestQuality);
  }

  /**
   * Reduces the number of points in a polyline while retaining its shape, giving a performance
   * boost when processing it and also reducing visual noise.
   *
   * @param points         an array of points
   * @param tolerance      affects the amount of simplification (in the same metric as the point
   *                       coordinates)
   * @param highestQuality excludes distance-based preprocessing step which leads to highest quality
   *                       simplification
   * @return an array of simplified points
   * @see <a href="http://mourner.github.io/simplify-js/">JavaScript implementation</a>
   * @since 1.2.0
   */
  @NonNull
  public static List<Point> simplify(@NonNull List<Point> points, double tolerance,
                                     boolean highestQuality) {
    if (points.size() <= 2) {
      return points;
    }

    double sqTolerance = tolerance * tolerance;

    points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
    points = simplifyDouglasPeucker(points, sqTolerance);

    return points;
  }

  /**
   * Square distance between 2 points.
   *
   * @param p1 first {@link Point}
   * @param p2 second Point
   * @return square of the distance between two input points
   */
  private static double getSqDist(Point p1, Point p2) {
    double dx = p1.longitude() - p2.longitude();
    double dy = p1.latitude() - p2.latitude();
    return dx * dx + dy * dy;
  }

  /**
   * Square distance from a point to a segment.
   *
   * @param point {@link Point} whose distance from segment needs to be determined
   * @param p1,p2 points defining the segment
   * @return square of the distance between first input point and segment defined by
   *   other two input points
   */
  private static double getSqSegDist(Point point, Point p1, Point p2) {
    double horizontal = p1.longitude();
    double vertical = p1.latitude();
    double diffHorizontal = p2.longitude() - horizontal;
    double diffVertical = p2.latitude() - vertical;

    if (diffHorizontal != 0 || diffVertical != 0) {
      double total = ((point.longitude() - horizontal) * diffHorizontal + (point.latitude()
        - vertical) * diffVertical) / (diffHorizontal * diffHorizontal + diffVertical
        * diffVertical);
      if (total > 1) {
        horizontal = p2.longitude();
        vertical = p2.latitude();

      } else if (total > 0) {
        horizontal += diffHorizontal * total;
        vertical += diffVertical * total;
      }
    }

    diffHorizontal = point.longitude() - horizontal;
    diffVertical = point.latitude() - vertical;

    return diffHorizontal * diffHorizontal + diffVertical * diffVertical;
  }

  /**
   * Basic distance-based simplification.
   *
   * @param points a list of points to be simplified
   * @param sqTolerance square of amount of simplification
   * @return a list of simplified points
   */
  private static List<Point> simplifyRadialDist(List<Point> points, double sqTolerance) {
    Point prevPoint = points.get(0);
    ArrayList<Point> newPoints = new ArrayList<>();
    newPoints.add(prevPoint);
    Point point = null;

    for (int i = 1, len = points.size(); i < len; i++) {
      point = points.get(i);

      if (getSqDist(point, prevPoint) > sqTolerance) {
        newPoints.add(point);
        prevPoint = point;
      }
    }

    if (!prevPoint.equals(point)) {
      newPoints.add(point);
    }
    return newPoints;
  }

  private static List<Point> simplifyDpStep(
    List<Point> points, int first, int last, double sqTolerance, List<Point> simplified) {
    double maxSqDist = sqTolerance;
    int index = 0;

    ArrayList<Point> stepList = new ArrayList<>();

    for (int i = first + 1; i < last; i++) {
      double sqDist = getSqSegDist(points.get(i), points.get(first), points.get(last));
      if (sqDist > maxSqDist) {
        index = i;
        maxSqDist = sqDist;
      }
    }

    if (maxSqDist > sqTolerance) {
      if (index - first > 1) {
        stepList.addAll(simplifyDpStep(points, first, index, sqTolerance, simplified));
      }

      stepList.add(points.get(index));

      if (last - index > 1) {
        stepList.addAll(simplifyDpStep(points, index, last, sqTolerance, simplified));
      }
    }

    return stepList;
  }

  /**
   * Simplification using Ramer-Douglas-Peucker algorithm.
   *
   * @param points a list of points to be simplified
   * @param sqTolerance square of amount of simplification
   * @return a list of simplified points
   */
  private static List<Point> simplifyDouglasPeucker(List<Point> points, double sqTolerance) {
    int last = points.size() - 1;
    ArrayList<Point> simplified = new ArrayList<>();
    simplified.add(points.get(0));
    simplified.addAll(simplifyDpStep(points, 0, last, sqTolerance, simplified));
    simplified.add(points.get(last));
    return simplified;
  }
}