{-| A type representing a line. -}
module Line
    ( Line
    , pattern Line
    , start
    , end
    , mkLine
    , asTuple
    , lineLength
    , slope
    , areParallel
    , containsPoint
    , overlappingSegments ) where

import Control.Monad (liftM2)

import Text.Blaze.Svg11 ((!))
import qualified Text.Blaze.Svg11 as S
import qualified Text.Blaze.Svg11.Attributes as A

import ApproxEq
import Point ((|/|))
import qualified Point as P
import Helpers (fta)
import Shape


data Line  = Line { start :: P.Point
                  , end   :: P.Point
                  } deriving (Eq, Show)


instance SvgShape Line where
    toSvg l = let Line (P.Point x1 y1) (P.Point x2 y2) = l in
        S.line ! A.x1 (fta x1) ! A.y1 (fta y1) ! A.x2 (fta x2) ! A.y2 (fta y2)

instance ApproxEq Line where
    approxEqual a b epsilon = sameStart && sameEnd
        where sameStart = approxEqual (start a) (start b) epsilon
              sameEnd = approxEqual (end a) (end b) epsilon


instance Transformable Line where
    translate p l       = Line (start l + p) (end l + p)
    rotate p t l        = Line (P.rotateP (start l) p t) (P.rotateP (end l) p t)
    mirror p v l        = Line (P.mirrorP (start l) p v) (P.mirrorP (end l) p v)
    offset d leftSide l = Line (start l + d * r) (end l + d * r)
        where
            n = end l - start l
            angle = if leftSide then pi / 2 else (-pi) / 2
            q = P.rotateP n (P.Point 0 0) angle
            r = q |/| P.mag q


instance Mergable Line where

    -- | Two line segments can be merged if their points are collinear and if one line segment
    -- contains at least one of the points in the other. The merged segment is the minimum
    -- and maximum (sorted by lexicographical order) of the set of all four endpoints,
    merge a b tol
        | approxEqual a b tol = Just a
        | overlappingSegments a b =
            let ps = [start a, end a, start b, end b] in Just (Line (minimum ps) (maximum ps))
        | otherwise = Nothing


instance Ord Line where
    compare a b = asTuple a `compare` asTuple b
    (<=) a b = asTuple a <= asTuple b


-- | Short-form constructor for a line.
mkLine :: Float -> Float -> Float -> Float -> Line
mkLine a b c d = Line (P.Point a b) (P.Point c d)


-- | Represent a line as a 4-tuple.
asTuple :: Line -> (Float, Float, Float, Float)
asTuple (Line a b) = (P.xVal a, P.yVal a, P.xVal b, P.yVal b)


-- | Calculate the length of a line.
lineLength :: Line -> Float
lineLength = P.mag . liftM2 (-) start end


-- | Calculate the slope of a line.
slope :: Line -> Either String Float
slope l
    | x == 0 && y == 0 = Left "NaN"
    | x == 0 && y /= 0 = Left "Inf"
    | otherwise = Right (y / x)
    where P.Point x y = start l - end l


-- | Are two lines parallel?
areParallel :: Line -> Line -> Float -> Bool
areParallel a b tolerance = sharedSlope (slope a) (slope b)
    where
        sharedSlope (Right aSlope) (Right bSlope) = abs (aSlope - bSlope) < tolerance
        sharedSlope _ _ = False


-- | Does the given point lie along the given line ?
onLine :: P.Point -> Line -> Bool
onLine (P.Point px py) (Line (P.Point ax ay) (P.Point bx by))  =
    (px - ax) * dy == (py - ay) * dx
  where
    dx = bx - ax
    dy = by - ay


-- | Does the given point lie inside the given line segment?
containsPoint :: P.Point -> Line -> Bool
containsPoint p (Line s e) = isOnLine && p <= lineMax && p >= lineMin
  where
    isOnLine = onLine p (Line s e)
    lineMin  = minimum [s, e]
    lineMax  = maximum [s, e]


-- | Are two line (segments) overlapping? (i.e. are collinear and 
-- the enpoint of one is contained within length of the other)
overlappingSegments :: Line -> Line -> Bool
overlappingSegments a b =  sameSlope && (overlappingEnpoint || completelyContained)
  where
    sameSlope           = slope a == slope b
    overlappingEnpoint  = containsPoint (start b) a || containsPoint (end b) a
    completelyContained = containsPoint (start a) b