{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}

-- | A generic interface to online radio services
module Web.Radio
  ( SongMeta(..)
  , Radio(..)
  , NeedLogin(..)
  , getLordDir
  , getPidFile
  , getLogFile
  , getStateFile
  , writeLog
  ) where

import           Control.Applicative ((<$>))
import           Control.Concurrent (forkIO, threadDelay)
import           Control.Concurrent.MVar
import           Control.Monad (liftM, when, void)
import           Data.Aeson hiding (encode)
import qualified Data.ByteString as B
import           Data.Maybe (isJust, fromJust)
import           Data.Monoid ((<>))
import           Data.String (fromString)
import           Data.Yaml
import           Network.MPD hiding (play, pause, config, Value)
import qualified Network.MPD as MPD
import           Network.MPD.Core (getResponse)
import           Network.Wai.Logger (ZonedDate, clockDateCacher)
import           System.Directory (doesFileExist, getHomeDirectory)
import           System.IO
import           System.IO.Unsafe (unsafePerformIO)
import           System.Log.FastLogger
import           System.Process


eof :: MVar ()
eof = unsafePerformIO newEmptyMVar

data SongMeta = SongMeta
    { artist    :: String
    , album     :: String
    , title     :: String
    }

instance Show SongMeta where
    show meta = artist meta ++ " - " ++ title meta

class FromJSON a => Radio a where
    data Param a :: *

    parsePlaylist :: Value -> [a]

    getPlaylist :: Param a -> IO [a]

    songUrl :: Param a -> a -> IO String

    songMeta :: a -> SongMeta

    tagged :: a -> Bool

    reportRequired :: a -> Bool
    reportRequired _ = False

    report :: Param a -> a -> IO ()
    report _ _ = return ()

    reportLoop :: Param a -> a -> IO ()
    reportLoop param x = do
        time <- liftM stTime <$> withMPD status
        case time of
            Right (Just (elapsed, _)) ->
                if elapsed < 30
                    then threadDelay (5*1000000) >> reportLoop param x
                    else report param x
            Right Nothing -> return ()
            Left err -> print err

    play :: LoggerSet -> Param a -> [a] -> IO ()
    play logger reqData xxs = do
        st <- withMPD status
        case st of
            Right _ -> playWithMPD logger reqData xxs
            Left  _ -> playWithMplayer logger reqData xxs

-- MPD can play remote m4a files directly since version-0.18
playWithMPD :: Radio a => LoggerSet -> Param a -> [a] -> IO ()
playWithMPD logger reqData [] =
    getPlaylist reqData >>= playWithMPD logger reqData
playWithMPD logger reqData (x:xs) = do
    surl <- songUrl reqData x
    print surl
    when (surl /= "") $ do
        logAndReport logger reqData x
        mpdLoad $ fromString surl
        takeMVar eof                     -- Finished
    playWithMPD logger reqData xs
  where
    mpdLoad :: Path -> IO ()
    mpdLoad path = do
        withMPD $ do
            clear
            add path
        withMPD $ MPD.play Nothing

        mpdTag $ songMeta x
        mpdPlay
    mpdPlay :: IO ()
    mpdPlay = do
        st <- mpdState
        if st == Right Stopped
            then putMVar eof ()
            else mpdPlay

playWithMplayer :: Radio a => LoggerSet -> Param a -> [a] -> IO ()
playWithMplayer logger reqData [] =
    getPlaylist reqData >>= playWithMplayer logger reqData
playWithMplayer logger reqData (x:xs) = do
    surl <- songUrl reqData x
    when (surl /= "") $ do
        logAndReport logger reqData x
        let sh = "mplayer -cache 2048 -cache-min 5 -novideo " ++ surl
        void $ waitForProcess =<< runCommand sh
    playWithMplayer logger reqData xs

logAndReport :: Radio a => LoggerSet -> Param a -> a -> IO ()
logAndReport logger reqData x = do
    writeLog logger (show $ songMeta x)
    getStateFile >>= flip writeFile (show $ songMeta x)
    -- Report song played if needed
    when (reportRequired x) $ void (forkIO $ reportLoop reqData x)

mpdState :: IO (MPD.Response State)
mpdState = do
    withMPD $ idle [PlayerS]
    -- This will block until paused/finished.

    st <- liftM stState <$> withMPD status
    print st
    return st

-- "addtagid" command is available since mpd-0.19
mpdTag :: SongMeta -> IO ()
mpdTag meta = void $ withMPD $ do
    cs <- currentSong
    when (isJust cs) $ do
        let (Id sid) = fromJust $ sgId $ fromJust cs
        void $ do
            addTag sid arTag
            addTag sid alTag
            addTag sid tiTag
  where
    addTag sid tag = getResponse $ "addtagid " ++ show sid ++ tag
    arTag = " artist \"" ++ artist meta ++ "\""
    alTag = " album \"" ++ album meta ++ "\""
    tiTag = " title \"" ++ title meta ++ "\""

class (Radio a, ToJSON (Param a), ToJSON (Config a)) => NeedLogin a where
    login :: String -> IO (Param a)
    login keywords = do
        hSetBuffering stdout NoBuffering
        hSetEcho stdin True
        putStrLn "Please Log in"
        putStr "Email: "
        email <- getLine
        putStr "Password: "
        hSetEcho stdin False
        pwd <- getLine
        hSetEcho stdin True
        putStrLn ""
        mtoken <- createSession keywords email pwd
        case mtoken of
             Just tok -> do
                 saveToken tok
                 return tok
             Nothing  -> do
                 putStrLn "ERROR: Invalid email or password!"
                 login keywords

    createSession :: String -> String -> String -> IO (Maybe (Param a))

    data Config a :: *

    mkConfig :: Param a -> Config a

    saveToken :: Param a -> IO ()
    saveToken tok = do
        yml <- getConfig
        exist <- doesFileExist yml
        bs <- if exist then B.readFile yml
                       else return ""
        let config = mkConfig tok
        B.writeFile yml $ B.append bs (encode config)
        putStrLn $ "Your token has been saved to " ++ yml

    mkParam :: Param a -> String -> Param a

    readToken :: FromJSON (Config a)
              => (Config a -> Param a) -> String -> IO (Maybe (Param a))
    readToken selector keywords = do
        yml <- getConfig
        exist <- doesFileExist yml
        if exist
            then do
                conf <- decodeFile yml
                case conf of
                    Nothing -> error $ "Invalid YAML file: " ++ show conf
                    Just c ->
                        case fromJSON c of
                            Success tok -> return $ Just $
                                mkParam (selector tok) keywords
                            Error err -> do
                                print $ "Parse token failed: " ++ show err
                                return Nothing
            else return Nothing

getLordDir :: IO FilePath
getLordDir = (++ "/.lord") <$> getHomeDirectory

getConfig :: IO FilePath
getConfig = (++ "/lord.yml") <$> getLordDir

getPidFile :: IO FilePath
getPidFile = (++ "/lord.pid") <$> getLordDir

getLogFile :: IO FilePath
getLogFile = (++ "/lord.log") <$> getLordDir

getStateFile :: IO FilePath
getStateFile = (++ "/lordstate") <$> getLordDir

formatLogMessage :: IO ZonedDate -> String -> IO LogStr
formatLogMessage getdate msg = do
    now <- getdate
    return $ toLogStr now <> " : " <> toLogStr msg <> "\n"

writeLog :: LoggerSet -> String -> IO ()
writeLog l msg = do
    (loggerDate, _) <- clockDateCacher
    formatLogMessage loggerDate msg >>= pushLogStr l