{-# Language MultiParamTypeClasses #-}
{-# Language FlexibleInstances #-}
--------------------------------------------------------------------------------
-- |
-- Module     : Geometry.Plane.General
-- Copyright  : (C) 2017 Maksymilian Owsianny
-- License    : BSD-style (see LICENSE)
-- Maintainer : Maksymilian.Owsianny@gmail.com
--
-- General representation of a plane. Plane in the General Form is Hession
-- Normal Form scaled by an arbitrary non-zero scalar.
--
--------------------------------------------------------------------------------
module Geometry.Plane.General
    ( Plane (..)
    , Plane2, Plane3
    , Plane2D, Plane3D

    , MakePlane (..)
    , unsafeMakePlane
    , flipPlane

    , collinear
 -- , coincidence, coorientation

    , PlanesRelation (..), Incidence (..), Orientation (..)
    , planesRelation
    , isParallel

    ) where

import Protolude hiding (zipWith, zero)
import Data.Maybe (fromJust)
import qualified Data.List as List
import Linear
-- import Linear.Solve
import Linear.Affine (Point, (.-.))
import qualified Linear.Affine as Point
import Data.EqZero

-- | Internally Plane is represented as a pair (sN, sO) where N is a normal
-- vector of a plane O is the distance of that plane from the origin and s is an
-- arbitrary non-zero scalar.
data Plane v n = Plane
   { planeVector :: !(v n)
   , planeLast   :: !n
   } deriving (Eq, Ord, Show)

type Plane2 = Plane V2
type Plane3 = Plane V3

type Plane2D = Plane V2 Double
type Plane3D = Plane V3 Double

instance (NFData (v n), NFData n) => NFData (Plane v n) where
    rnf (Plane vs l) = rnf vs `seq` rnf l

-- | Flip plane orientation.
flipPlane :: (Functor v, Num n) => Plane v n -> Plane v n
flipPlane (Plane v n) = Plane (fmap negate v) (negate n)

class MakePlane v n where
    -- | Make plane from vector of points. Returns Nothing if vectors between
    -- points are linearly dependent
    makePlane :: v (Point v n) -> Maybe (Plane v n)

instance (Num n, Eq n) => MakePlane V3 n where
    makePlane (V3 p1 p2 p3)
        | n == zero = Nothing
        | otherwise = Just $ Plane n d
        where
        n = cross (p2 .-. p1) (p3 .-. p1)
        d = negate $ dot n $ unPoint p1

-- | Assumes that points form a valid plane (i.e. vectors between all points are
-- linearly independent).
unsafeMakePlane :: MakePlane v n => v (Point v n) -> Plane v n
unsafeMakePlane = fromJust . makePlane

{-
makePlane :: (Applicative v, Solve v n, Num n)
    => v (Point v n) -> Maybe (Plane v n)
-- makePlane ps = Plane <$> solve ups (pure 1) <*> pure 1
makePlane ps = uncurry Plane <$> solve ups (pure 1)
    where
    ups = fmap unPoint ps

-- | Assumes that points form a valid plane (i.e. vectors between all points are
-- linearly independent).
unsafeMakePlane :: (Applicative v, Solve v n, Num n)
    => v (Point v n) -> Plane v n
-- unsafeMakePlane ps = Plane (fromJust $ solve ups (pure 1)) 1
-- unsafeMakePlane ps = Plane v d
unsafeMakePlane ps = case solve ups (pure 1) of
    Just (v, d) -> Plane v d
    Nothing     -> error "Bla" -- . toS $ List.unlines $ map show ps
    where
    -- Just (v, d) = solve ups (pure 1)
    ups = fmap unPoint ps
-}

-- | Convert point to a vector.
unPoint :: Point v n -> v n
unPoint (Point.P x) = x

--------------------------------------------------------------------------------

-- | Test whether two vectors are collinear.
collinear :: (Foldable v, Num n, EqZero n) => v n -> v n -> Bool
collinear v w = all f $ combinations 2 $ zipWith (,) v w
    where
    f [(a, b), (c, d)] = eqZero $ a*d - b*c
    f _                = False -- To silence exhaustiveness checker

-- | All n-combinations of a given list.
combinations :: Int -> [a] -> [[a]]
combinations k is
    | k <= 0    = [ [] ]
    | otherwise = [ x:r | x:xs <- tails is, r <- combinations (k-1) xs ]

-- | Zip two `Foldable` structures to a list with a given function.
zipWith :: Foldable f => (a -> b -> c) -> f a -> f b -> [c]
zipWith f a b = List.zipWith f (toList a) (toList b)

-- | Test co-incidence of two planes assuming collinearity.
coincidence :: (Foldable v, Num n, EqZero n) => Plane v n -> Plane v n -> Bool
coincidence (Plane v1 d1) (Plane v2 d2) = all f $ zipWith (,) v1 v2
    where
    f (x1, x2) = eqZero $ x1*d2 - x2*d1

-- | Test co-orientation of two assuming collinearity.
coorientation :: (Foldable v, Num n, Ord n, EqZero n)
    => Plane v n -> Plane v n -> Bool
coorientation (Plane v1 d1) (Plane v2 d2)
    = all geqZero $ d1*d2 : zipWith (*) v1 v2

--------------------------------------------------------------------------------

data PlanesRelation = Parallel Incidence Orientation | Crossing deriving Show
data Incidence      = CoIncident |  NonIncident                 deriving Show
data Orientation    = CoOriented | AntiOriented                 deriving Show

-- | Relate two planes on Parallelism, Incidence and Orientation.
planesRelation :: (Foldable v, Num n, Ord n, EqZero n)
    => Plane v n -> Plane v n -> PlanesRelation
planesRelation p1@(Plane v1 _) p2@(Plane v2 _)
    | collinear v1 v2 = Parallel incidence orientation
    | otherwise       = Crossing
    where
    incidence   = bool  NonIncident CoIncident $ coincidence   p1 p2
    orientation = bool AntiOriented CoOriented $ coorientation p1 p2

isParallel :: (Foldable v, Num n, Ord n, EqZero n)
    => Plane v n -> Plane v n -> Bool
isParallel a b = case planesRelation a b of
    Parallel _ _ -> True
    Crossing     -> False