{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, FlexibleContexts #-}
{-| This is a partial parsing of the GPX 1.0 and 1.0 exchange types.
 -}
module Geo.GPX.Conduit
        ( Track(..), GPX(..), Segment(..), Point(..)
        , readGPXFile, pt
        ) where

import Control.Monad.Trans.Control
import Control.Monad
import Data.Conduit
import Data.Conduit.Text
import Data.Conduit.List as L
import Data.Void (Void)
import Data.Time.Format
import Data.Text (Text)
import qualified Data.Text as T
import System.Locale
import System.FilePath
import Data.String
import Data.Maybe (fromMaybe)
import Data.Time (UTCTime, buildTime, parseTime)
import Data.XML.Types
import Text.XML hiding (parseText)
import Text.XML.Stream.Parse
import qualified Data.Attoparsec.Text as AT

import Debug.Trace


-- |A GPX file usually is a single track (but can be many)
-- with one or more segments and many points in each segment.
data GPX = GPX  { -- waypoints  :: [Waypoint]
                -- , routes     :: [Route]
                tracks  :: [Track] }
        deriving (Eq, Ord, Show, Read)
data Track = Track 
        { trkName               :: Maybe Text
        , trkDescription        :: Maybe Text
        , segments              :: [Segment]
        }
        deriving (Eq, Ord, Show, Read)

-- |A GPX segments is just a bundle of points.
data Segment = Segment { points  :: [Point] }
        deriving (Eq, Ord, Show, Read)

type Latitude = Double
type Longitude = Double

-- |Track point is a full-fledged representation of all the data
-- available in most GPS loggers.  It is possible you don't want
-- all this data and can just made do with coordinates (via 'Pnt')
-- or a custom derivative.
data Point = Point
        { pntLat        :: Latitude
        , pntLon        :: Longitude
        , pntEle        :: Maybe Double -- ^ In meters
        , pntTime       :: Maybe UTCTime
        -- , pntSpeed   :: Maybe Double -- ^ Non-standard.  Usually in meters/second.
        }
        deriving (Eq, Ord, Show, Read)

pt :: Latitude -> Longitude -> Maybe Double -> Maybe UTCTime -> Point
pt t g e m = Point t g e m

zeroPoint = Point 0 0 Nothing Nothing

readGPXFile :: FilePath -> IO (Maybe GPX)
readGPXFile fp = runResourceT (parseFile def (fromString fp) $$ conduitGPX)

parseGPX :: (MonadThrow m, MonadBaseControl IO m) => Text -> m (Maybe GPX)
parseGPX t = runResourceT (yield t =$= mapOutput snd (parseText def) 
                                    $$ conduitGPX)

conduitGPX :: MonadThrow m => Sink Event m (Maybe GPX)
conduitGPX =
        tagPredicate ((== "gpx") . nameLocalName)
                        ignoreAttrs
                        (\_ -> do
                skipTagAndContents "metadata" 
                ts <- many conduitTrack
                return $ GPX ts)

skipTagAndContents :: (MonadThrow m) => Text -> Pipe Event Event Void () m ()
skipTagAndContents n = do
  tagPredicate ((== n) . nameLocalName) ignoreAttrs
               (const $ L.sinkNull)
  return ()


conduitTrack :: MonadThrow m => Sink Event m (Maybe Track)
conduitTrack = do
        tagPredicate ((== "trk") . nameLocalName) ignoreAttrs $ \_ -> do
        n <- join `fmap` tagPredicate (("name" ==) . nameLocalName) ignoreAttrs (const contentMaybe)
        d <- join `fmap` tagPredicate (("desc" ==) . nameLocalName) ignoreAttrs (const contentMaybe)
        segs <- many conduitSegment
        return (Track n d segs)

conduitSegment :: MonadThrow m => Sink Event m (Maybe Segment)
conduitSegment = do 
        tagPredicate ((== "trkseg") . nameLocalName) ignoreAttrs $ \_ -> do
        pnts <- (many conduitPoint)
        return (Segment pnts)

conduitPoint :: MonadThrow m => Sink Event m (Maybe Point)
conduitPoint =
        tagPredicate ((== "trkpt") . nameLocalName )
                                (do l <- parseDouble `fmap` requireAttr "lat"
                                    g <- parseDouble `fmap` requireAttr "lon"
                                    return $ zeroPoint { pntLon = g, pntLat = l })
                                parseETS

-- Parse elevation, time, and speed tags
parseETS :: MonadThrow m => Point -> Sink Event m Point
parseETS pnt = do
        let nameParse :: Name -> Maybe (Point -> Text -> Point)
            nameParse n =
                case nameLocalName n of
                        "ele"   -> Just (\p t -> p { pntEle = Just (parseDouble t) })
                        "time"  -> Just (\p t -> p { pntTime = (parseUTC t) })
                        _       -> Just const -- ignore everything else
            handleName :: (MonadThrow m) => pnt -> (pnt -> Text -> pnt) -> Sink Event m pnt
            handleName p op = fmap (op p) content
        skipTagAndContents "extensions"
        pnt' <- tag nameParse return (handleName pnt)
        case pnt' of
                Nothing -> return pnt
                Just p  -> parseETS p

parseDouble :: Text -> Double
parseDouble l = either (const 0) id (AT.parseOnly AT.double l)

parseUTC :: Text -> Maybe UTCTime
parseUTC = either (const Nothing) id . AT.parseOnly (do 
        yearMonthDay <- AT.manyTill AT.anyChar (AT.char 'T')
        hourMinSec <- AT.manyTill AT.anyChar (AT.choice [AT.char '.', AT.char 'Z'])
        fraction <- AT.choice [AT.manyTill AT.anyChar (AT.char 'Z'), return ""]
        -- The Time package version 1.4 does not handle F T and Q property for
        -- buildTime.
        -- return (buildTime defaultTimeLocale 
        --   [('F', yearMonthDay), ('T', hourMinSec), ('Q', fraction)]))
        return (parseTime defaultTimeLocale "%F %T %Q"
                        (unwords [yearMonthDay,hourMinSec,'.':fraction]))
        )