{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UnicodeSyntax #-}
{-# OPTIONS_HADDOCK show-extensions #-}

-- |
-- Module      :  HBooru.Network
-- Copyright   :  (c) Mateusz Kowalczyk 2013-2014
-- License     :  GPL-3
--
-- Maintainer  :  fuuzetsu@fuuzetsu.co.uk
-- Stability   :  experimental
--
-- Module providing functions to interface with some booru sites.
-- Amongst other things, it should (semi-transparently) handle post count
-- limits. The user should simply be able to ask for all images with certain
-- rather than worrying about hard limits per page set by the sites &c.
module HBooru.Network where

import Control.Applicative ((<$>))
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Error
import Data.ByteString.Lazy (toStrict, writeFile)
import Data.ByteString.UTF8
import Data.Either (rights)
import HBooru.Parsers.Ichijou
import HBooru.Types
import Network.HTTP.Conduit (simpleHttp, HttpException(..))

-- | Given a 'Site', 'DataFormat' and a list of 'Tag's, naively fetch the first
-- page or so and parse it to the appropriate image type. Both the site and the
-- format need to together form an instance of 'Postable' and the data format
-- has to exist in an instance of 'CoerceResponse'. Uses 'fetchPostPage' to
-- fetch the data.
fetchTaggedPosts
   (Postable s d, CoerceResponse d r)  s  d  [Tag]  ExcIO [ImageTy s d]
fetchTaggedPosts s d ts = parseResponse s <$> fetchPostPage s d ts

-- | As 'fetchTaggedPosts' but works with sites which allow indexing by page.
fetchTaggedPostsIndexed  (CoerceResponse r a, PostablePaged s r)
                           s  r  [Tag]  Integer  ExcIO [ImageTy s r]
fetchTaggedPostsIndexed s d ts i =
  parseResponse s <$> fetchPostPageIndexed s d ts i

-- | Given an instance of 'Postable', 'CoerceResponse', and a list of 'Tag's,
-- fetch the post page.
fetchPostPage  (Postable s d, CoerceResponse d r)  s  d  [Tag]  ExcIO r
fetchPostPage s d ts = fetchResponse (postUrl s d ts) d

-- | Given an instance of 'Postable', 'CoerceResponse', and a list of 'Tag's,
-- fetch the post page.
fetchPostPageIndexed  (PostablePaged s d, CoerceResponse d r)
                        s  d  [Tag]  Integer  ExcIO r
fetchPostPageIndexed s d ts i = fetchResponse (postUrlPaged s d ts i) d

-- | Given a URL and protocol, tries to fetch a response.
fetchResponse  CoerceResponse r r'  String  r  ExcIO r'
fetchResponse u r = do
  liftIO (try (simpleHttp u)) >>= \case
    Left (e  HttpException)  throwError $ Network e
    Right x  return . toResponse r . toString . toStrict $ x

-- | Uses 'fetchPostPage' to parse the number of posts available based on
-- provided 'Tag's.
fetchPostCount
   (Postable s r, Counted s r, CoerceResponse r a)  s  r  [Tag]
   ExcIO Integer
fetchPostCount s d ts = parseCount s <$> fetchPostPage s d ts

-- | Attemps to fetch all posts from a site, from all its pages. The
-- upper limit of images per page is used.
fetchAllTaggedPosts
   (CoerceResponse r a, PostablePaged s r)  s  r  [Tag]  IO [ImageTy s r]
fetchAllTaggedPosts s r ts = do
  runErrorT (fetchPostCount s r ts) >>= \case
    Left e  print e >> return []
    Right i  do
      let count = fromIntegral i  Double
          pages = case hardLimit s r of
            NoLimit  0
            Limit x  max 0 (ceiling $ (count / fromIntegral x) - 1)
      r'  mapM (runErrorT . fetchTaggedPostsIndexed s r ts) [0 .. pages]
      return . concat $ rights r'

data DownloadStatus = OK String
                    | Failed (Either HttpException IOException, String)
                    | EndOfQueue
                    deriving Show

-- | Downloads the given files. Writes the status information back to
-- the provided TChan.
downloadFiles  [(String, FilePath)] -- ^ URL with save location
               TChan DownloadStatus -- ^ Channel to send back status info on
               Int -- ^ Max threads to run at once. Bounded to minimum of 1.
               IO ()
downloadFiles ts ds mt = do
  tv  atomically $ newTVar ts
  count  atomically $ newTVar (0  Int)
  let maxThreads = max 1 mt
      wf x = x `seq` Data.ByteString.Lazy.writeFile x
      modCount f = atomically $ modifyTVar count (\x -> max 0 (f x))
      runDownload (url, path) =
        try (simpleHttp url) >>= \case
          Left (e  HttpException) 
            atomically . writeTChan ds $ Failed (Left e, url)
          Right c  try (wf path c) >>= atomically . \case
            Left (e  IOException)  writeTChan ds $ Failed (Right e, url)
            Right _  writeTChan ds $ OK url
      readVs = atomically $ liftM2 (,) (readTVar tv) (readTVar count)
      spawnThreads = readVs >>= \case
        ([], 0)  atomically $ writeTChan ds EndOfQueue
        (ys, n) | n >= maxThreads  spawnThreads
                | otherwise  case ys of
                  []  threadDelay 10000 >> spawnThreads
                  x:xs  void $ do
                    atomically (writeTVar tv xs)
                    forkIO $ modCount succ >> runDownload x >> modCount pred
                    spawnThreads
  spawnThreads