{-# LANGUAGE UnicodeSyntax #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- |
-- Module      :  Data.Rtorrent.StateFile
-- Copyright   :  (c) Mateusz Kowalczyk 2014
-- License     :  GPL-3
--
-- Maintainer  :  fuuzetsu@fuuzetsu.co.uk
-- Stability   :  experimental
-- Portability :  portable
--
-- Exposes few convenient functions to work with 'StateFile's. For the
-- types and the instances actually used during parsing see
-- "Data.Rtorrent.StateFile.Types".
--
-- A few handy lenses for accessing and manipulating a 'StateFile' are
-- provided but they are not meant to be comprehensive: for most
-- scenarios, you can just use the generated lenses directly.
--
-- An example use-case of this module is stopping all torrents without
-- relying on rtorrent itself. This might be useful if your rtorrent
-- has troubles starting up. Here it is as easy as:
--
-- @'overFilesIn' "rtorrent/session/dir" 'stopTorrent'@
module Data.Rtorrent.StateFile where

import           Control.Applicative ((<$>))
import           Control.Exception (catch, IOException)
import           Control.Lens ((.~), set, view)
import           Control.Monad ((>=>))
import           Data.BEncode (decode, encode, Result)
import           Data.ByteString (readFile, writeFile, length)
import qualified Data.ByteString.Lazy as L (toStrict, ByteString)
import           Data.ByteString.UTF8 (fromString, toString, ByteString)
import           Data.List (isSuffixOf)
import           Data.Map (Map, mapEither, fromList)
import           Data.Rtorrent.StateFile.Types
import           Prelude hiding (readFile, writeFile, length)
import           System.Directory (getDirectoryContents)
import           System.FilePath ((</>))


-- | Takes a directory, 'StateFile' modification function and does an
-- ‘in-place’ modifications to all .rtorrent files it can find and
-- parse in that directory. Returns the list of all file paths and any
-- potential errors that with each.
--
-- See 'getRtorrentFiles' for the type of exception this function can throw.
overFilesIn  FilePath  StateMod  IO [(FilePath, Maybe String)]
overFilesIn fp f =
  getRtorrentFiles fp >>= mapM (\x  (x,) <$> withRtorrentState x f)

-- | Gets a list of all @.rtorrent@ files in the specified directory.
--
-- This function uses 'getDirectoryContents' which can throw various
-- IOExceptions. It's up to the user to catch these if they wish to do
-- so.
getRtorrentFiles  FilePath  IO [FilePath]
getRtorrentFiles t =
  map (t </>) . filter (".rtorrent" `isSuffixOf`) <$> getDirectoryContents t

-- | Attempts to read in the file at specified file path, change it
-- with the user-supplied function and saved the file with changes.
--
-- In case the parsing fails, the result will be @Just errMsg@.
withRtorrentState  FilePath  StateMod  IO (Maybe String)
withRtorrentState fp f = parseFile fp >>= \case
  Left s  return $ Just s
  Right x  writeFile fp (encodeState' (f x)) >> return Nothing

-- | Only keeps properly parsing results. Useful with 'parseFiles'.
keepValid  Map FilePath (Result StateFile)  Map FilePath StateFile
keepValid = snd . mapEither id

-- | Similar to 'keepValid' but instead only keeps results that failed
-- to parse and the reason.
keepInvalid  Map FilePath (Result StateFile)  Map FilePath String
keepInvalid = fst . mapEither id

-- | Given a directory path, produces a 'Map' of file paths from
-- individual files to their parsing results.
--
-- This function uses 'getRtorrentFiles' which can throw an
-- IOException in case there is a problem reading the directory.
parseFiles  FilePath  IO (Map FilePath (Result StateFile))
parseFiles t = getRtorrentFiles t >>= mapM parseFile' >>= return . fromList

-- | Like 'parseFile'' but throws away the 'FilePath': useful if we
-- only play with single files.
parseFile  FilePath  IO (Result StateFile)
parseFile = parseFile' >=> return . snd

-- | Parses a file and returns a pair of of its path and the parsing
-- result. The path is useful if we're processing whole directories.
parseFile'  FilePath  IO (FilePath, Result StateFile)
parseFile' f = readFileE >>= return . (f,) . \case
  Left (e  IOException)  Left $ show e
  Right c  length c `seq` decodeState c
  where
    readFileE  IO (Either IOException ByteString)
    readFileE = (Right <$> readFile f) `catch` (return . Left)


-- | Sets the torrent to started state.
startTorrent  StateMod
startTorrent = state .~ 1

-- | Sets the torrent to stopped stated.
stopTorrent  StateMod
stopTorrent = state .~ 0

-- | Changes the file the torrent is tied to.
setTiedFile  FilePath  StateMod
setTiedFile = set tiedToFile . BS8 . fromString

-- | Gets a path of the loaded file.
getLoadedFile  StateFile  FilePath
getLoadedFile = toString . _bs8 . view loadedFile

-- | Wrapper for 'decode' which works for 'StateFile's.
decodeState  ByteString  Result StateFile
decodeState = decode

-- | Wrapper for 'encode' which works for 'StateFile's.
--
-- See 'encodeState'' for strict 'ByteString' version.
encodeState  StateFile  L.ByteString
encodeState = encode

-- | Encodes a 'StateFile' to a strict 'ByteString'.
encodeState'  StateFile  ByteString
encodeState' = L.toStrict . encode