{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TemplateHaskell #-}

module Flight.Types
    ( Fix(..)
    , LLA(..)
    , LatLngAlt(..)
    , FixMark(..)
    , Seconds(..)
    , Latitude(..)
    , Longitude(..)
    , Altitude(..)
    , MarkedFixes(..)
    , mkPosition
    ) where

import Data.Time.Clock (UTCTime)
import GHC.Generics (Generic)
import Data.Aeson (ToJSON(..), FromJSON(..))

import Data.Via.Scientific
    ( DefaultDecimalPlaces(..)
    , deriveDecimalPlaces, toSci, showSci, dpDegree
    )

-- | Latitude in degress.
newtype Latitude = Latitude Rational
    deriving (Eq, Generic)
    deriving anyclass (ToJSON, FromJSON)

-- | Longitude in degress.
newtype Longitude = Longitude Rational
    deriving (Eq, Generic)
    deriving anyclass (ToJSON, FromJSON)

-- | Altitude in metres.
newtype Altitude = Altitude Integer
    deriving (Eq, Ord, Generic)
    deriving newtype Num
    deriving anyclass (ToJSON, FromJSON)

-- | The number of seconds offset from the time of the first fix.
newtype Seconds = Seconds Integer
    deriving (Eq, Ord, Generic)
    deriving newtype Num
    deriving anyclass (ToJSON, FromJSON)

deriveDecimalPlaces dpDegree ''Latitude
deriveDecimalPlaces dpDegree ''Longitude
deriveDecimalPlaces dpDegree ''Altitude
deriveDecimalPlaces dpDegree ''Seconds

instance Show Latitude where
    show x@(Latitude lat') =
        showSci dp (toSci dp lat') ++ "°"
            where
                dp = defdp x

instance Show Longitude where
    show x@(Longitude lng') =
        showSci dp (toSci dp lng') ++ "°"
            where
                dp = defdp x

instance Show Altitude where
    show (Altitude alt) = show alt ++ "m"

instance Show Seconds where
    show (Seconds sec) = show sec ++ "s"

-- | Latitude, longitude and GPS altitude.  Use 'mkPosition' to construct a 'LLA'.
data LLA =
    LLA
        { llaLat :: Latitude
        , llaLng :: Longitude
        , llaAltGps :: Altitude
        }
    deriving (Eq, Show, Generic)
    deriving anyclass (ToJSON, FromJSON)

-- | Constructs a 'LLA' from its parts.
-- 
-- >>> mkPosition (Latitude (-33.65073300), Longitude 147.56036700, Altitude 214)
-- LLA {llaLat = -33.65073300°, llaLng = 147.56036700°, llaAltGps = 214m}
mkPosition :: (Latitude, Longitude, Altitude) -> LLA
mkPosition (lat', lng', alt') = LLA lat' lng' alt'

-- | Latitude, longitude and GPS altitude with a relative time offset in
-- seconds and possibly a barometric pressure altitude.
data Fix =
    Fix
        { fixMark :: Seconds
        -- ^ A mark in time, seconds offset from the first fix.
        , fix :: LLA
        -- ^ The coordinates of the fix, latitude, longitude and altitude.
        , fixAltBaro :: Maybe Altitude
        -- ^ The barometric pressure altitude of the fix.
        }
    deriving (Eq, Show, Generic)
    deriving anyclass (ToJSON, FromJSON)

-- | Class for a fix made up of latitude, longitude and GPS altitude.
class LatLngAlt a where
    lat :: a -> Latitude
    lng :: a -> Longitude
    altGps :: a -> Altitude

instance LatLngAlt LLA where
    lat LLA{llaLat} = llaLat
    lng LLA{llaLng} = llaLng
    altGps LLA{llaAltGps} = llaAltGps

instance LatLngAlt Fix where
    lat Fix{fix} = lat fix
    lng Fix{fix} = lng fix
    altGps Fix{fix} = altGps fix

-- | Class for a tracklog relative fix, offset in seconds, with an optional
-- barometric pressure altitude.
class LatLngAlt a => FixMark a where
    -- | Seconds offset from first fix.
    mark :: a -> Seconds
    -- | Barometric pressure altitude.
    altBaro :: a -> Maybe Altitude

instance FixMark Fix where
    mark Fix{fixMark} = fixMark
    altBaro Fix{fixAltBaro} = fixAltBaro

-- | A tracklog is a list of fixes along with the UTC time of the first fix.
data MarkedFixes =
    MarkedFixes
        { mark0 :: UTCTime -- ^ The UTC time of the first fix.
        , fixes :: [Fix] -- ^ The fixes of the track log.
        }
    deriving (Eq, Show, Generic, ToJSON, FromJSON)