module Main where

import System.Directory
import System.Environment
import System.Exit
import System.Console.GetOpt
import System.Time (TimeDiff(..), formatTimeDiff, noTimeDiff, normalizeTimeDiff)
import System.Locale (defaultTimeLocale)

import Control.Monad (unless, liftM, forM_)
import Data.Char (isDigit, isLetter)
import Data.List (sort)
import Data.Maybe
import Text.PrettyPrint.HughesPJ
import Numeric

import BusTracker

data OptArgs = OptArgs
    { maxresults    :: Maybe Limit
    , delay         :: Maybe Int
    , disabledOnly  :: Bool
    , realtimeOnly  :: Bool
    } deriving Show

data Limit = First Int | Last Int deriving Show

toLimit n | n < 0     = Last (negate n)
          | otherwise = First n

defaultQuery = Q "" Nothing Nothing
defaultOptArgs = OptArgs
    { maxresults   = Nothing
    , delay        = Nothing
    , disabledOnly = False
    , realtimeOnly = False
    }

ppResult (R n d h t e) = render $
            busnumber <> handicap <+> destination <+> estimate <> time
    where handicap = text $ if h then " (D)" else "    "
          estimate = text $ if e then "*" else " "
          busnumber = text $ pad n 5
          destination = text $ pad d 20
          time = text $ formatTimeDiff defaultTimeLocale "%Hh %Mm" t
          pad str len = take len $ str ++ repeat ' '

main = do
    (info,options,query) <- parseArgs =<< parseConfigFile
    unless (null info)
        (mapM_ putStrLn info >> exitWith ExitSuccess)
    if isStopId (queryBusStop query)
        then getBusTimes query >>= printResults options
        else putStrLn "Please enter a valid bus stop ID."

printResults opts rs = mapM_ putStrLn results
    where results = maybe ["No results found."] ppTable rs
          ppTable = map ppResult . trim (maxresults opts) . sort . filter (keep opts)

version = "BusTracker, v0.1"

usage = unlines $
    [ "Invoke as: buses [QUERY] [OPTIONS]"
    , usageInfo queryHeader queryOptions
    , usageInfo optsHeader argOptions
    ]
    where queryHeader = "QUERY: bus stop, number and time."
          optsHeader = "OPTIONS: limiting and filtering results."

queryOptions =
    [ Option "s" ["stop"] (ReqArg stopNum "ID") "Stop ID"
    , Option "b" ["bus"]  (ReqArg busNum "NUM") "Bus number"
    , Option "t" ["time"] (ReqArg time "HHmm") "Arrival time"
    , Option "n" ["now"]  (NoArg (time "now"))
        "Use current time as desired arrival time"
    ] where busNum s q | isBusId s = q { queryBusNumber = Just s }
                       | otherwise = q { queryBusNumber = Nothing }
            stopNum s q | isStopId s = q { queryBusStop = s }
                        | otherwise  = q
            time "now" q = q { queryBusTime = Nothing }
            time hhmm q | isTime hhmm = q { queryBusTime = Just hhmm }
                        | otherwise   = q 

argOptions =
    [ Option "l" ["limit"] (ReqArg limit "N")
        "Display max N results. Use negative N for last N results."
    , Option "w" ["wait"] (ReqArg delay "N")
        "Omit results arriving in less than N minutes."
    , Option "d" ["disabled"] (NoArg (\opt -> opt {disabledOnly = True }))
        "Show only buses with disabled access"
    , Option "r" ["real"] (NoArg (\opt -> opt {realtimeOnly = True}))
        "Show only realtime results (no estimates)"
    ]
  where limit l os = let ml = fmap (toLimit . fst) $ listToMaybe $ readSigned readDec l
                     in os { maxresults = ml }
        delay d os = let md = fmap (fst) $ listToMaybe $ readDec d
                     in os { delay = md }

infoOptions =
    [ Option "h" ["help"] (NoArg (\o -> usage : o))
        "Show this usage information and quit"
    , Option "v" ["version"] (NoArg (\o -> version : o))
        "Show version information and quit"
    ]

parseArgs names = do
    args <- replace names `liftM` getArgs
    let parseWith os z = case getOpt Permute os args of
                            (o,_,_) -> foldr id z o
    let info = parseWith infoOptions []
    let options = parseWith argOptions defaultOptArgs
    let query = parseWith queryOptions defaultQuery
    return (info, options, query)

parseConfigFile = do
    configfile <- getAppUserDataDirectory "bustracker.conf"
    exists <- doesFileExist configfile
    if exists
        then readFile configfile >>= return . map (mkAssoc . words) . lines
        else return []
  where mkAssoc (a:b:xs) = (a,b)
          
trim (Just (First 0)) = id
trim (Just (First n)) = take n
trim (Just (Last  n)) = reverse . take n . reverse
trim Nothing = id

keep opts r = dis && rt && del
    where dis = if disabledOnly opts then resultDisabled r else True
          rt  = if realtimeOnly opts then not (resultEstimated r) else True
          del = maybe True (\m -> resultArrivalTime r > normalizeTimeDiff (noTimeDiff {tdMin = m})) (delay opts)

replace assocs = map (\l -> maybe l id (lookup l assocs))

-- Bus stop identifiers are N digit strings.
isStopId s = not (null s) && all isDigit s
-- Times are four digits long
isTime s = length s == 4 && all isDigit s
-- Bus numbers may be prefixed with a letter
isBusId s | isLetter (head s) = all isDigit (tail s)
          | otherwise         = all isDigit s

