{-# LANGUAGE DataKinds #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE UnicodeSyntax #-}
-- | liblastfm internals
--
-- You shouldn't need to import this module unless you are doing something interesting.
module Network.Lastfm.Internal
  ( Request(..), Format(..), Auth(..), Ready
  , R(..), wrap, unwrap, Coercing(..), render
    -- * Lenses
  , host, method, query
  ) where

import Control.Applicative
import Data.Monoid

import           Data.Serialize (Serialize(..))
import           Data.ByteString (ByteString)
import           Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import           Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import           Network.URI (escapeURIChar, isUnreserved)


-- | Coerce requests changing their phantom parameters.
-- Used to ensure right flow of working with liblastfm. If you use it on your worn, then
-- you will break abstraction
class Coercing t where
  coerce  t (a  Auth) b  t c d


-- | Lastfm API request data type
--
-- low-level representation
data R (f  Format) (a  Auth) t = R
  { _host    {-# UNPACK #-} !Text
  , _method  {-# UNPACK #-} !ByteString
  , _query   !(Map Text Text)
  }

-- | Response format: either JSON or XML
data Format = JSON | XML

-- | Authentication method
data Auth =
    Send -- ^ Public API. Doesn't require anything special besides API key
  | Sign -- ^ Private API. Requires Session key and Secret as well as API key

-- | Indicates that request is ready for sending
data Ready

instance Coercing (R f) where
  coerce R { _host = h, _method = m, _query = q } = R { _host = h, _method = m, _query = q }
  {-# INLINE coerce #-}


-- | Lastfm API request data type
--
-- @a@ is authentication state. Might be 'Send' which indicates
-- that you may send this request already or 'Sign', when request signature
-- isn't computed yet
--
-- @f@ is response format. liblastfm currently supports JSON or XML
newtype Request f a t = Request { unRequest  Dual (Endo (R f a t)) }

instance Coercing (Request f) where
  coerce q = wrap $ coerce . unwrap q . coerce
  {-# INLINE coerce #-}

instance Functor (Request f a) where
  fmap _ = coerce
  {-# INLINE fmap #-}

instance Applicative (Request f a) where
  pure _ = wrap id
  f <*> x = let Request g = coerce f
                Request y = coerce x
            in Request $ g <> y
  {-# INLINE (<*>) #-}


-- | Construct String from request for networking
render  R f a t  String
render R { _host = h, _query = q } =
  T.unpack $ mconcat [h, "?", argie q]
 where
  argie = T.intercalate "&" . M.foldrWithKey (\k v m  T.concat [escape k, "=", escape v] : m) []

  escape = T.concatMap (T.pack . escapeURIChar isUnreserved)


-- | Wrapping to interesting 'Monoid' ('R' -> 'R') instance
wrap  (R f a t  R f a t)  Request f a t
wrap = Request . Dual . Endo
{-# INLINE wrap #-}


-- | Unwrapping from interesting 'Monoid' ('R' -> 'R') instance
unwrap  Request f a t  R f a t  R f a t
unwrap = appEndo . getDual . unRequest
{-# INLINE unwrap #-}


-- Miscellaneous instances

instance Serialize (R f a t) where
  put r = do
    put $ T.encodeUtf8 (_host r)
    put $ _method r
    put $ mapmap T.encodeUtf8 T.encodeUtf8 (_query r)
  get = do
    h  T.decodeUtf8 <$> get
    m  get
    q  mapmap T.decodeUtf8 T.decodeUtf8 <$> get
    return R { _host = h, _method = m, _query = q }

mapmap  (Ord s, Ord t)  (s  t)  (a  b)  Map s a  Map t b
mapmap f g = M.mapKeys f . M.map g


-- | Request _host lens
host :: Functor f => (Text -> f Text) -> R h a t -> f (R h a t)
host f r@R { _host = h } = (\h' -> r { _host = h' }) <$> f h

-- | Request http _method lens
method :: Functor f => (ByteString -> f ByteString) -> R h a t -> f (R h a t)
method f r@R { _method = m } = (\m' -> r { _method = m' }) <$> f m

-- | Request _query string lens
query :: Functor f => (Map Text Text -> f (Map Text Text)) -> R h a t -> f (R h a t)
query f r@R { _query = q } = (\q' -> r { _query = q' }) <$> f q