{-# LANGUAGE TypeOperators, TypeFamilies, RankNTypes #-}

{-|
Module      : Action.Internals
Copyright   : (c) Kai Lindholm, 2014
License     : MIT
Maintainer  : megantti@gmail.com
Stability   : experimental

-}

module Network.RTorrent.Action.Internals (
      Action (..)
    , simpleAction
    , pureAction

    , sequenceActions
    , (<+>)

    , Param (..)
    , ActionB (..)

    , AllAction (..)
    , allToMulti 
) where

import Control.Applicative
import Control.Monad

import Data.Monoid
import Data.Traversable hiding (mapM)

import Network.XmlRpc.Internals
import Network.RTorrent.Command.Internals
import Network.RTorrent.Priority

-- | A type for actions that can act on different things like torrents and files.
--
-- @a@ is the return type.
data Action i a 
    = Action [(String, [Param])] (forall m. (Monad m, Applicative m) => Value -> m a) i

-- | Wrapper to get monoid and applicative instances.
newtype ActionB i a = ActionB { runActionB :: i -> Action i a} 

-- | A simple action that can be used when constructing new ones.
-- 
-- Watch out for using @Bool@ as @a@ since using it with this function will probably result in an error,
-- since RTorrent actually returns 0 or 1 instead of a bool.
-- One workaround is to get an @Int@ and use @Bool@'s @Enum@ instance.
simpleAction :: XmlRpcType a => 
       String
    -> [Param] 
    -> i
    -> Action i a
simpleAction cmd params = Action [(cmd, params)] parseSingle

instance Functor (Action i) where
    fmap f (Action cmds p fid) = Action cmds (fmap f . p) fid

instance Functor (ActionB i) where
    fmap f = ActionB . (fmap f .) . runActionB

instance Applicative (ActionB i) where
    pure a = ActionB $ Action [] (const (pure a))  

    (ActionB a) <*> (ActionB b) = ActionB $ \tid -> let 
        parse :: (Monad m, Applicative m) => (Value -> m (a -> b)) -> (Value -> m a) -> Value -> m b
        parse parseA parseB arr = do
            (valsA, valsB) <- splitAt len <$> getArray arr
            parseA (ValueArray valsA) 
              <*> parseB (ValueArray valsB) 
        len = length cmdsA
        Action cmdsA pA _ = a tid
        Action cmdsB pB _ = b tid
      in Action (cmdsA ++ cmdsB) (parse pA pB) tid

instance Monoid a => Monoid (ActionB i a) where
    mempty = pure mempty
    mappend = liftA2 mappend  

instance XmlRpcType i => Command (Action i a) where
    type Ret (Action i a) = a

    commandCall (Action cmds _ tid) = 
          RTMethodCall 
        . ValueArray
        . concatMap (\(cmd, params) -> 
                        getArray'
                      . runRTMethodCall $ mkRTMethodCall cmd 
                                    (toValue tid : map toValue params))
        $ cmds

    commandValue (Action _ parse _) = parse
    levels (Action cmds _ _) = length cmds

-- | Parameters for actions.
data Param = 
    PString String
  | PInt Int
  | PTorrentPriority TorrentPriority
  | PFilePriority FilePriority

instance Show Param where
    show (PString str) = show str
    show (PInt i) = show i
    show (PTorrentPriority p) = show (fromEnum p)
    show (PFilePriority p) = show (fromEnum p)

instance XmlRpcType Param where
    toValue (PString str) = toValue str
    toValue (PInt i) = toValue i
    toValue (PTorrentPriority p) = toValue p
    toValue (PFilePriority p) = toValue p

    fromValue = fail "No fromValue for Params"
    getType _ = TUnknown

-- | Sequence multiple actions, for example with @f = []@.
sequenceActions :: Traversable f => f (i -> Action i a) -> i -> Action i (f a)
sequenceActions = runActionB . traverse ActionB

-- | An action that does nothing but return the value.
pureAction :: a -> i -> Action i a
pureAction a = Action [] (const (return a))

infixr 6 <+>
-- | Combine two actions to get a new one.
(<+>) :: (i -> Action i a) -> (i -> Action i b) -> i -> Action i (a :*: b)
a <+> b = runActionB $ (:*:) <$> ActionB a <*> ActionB b

data AllAction i a = AllAction i String (i -> Action i a)

makeMultiCall :: [(String, [Param])] -> [String]
makeMultiCall = ("" :) 
              . map (\(cmd, params) -> cmd ++ "=" ++ makeList params)
  where
    makeList :: Show a => [a] -> String
    makeList params = ('{' :) . go params $ "}"
      where
        go :: Show a => [a] -> ShowS
        go [x] = shows x 
        go (x:xs) = shows x . (',' :) . go xs
        go [] = id

wrapForParse :: Monad m => Value -> m [Value]
wrapForParse = mapM ( 
                 return . ValueArray 
                 . map (ValueArray . (:[])) 
                 <=< getArray) 
               <=< getArray <=< single <=< single

allToMulti :: AllAction i a -> j -> Action j [a]
allToMulti (AllAction emptyId multicall action) = 
    Action [(multicall, map PString $ makeMultiCall cmds)] 
           (mapM parse <=< wrapForParse)
  where 
    Action cmds parse _ = action emptyId
    
instance Command (AllAction i a) where
    type Ret (AllAction i a) = [a]
    commandCall (AllAction emptyId multicall action) = 
                      mkRTMethodCall multicall
                    . map ValueString
                    . makeMultiCall 
                    $ cmds
      where
        Action cmds _ _ = action emptyId

    commandValue (AllAction emptyId _ action) = 
        mapM parse <=< wrapForParse
      where
        Action _ parse _ = action emptyId