defmodule Distance do
  @moduledoc ~S"""
  Basic distance calculations for cartesian coordinates for calculting
  distances on a single plane.  If you are looking to calculating distance
  on the surface of Earth, check out the `Distance.GreatCircle` module.

  ## Examples
      iex> Distance.distance({2.5, 2.5}, {4, 0.8})
      2.2671568097509267
      iex> Distance.segment_distance({2.5, 2.5}, {4, 0.8}, {-2, 3})
      1.0797077632696
      iex> Distance.distance([{2.5, 2.5}, {4, 0.8}, {-2, 3}, {1, -1}])
      13.657774933219109
  """

  @type point() :: {number(), number()}

  @doc """
  Returns the geometric distance between two points.  Accepts 2- or
  3-dimensional points.

  ## Examples
      iex> Distance.distance({1, -2}, {-2, 2})
      5.0
      iex> Distance.distance({1, -2, 2}, {-2, 2, 1})
      5.0990195135927845
  """
  @spec distance(point, point) :: float()
  def distance(p1, p2), do: :math.sqrt(distance_squared(p1, p2))

  @doc """
  Returns the square of the distance between two points.  This is used by the
  `Distance.distance` function above, but having access to the value before
  the expensice sqaure root operation is useful for time-sensitive applications
  that only need values for comparison.

  ## Examples
      iex> Distance.distance_squared({1, -2}, {-2, 2})
      25
      iex> Distance.distance_squared({1, -2, 2}, {-2, 2, 1})
      26
  """
  @spec distance_squared(point, point) :: float()
  def distance_squared({x1, y1}, {x2, y2}) do
    dx = x1 - x2
    dy = y1 - y2

    dx * dx + dy * dy
  end

  def distance_squared({x1, y1, z1}, {x2, y2, z2}) do
    dx = x1 - x2
    dy = y1 - y2
    dz = z1 - z2

    dx * dx + dy * dy + dz * dz
  end

  @doc """
  Returns the geometric distance from a point `p` and the infinite line
  extending through points `p1` and `p2`. 

  ## Examples
      iex> Distance.line_distance({3, 2}, {-2, 1}, {5, 3})
      0.4120816918460673 # distance between the point {3, 2} and the closest point along line segment ({-2, 1}, {5, 3})
      iex> Distance.line_distance({1, -2}, {-2, 2}, {-10, 102})
      2.671464946476815
      iex> Distance.line_distance({1, -2}, {-2, 2}, {1, -2})
      0.0
  """
  @spec line_distance(point, point, point) :: float()
  def line_distance(p, p1, p2), do: :math.sqrt(line_distance_squared(p, p1, p2))

  @doc """
  Similar to `Distance.distance_squared`, this provides much faster comparable
  version of `Distance.line_distance`.

  ## Examples
      iex> Distance.line_distance_squared({3, 2}, {-2, 1}, {5, 3})
      0.16981132075471717
      iex> Distance.line_distance_squared({1, -2}, {-2, 2}, {-10, 102})
      7.136724960254371
  """
  @spec line_distance_squared(point, point, point) :: float()
  def line_distance_squared(_, {x1, y1}, {x2, y2}) when x1 == x2 and y1 == y2, do: 0.0

  def line_distance_squared({x, y}, {x1, y1}, {x2, y2}) do
    dx = x2 - x1
    dy = y2 - y1
    t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy)
    distance_squared({x, y}, {x1 + dx * t, y1 + dy * t})
  end

  @doc """
  Returns the geometric distance from a point `p` and the line segment
  between two points `p1` and `p2`.  Note that this is a line segment, not
  an infinite line, so points not between `p1` and `p2` will return the
  distance to the nearest of the two endpoints.

  ## Examples
      iex> Distance.segment_distance({3, 2}, {-2, 1}, {5, 3})
      0.4120816918460673 # distance between the point {3, 2} and the closest point along line segment ({-2, 1}, {5, 3})
      iex> Distance.segment_distance({1, -2}, {-2, 2}, {-10, 102})
      5.0
      iex> Distance.segment_distance({1, -2}, {-2, 2}, {1, -2})
      0.0
  """
  @spec segment_distance(point, point, point) :: float()
  def segment_distance(p, p1, p2), do: :math.sqrt(segment_distance_squared(p, p1, p2))

  @doc """
  Similar to `Distance.distance_squared`, this provides much faster comparable
  version of `Distance.segment_distance`.

  ## Examples
      iex> Distance.segment_distance_squared({3, 2}, {-2, 1}, {5, 3})
      0.16981132075471717
      iex> Distance.segment_distance_squared({1, -2}, {-2, 2}, {-10, 102})
      25
  """
  @spec segment_distance_squared(point, point, point) :: float()
  def segment_distance_squared({x, y}, {x1, y1}, {x2, y2}) when x1 == x2 and y1 == y2,
    do: distance_squared({x, y}, {x1, y1})

  def segment_distance_squared({x, y}, {x1, y1}, {x2, y2}) do
    dx = x2 - x1
    dy = y2 - y1
    t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy)

    cond do
      t > 1 -> distance_squared({x, y}, {x2, y2})
      t > 0 -> distance_squared({x, y}, {x1 + dx * t, y1 + dy * t})
      true -> distance_squared({x, y}, {x1, y1})
    end
  end

  @doc """
  Provides the minimum distance between any two points along the given line
  segments.  In the case where the segements are not disjoint, this will
  always return `0.0`.

  ## Example
      iex> Distance.segment_segment_distance({0, 0}, {1, 1}, {1, 0}, {2, 0})
      0.7071067811865476
      iex> Distance.segment_segment_distance({0, 0}, {1, 1}, {1, 1}, {2, 2})
      0.0
  """
  @spec segment_segment_distance(point, point, point, point) :: float()
  def segment_segment_distance(a1, a2, b1, b2),
    do: :math.sqrt(segment_segment_distance_squared(a1, a2, b1, b2))

  @doc """
  Similar to `Distance.distance_squared`, this provides much faster comparable
  version of `Distance.segment_segment_distance`.
  """
  @spec segment_segment_distance_squared(point, point, point, point) :: float()
  def segment_segment_distance_squared(a1, a2, b1, b2) do
    case SegSeg.intersection(a1, a2, b1, b2) do
      {true, _, _} ->
        0.0

      {false, _, _} ->
        [
          segment_distance_squared(a1, b1, b2),
          segment_distance_squared(a2, b1, b2),
          segment_distance_squared(b1, a1, a2),
          segment_distance_squared(b2, a1, a2)
        ]
        |> Enum.min()
    end
  end

  @doc """
  Returns the geometric distance of the linestring defined by the List of
  points.  Accepts 2- or 3-dimensional points.

  ## Examples
      iex> Distance.distance([{2.5, 2.5}, {4, 0.8}, {2.5, 3.1}, {2.5, 3.1}])
      5.013062853300123
      iex> Distance.distance([{1, -2, 1}, {-2, 2, -1}, {-2, 1, 0}, {2, -3, 1}])
      12.543941016045627
  """
  @spec distance(list(point)) :: float()
  def distance([]), do: 0.0
  def distance([_]), do: 0.0
  def distance([p1, p2]), do: distance(p1, p2)

  def distance([p1, p2 | tail]) do
    distance(p1, p2) + distance([p2 | tail])
  end

  @doc """
  Returns a point `distance` units away in the direction `direction`.

  The direction is measured as radians off of the positive x-axis in the direction of
  the positive y-axis.  Thus the new coordinates are:

  ```elixir
  x1 = x0 + distance * cos(direction)
  y1 = y0 + distance * sin(direction)
  ```

  ## Examples
      iex> Distance.project({3, 5}, 3 * :math.pi() / 4, 2)
      {1.585786437626905, 6.414213562373095} 
  """
  @spec project(point(), number(), number()) :: point()
  def project({x0, y0}, direction, distance) do
    {
      x0 + distance * :math.cos(direction),
      y0 + distance * :math.sin(direction)
    }
  end

  @doc """
  Returns the direction from p0 to p1.  The direction is
  measured as radians off of the positive x-axis in the direction of the
  positive y-axis.

  The returned value will always be in the range of (-π, π]. The direction 
  along the negative x-axis will always return positive π.

  ## Examples
      iex> Distance.angle_to({2, -1}, {2, 5}) 
      :math.pi() / 2
  """
  @spec angle_to(point(), point()) :: float()
  def angle_to({x0, y0}, {x1, y1}) do
    :math.atan2(y1 - y0, x1 - x0)
  end

  @doc """
  Returns the cotemrinal angle closest to 0 for the given angle

  No matter the angle provided, the returned angle will be in the range (-π, π]

  ## Examples
      iex> Distance.min_coterminal_angle(:math.pi() / 2.0)
      :math.pi() / 2
      iex> Distance.min_coterminal_angle(-2.0 + :math.pi() * 6)
      -2.0
      iex> Distance.min_coterminal_angle(-:math.pi())
      :math.pi()
  """
  @spec min_coterminal_angle(number()) :: float()
  def min_coterminal_angle(angle) do
    :math.pi() - min_positive_coterminal_angle(:math.pi() - angle)
  end

  @doc """
  Returns the minimal positive coterminal angle for the given angle.

  No matter the angle provided, the returned angle will be in the range [0, 2π)


  ## Examples
      iex> Distance.min_positive_coterminal_angle(:math.pi() / 2.0)
      :math.pi() / 2
      iex> Distance.min_positive_coterminal_angle(-2.0 + :math.pi() * 6)
      :math.pi() * 2.0 - 2.0
      iex> Distance.min_positive_coterminal_angle(-:math.pi())
      :math.pi()
  """
  @spec min_positive_coterminal_angle(number()) :: float()
  def min_positive_coterminal_angle(angle) do
    :math.fmod(angle, :math.pi() * 2)
    |> case do
      a when a < 0.0 -> a + :math.pi() * 2
      a -> a
    end
  end

  @doc """
  Returns the angular difference between two directions in the range 
  """
  @spec angular_difference(number(), number()) :: float()
  def angular_difference(a1, a2) do
    min_coterminal_angle(a2 - a1)
  end
end
