-- | Some useful functions for operations with vectors.
--
module Math.Grads.Angem.Internal.VectorOperations
  ( areIntersected
  , avg
  , eqV2
  , reflectPoint
  ) where

import           Linear.Metric (distance, norm)
import           Linear.V2     (V2 (..))
import           Linear.Vector ((*^), (^+^), (^/))

-- | End of each line shouldn't be closer then this to other line.
--
eps :: Float
eps = 5

-- | Checks whether two lines intersect.
--
areIntersected :: (V2 Float, V2 Float) -> (V2 Float, V2 Float) -> Bool
areIntersected (x@(V2 x0 y0), y@(V2 x1 y1)) (x'@(V2 x0' y0'), y'@(V2 x1' y1')) = res
  where
    epsA = 20 -- Minimal distance between two lines

    a = x0 * y1 - y0 * x1
    b = x0' * y1' - x1' * y0'

    x01  = x0 - x1
    x01' = x0' - x1'
    y01  = y0 - y1
    y01' = y0' - y1'

    division = x01 * y01' - y01 * x01'

    px = (a * x01' - x01 * b) / division
    py = (a * y01' - y01 * b) / division

    notCommonPoint = not (x `eqV2` x' || x `eqV2` y' || y `eqV2` x' || y `eqV2` y')

    inXBounds = min x0 x1 - eps < px && px < max x0 x1 + eps && min x0' x1' - eps < px && px < max x0' x1' + eps
    inYBounds = min y0 y1 - eps < py && py < max y0 y1 + eps && min y0' y1' - eps < py && py < max y0' y1' + eps

    pointOnLine = pointBelongsToLine (x', y') x || pointBelongsToLine (x', y') y
                  || pointBelongsToLine (x, y) x' || pointBelongsToLine (x, y) y'
    notDistantEnough = not (norm (x - x') > epsA && norm (x - y') > epsA && norm (y - x') > epsA && norm (y - y') > epsA)

    res = notCommonPoint && (division /= 0 && inXBounds && inYBounds || pointOnLine || notDistantEnough)

-- | Reflects point over given line.
--
reflectPoint :: (V2 Float, V2 Float) -> V2 Float -> V2 Float
reflectPoint (coordA, coordB) thisPoint = res
  where
    V2 dirA dirB = coordB - coordA

    a' = V2 (-dirB) dirA
    a  = a' ^/ distance a' (V2 0.0 0.0)
    b' = V2 dirB (-dirA)
    b  = b' ^/ distance b' (V2 0.0 0.0)

    distanceFrom = distanceFromPointToLine (coordA, coordB)
    normVec = if distanceFrom (thisPoint + a) < distanceFrom (thisPoint + b) then a
              else b

    transform x = x + 2 * distanceFromPointToLine (coordA, coordB) x *^ normVec

    res = if pointBelongsToLine (coordA, coordB) thisPoint then thisPoint
          else transform thisPoint

distanceFromPointToLine :: (V2 Float, V2 Float) -> V2 Float -> Float
distanceFromPointToLine (V2 x1 y1, V2 x2 y2) (V2 x0 y0) = res
  where
    res = abs ((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / sqrt ((x1 - x2) ** 2 + (y1 - y2) ** 2)

pointBelongsToLine :: (V2 Float, V2 Float) -> V2 Float -> Bool
pointBelongsToLine (V2 x0 y0, V2 x1 y1) (V2 x' y') = (x0 * (x' - x1) + y0 * (y' - y1)) `eqFloat` 0.0 &&
 (min x0 x1 < x' && x' < max x0 x1 && min y0 y1 < y' && y' < max y0 y1)

-- | Given list of points calculates centroid of these points.
--
avg :: [V2 Float] -> V2 Float
avg points = foldl1 (^+^) points ^/ fromIntegral (length points)

-- | Checks two vectors of coordinates for equality.
--
eqV2 :: V2 Float -> V2 Float -> Bool
eqV2 (V2 a b) (V2 a' b') = a `eqFloat` a' && b `eqFloat` b'

-- TODO: We need to somehow consider length of line when comparing coordinates of two points
eqFloat :: Float -> Float -> Bool
eqFloat x y = abs (x - y) < eps