{-# LANGUAGE FlexibleContexts #-}

module Rdioh where
import Rdioh.Auth
import Data.Either 
import qualified Data.ByteString.Lazy.Char8 as B
import qualified Data.List.Utils as U
import Control.Monad.Reader

import Network.OAuth.Consumer
import Network.OAuth.Http.Request
import Network.OAuth.Http.Response
import Network.OAuth.Http.HttpClient
import Network.OAuth.Http.CurlHttpClient
-- import Network.OAuth.Http.PercentEncoding
import Rdioh.Util
import Rdioh.Models
import Control.Applicative
import Data.Aeson
import qualified Debug.Trace as D

-- | Takes: a key, a secret, a function to run.
runRdio :: String -> String -> Rdio a -> IO a
runRdio key secret func = runReaderT func (twoLegToken key secret)

-- | Same as @runRdio@, but with 3-legged authentication i.e. the user will
-- | have to authorize your app.
runRdioWithAuth :: String -> String -> Rdio a -> IO a
runRdioWithAuth key secret func = do
    tok <- liftIO (threeLegToken key secret)
    runReaderT func tok

-- | The @Rdio@ monad...just a wrapper around a @ReaderT@ monad.
type Rdio a = ReaderT Token IO a

-- | used internally
mkExtras :: Show e => [e] -> (String, String)
mkExtras extras = ("extras", U.join "," $ show <$> extras)

-- | Send a arbitrary request to rdio's api. Return type should
-- | be an instance of @FromJSON@, and you need to specify the type. Example:
--
-- > result <- (runRequest [("method", "getTopCharts"), ("type", "Artist")] :: Rdio (Either String [Artist]))
runRequest :: (Show v, FromJSON v) => [(String, String)] -> Rdio (Either String v)
runRequest params = do
    tok <- ask
    let request = srvUrl . B.pack . toParams $ params
    response <- liftIO $ runOAuthM tok $ do
                      request_ <- signRq2 HMACSHA1 Nothing request
                      serviceRequest CurlClient request_

    -- D.trace (B.unpack . rspPayload $ response) (return ())
    let value = eitherDecode . rspPayload $ response
    return $ rdioResult <$> value

-- RDIO methods

-- CORE methods TODO they all have multiple return types.

-- TODO currently unsupported because it has a variable specification.
-- All you can really do is request an Object back.

-- | Takes: [keys], [extras] (optional)
get :: (Show a, FromJSON a) => [String] -> [String] -> Rdio (Either String a)
get keys extras = runRequest $ [("method", "get"), ("keys", toParam keys), ("extras", toParam extras)]

-- | Takes: short code (everything after the http://rd.io/x/), [extras]
-- (optional)
getObjectFromShortCode :: (Show a, FromJSON a) => String -> [String] -> Rdio (Either String a)
getObjectFromShortCode shortCode extras = runRequest $ [("method", "getObjectFromShortCode"), ("short_code", shortCode), ("extras", toParam extras)]

-- | Takes: url (everything after http://rdio.com/), [extras] (optional)
getObjectFromUrl :: (Show a, FromJSON a) => String -> [String] -> Rdio (Either String a)
getObjectFromUrl url extras = runRequest $ [("method", "getObjectFromUrl"), ("url", url), ("extras", toParam extras)]

-- CATALOG methods

-- | Takes: a UPC code, [extras] (optional)
getAlbumsByUPC :: Int -> [AlbumExtra] -> Rdio (Either String [Album])
getAlbumsByUPC upc extras = runRequest $ [("method", "getAlbumsByUPC"), ("upc", toParam upc), mkExtras extras]

-- | Takes: A key of an artist
getAlbumsForArtist :: String -> Rdio (Either String [Album])
getAlbumsForArtist artist = getAlbumsForArtist' artist Nothing [] Nothing Nothing

getAlbumsForArtist' :: String -> Maybe Bool -> [AlbumExtra] -> Maybe Int -> Maybe Int -> Rdio (Either String [Album])
getAlbumsForArtist' artist featuring extras start count =
    runRequest $ [("method", "getAlbumsForArtist"), ("artist", artist), mkExtras extras]
                   <+> ("featuring", featuring)
                   <+> ("start", start)
                   <+> ("count", count)

-- | Takes: a key of a label
getAlbumsForLabel :: String -> Rdio (Either String [Album])
getAlbumsForLabel label = getAlbumsForLabel' label [] Nothing Nothing

getAlbumsForLabel' :: String -> [AlbumExtra] -> Maybe Int -> Maybe Int -> Rdio (Either String [Album])
getAlbumsForLabel' label extras start count = 
    runRequest $ [("method", "getAlbumsForLabel"), ("label", label), mkExtras extras]
                   <+> ("start", start)
                   <+> ("count", count)

-- | Takes: a key of a label
getArtistsForLabel :: String -> Rdio (Either String [Artist])
getArtistsForLabel label = getArtistsForLabel' label [] Nothing Nothing

getArtistsForLabel' :: String -> [ArtistExtra] -> Maybe Int -> Maybe Int -> Rdio (Either String [Artist])
getArtistsForLabel' label extras start count = 
    runRequest $ [("method", "getArtistsForLabel"), ("label", label), mkExtras extras]
                   <+> ("start", start)
                   <+> ("count", count)

-- | Takes: an ISRC code, [extras] (optional)
getTracksByISRC :: String -> [TrackExtra] -> Rdio (Either String [Track])
getTracksByISRC isrc extras = runRequest $ [("method", "getTracksByISRC"), ("isrc", isrc), mkExtras extras]

-- | Takes: an artist key
getTracksForArtist :: String -> Rdio (Either String [Track])
getTracksForArtist artist = getTracksForArtist' artist Nothing [] Nothing Nothing

getTracksForArtist' :: String -> Maybe Bool -> [TrackExtra] -> Maybe Int -> Maybe Int -> Rdio (Either String [Track])
getTracksForArtist' artist appears_on extras start count =
    runRequest $ [("method", "getTracksForArtist"), ("artist", artist), mkExtras extras]
                    <+> ("appears_on", appears_on)
                    <+> ("start", start)
                    <+> ("count", count)

-- TODO this should also have an extras field but how to implement that and
-- still be able to return a generic type?
-- | Takes: a query, a type (\"Artist\", \"Album\", \"Track\", \"Playlist\", or
-- \"User\")
-- This method can return any of those types, so you need to specify what
-- you want returned. Example:
--
-- > search "Radiohead" "Artist" :: Rdio (Either String [Artist])
search :: (Show a, FromJSON a) => String -> String -> Rdio (Either String [a])
search query types = search' query types Nothing Nothing Nothing

search' :: (Show a, FromJSON a) => String -> String -> Maybe Bool -> Maybe Int -> Maybe Int -> Rdio (Either String [a])
search' query types neverOr start count = do
    res <- runRequest $ [("method", "search"), ("query", query), ("types", types)]
                   <+> ("never_or", neverOr)
                   <+> ("start", start)
                   <+> ("count", count)

    return (results <$> res)

-- TODO searchSuggestions

-- | Takes: a list of keys of tracks or playlists. *Requires
-- authentication*.
addToCollection :: [String] -> Rdio (Either String Object)
addToCollection keys = runRequest $ [("method", "addToCollection"), ("keys", toParam keys)]

-- | Takes: an artist key. Requires authentication OR use @getAlbumsForArtistInCollection'@ and pass in a user key.
getAlbumsForArtistInCollection :: String -> Rdio (Either String [Album])
getAlbumsForArtistInCollection artist = getAlbumsForArtistInCollection' artist Nothing [] Nothing

getAlbumsForArtistInCollection' :: String -> Maybe String -> [AlbumExtra] -> Maybe String -> Rdio (Either String [Album])
getAlbumsForArtistInCollection' artist user extras sort = 
    runRequest $ [("method", "getAlbumsForArtistInCollection"), ("artist", artist), mkExtras extras]
                  <+> ("user", user)
                  <+> ("sort", sort)

-- | Requires authentication OR use @getAlbumsInCollection'@ and pass in
-- a user key.
getAlbumsInCollection :: Rdio (Either String [Album])
getAlbumsInCollection = getAlbumsInCollection' Nothing Nothing Nothing Nothing Nothing []

getAlbumsInCollection' :: Maybe String -> Maybe Int -> Maybe Int -> Maybe String -> Maybe String -> [AlbumExtra] -> Rdio (Either String [Album])
getAlbumsInCollection' user start count sort query extras = 
    runRequest $ [("method", "getAlbumsInCollection"), mkExtras extras]
                  <+> ("user", user)
                  <+> ("start", start)
                  <+> ("count", count)
                  <+> ("sort", sort)
                  <+> ("query", query)

-- | Requires authentication OR use @getArtistsInCollection'@ and pass in
-- a user key.
getArtistsInCollection :: Rdio (Either String [Artist])
getArtistsInCollection = getArtistsInCollection' Nothing Nothing Nothing Nothing Nothing []

getArtistsInCollection' :: Maybe String -> Maybe Int -> Maybe Int -> Maybe String -> Maybe String -> [ArtistExtra] -> Rdio (Either String [Artist])
getArtistsInCollection' user start count sort query extras = 
    runRequest $ [("method", "getArtistsInCollection"), mkExtras extras]
                  <+> ("user", user)
                  <+> ("start", start)
                  <+> ("count", count)
                  <+> ("sort", sort)
                  <+> ("query", query)

-- | Requires authentication.
getOfflineTracks :: Rdio (Either String [Track])
getOfflineTracks = getOfflineTracks' Nothing Nothing []

getOfflineTracks' :: Maybe Int -> Maybe Int -> [TrackExtra] -> Rdio (Either String [Track])
getOfflineTracks' start count extras = 
    runRequest $ [("method", "getOfflineTracks"), mkExtras extras]
                  <+> ("start", start)
                  <+> ("count", count)

-- | Takes: an album key. Requires authentication OR use @getTracksForAlbumInCollection'@ and pass in a user key.
getTracksForAlbumInCollection :: String -> Rdio (Either String [Track])
getTracksForAlbumInCollection album = getTracksForAlbumInCollection' album Nothing []

getTracksForAlbumInCollection' :: String -> Maybe String -> [TrackExtra] -> Rdio (Either String [Track])
getTracksForAlbumInCollection' album user extras =
    runRequest $ [("method", "getTracksForAlbumInCollection"), ("album", album), mkExtras extras]
                  <+> ("user", user)

-- | Takes: an artist key. Requires authentication OR use @getTracksForArtistInCollection'@ and pass in a user key.
getTracksForArtistInCollection :: String -> Rdio (Either String [Track])
getTracksForArtistInCollection artist = getTracksForArtistInCollection' artist Nothing []

getTracksForArtistInCollection' :: String -> Maybe String -> [TrackExtra] -> Rdio (Either String [Track])
getTracksForArtistInCollection' artist user extras =
    runRequest $ [("method", "getTracksForArtistInCollection"), ("artist", artist), mkExtras extras]
                  <+> ("user", user)

-- | Requires authentication OR use @getTracksInCollection'@ and pass in
-- a user key.
getTracksInCollection :: Rdio (Either String [Track])
getTracksInCollection = getTracksInCollection' Nothing Nothing Nothing Nothing Nothing []

getTracksInCollection' :: Maybe String -> Maybe Int -> Maybe Int -> Maybe String -> Maybe String -> [TrackExtra] -> Rdio (Either String [Track])
getTracksInCollection' user start count sort query extras = 
    runRequest $ [("method", "getTracksInCollection"), mkExtras extras]
                   <+> ("user", user)
                   <+> ("start", start)
                   <+> ("count", count)
                   <+> ("sort", sort)
                   <+> ("query", query)

-- | Takes: a list of track or playlist keys. Requires authentication.
removeFromCollection :: [String] -> Rdio (Either String Bool)
removeFromCollection keys = runRequest $ [("method", "removeFromCollection"), ("keys", toParam keys)]

-- | Takes: a list of track or playlist keys. Requires authentication.
setAvailableOffline :: [String] -> Bool -> Rdio (Either String Object)
setAvailableOffline keys offline = runRequest $ [("method", "setAvailableOffline"), ("keys", toParam keys), ("offline", toParam offline)]

-- | Takes: a playlist key, a list of track keys to add to the playlist,
-- [extras] (optional).
-- Requires authentication.
addToPlaylist :: String -> [String] -> [PlaylistExtra] -> Rdio (Either String Playlist)
addToPlaylist playlist tracks extras = runRequest $ [("method", "addToPlaylist"), ("playlist", playlist), ("tracks", toParam tracks), mkExtras extras]

-- | Takes: a name, a description, a list of track keys to start the
-- playlist with, [extras] (optional). Requires authentication.
createPlaylist :: String -> String -> [String] -> [PlaylistExtra] -> Rdio (Either String Playlist)
createPlaylist name description tracks extras =
    runRequest $ [("method", "createPlaylist"), ("name", name), ("description", description), ("tracks", toParam tracks), mkExtras extras]

-- | Takes: a playlist key. Requires authentication.
deletePlaylist :: String -> Rdio (Either String Bool)
deletePlaylist playlist = runRequest $ [("method", "deletePlaylist"), ("playlist", playlist)]

-- | Requires authentication OR use @getPlaylists'@ and pass in a user key.
getPlaylists :: Rdio (Either String UserPlaylists)
getPlaylists = getPlaylists' Nothing [] Nothing

getPlaylists' :: Maybe String -> [PlaylistExtra] -> Maybe Bool -> Rdio (Either String UserPlaylists)
getPlaylists' user extras orderedList = 
    runRequest $ [("method", "getPlaylists"), mkExtras extras]
                   <+> ("user", user)
                   <+> ("ordered_list", orderedList)

-- | Requires authentication OR use @getUserPlaylists'@ and pass in a user key.
getUserPlaylists :: String -> Rdio (Either String [Playlist])
getUserPlaylists user = getUserPlaylists' user Nothing Nothing Nothing Nothing []

getUserPlaylists' :: String -> Maybe PlaylistType -> Maybe String -> Maybe Int -> Maybe Int -> [PlaylistExtra] -> Rdio (Either String [Playlist])
getUserPlaylists' user kind sort start count extras =
    runRequest $ [("method", "getUserPlaylists"), ("user", user), mkExtras extras]
                   <+> ("kind", kind)
                   <+> ("sort", sort)
                   <+> ("start", start)
                   <+> ("count", count)

-- | Takes:
-- - a playlist key
--
-- - the index of the first item to remove
--
-- - number of tracks to remove
--
-- - the keys of the tracks to remove (redundancy to prevent accidental
-- deletion)
--
-- - [extras] (optional)
--
-- Requires authentication.
removeFromPlaylist :: String -> Int -> Int -> Int -> [PlaylistExtra] -> Rdio (Either String Playlist)
removeFromPlaylist playlist index count tracks extras =
    runRequest $ [("method", "removeFromPlaylist"), ("playlist", playlist), ("index", toParam index), ("count", toParam count), ("tracks", toParam tracks), mkExtras extras]

-- | Takes: a playlist key, a boolean (true == collaborating, false == not
-- collaborating). Requires authentication.
setPlaylistCollaborating :: String -> Bool -> Rdio (Either String Bool)
setPlaylistCollaborating playlist collaborating =
    runRequest $ [("method", "setPlaylistCollaborating"), ("playlist", playlist), ("collaborating", toParam collaborating)]

-- | Takes: a playlist key, a collaboration mode. Requires authentication.
setPlaylistCollaborationMode :: String -> CollaborationMode -> Rdio (Either String Bool)
setPlaylistCollaborationMode playlist mode =
    runRequest $ [("method", "setPlaylistCollaborationMode"), ("playlist", playlist), ("mode", toParam mode)]

-- | Takes: a playlist key, a name, a description. Requires authentication.
setPlaylistFields :: String -> String -> String -> Rdio (Either String Bool)
setPlaylistFields playlist name description =
    runRequest $ [("method", "setPlaylistFields"), ("playlist", playlist), ("name", name), ("description", description)]

-- | Takes: a playlist key, a list of track keys, [extras] (optional).
-- Requires authentication.
setPlaylistOrder :: String -> [String] -> [PlaylistExtra] -> Rdio (Either String Playlist)
setPlaylistOrder playlist tracks extras =
    runRequest $ [("method", "setPlaylistOrder"), ("playlist", playlist), ("tracks", toParam tracks), mkExtras extras]

-- | Takes: a user key. Requires authentication.
addFriend :: String -> Rdio (Either String Bool)
addFriend user = runRequest $ [("method", "addFriend"), ("user", user)]

-- | Requires authentication.
currentUser :: [UserExtra] -> Rdio (Either String User)
currentUser extras = runRequest $ [("method", "currentUser"), mkExtras extras]

-- | Takes: an email address, [extras] (optional).
findUserByEmail :: String -> [UserExtra] -> Rdio (Either String User)
findUserByEmail email extras = runRequest $ [("method", "findUser"), ("email", email), mkExtras extras]

-- | Takes: user name, [extras] (optional).
findUserByName :: String -> [UserExtra] -> Rdio (Either String User)
findUserByName vanityName extras = runRequest $ [("method", "findUser"), ("vanityName", vanityName), mkExtras extras]

-- | Takes: a user key. Requires authentication.
removeFriend :: String -> Rdio (Either String Bool)
removeFriend user = runRequest $ [("method", "removeFriend"), ("user", user)]

-- | Takes: a user key.
userFollowers :: String -> Rdio (Either String [User])
userFollowers user = userFollowers' user Nothing Nothing [] Nothing

userFollowers' :: String -> Maybe Int -> Maybe Int -> [UserExtra] -> Maybe String -> Rdio (Either String [User])
userFollowers' user start count extras inCommon =
    runRequest $ [("method", "userFollowers"), ("user", user), mkExtras extras]
                   <+> ("start", start)
                   <+> ("count", count)
                   <+> ("inCommon", inCommon)

-- | Takes: a user key.
userFollowing :: String -> Rdio (Either String [User])
userFollowing user = userFollowing' user Nothing Nothing [] Nothing

userFollowing' :: String -> Maybe Int -> Maybe Int -> [UserExtra] -> Maybe String -> Rdio (Either String [User])
userFollowing' user start count extras inCommon =
    runRequest $ [("method", "userFollowing"), ("user", user), mkExtras extras]
                   <+> ("start", start)
                   <+> ("count", count)
                   <+> ("inCommon", inCommon)


-- TODO extras not implemented here because I don't know what it means in
-- this context. Also, the docs say there will be additional data depending
-- on the update_type. Without specifying crap. So this is incomplete for
-- now.

-- | Takes: a user key, a scope.
getActivityStream :: String -> Scope -> Rdio (Either String Activity)
getActivityStream user scope = getActivityStream' user scope Nothing Nothing

getActivityStream' :: String -> Scope -> Maybe Int -> Maybe Int -> Rdio (Either String Activity)
getActivityStream' user scope lastId count = 
    runRequest $ [("method", "getActivityStream"), ("user", user), ("scope", toParam scope)]
                   <+> ("last_id", lastId)
                   <+> ("count", count)

getHeavyRotationArtists :: Rdio (Either String [Artist])
getHeavyRotationArtists = getHeavyRotationArtists' Nothing Nothing Nothing Nothing Nothing []

getHeavyRotationArtists' :: Maybe String -> Maybe Bool -> Maybe Int -> Maybe Int -> Maybe Int -> [ArtistExtra] -> Rdio (Either String [Artist])
getHeavyRotationArtists' user friends limit start count extras =
    runRequest $ [("method", "getHeavyRotation"), ("type", "artists"), mkExtras extras]
                   <+> ("user", user)
                   <+> ("friends", friends)
                   <+> ("limit", limit)
                   <+> ("start", start)
                   <+> ("count", count)

getHeavyRotationAlbums :: Rdio (Either String [Album])
getHeavyRotationAlbums = getHeavyRotationAlbums' Nothing Nothing Nothing Nothing Nothing []

getHeavyRotationAlbums' :: Maybe String -> Maybe Bool -> Maybe Int -> Maybe Int -> Maybe Int -> [AlbumExtra] -> Rdio (Either String [Album])
getHeavyRotationAlbums' user friends limit start count extras =
    runRequest $ [("method", "getHeavyRotation"), ("type", "artists"), mkExtras extras]
                   <+> ("user", user)
                   <+> ("friends", friends)
                   <+> ("limit", limit)
                   <+> ("start", start)
                   <+> ("count", count)

getNewReleases :: Rdio (Either String [Album])
getNewReleases = getNewReleases' Nothing Nothing Nothing []

getNewReleases' :: Maybe Timeframe -> Maybe Int -> Maybe Int -> [AlbumExtra] -> Rdio (Either String [Album])
getNewReleases' time start count extras =
    runRequest $ [("method", "getNewReleases"), mkExtras extras]
                   <+> ("time", time)
                   <+> ("start", start)
                   <+> ("count", count)

getTopChartArtists :: Rdio (Either String [Artist])
getTopChartArtists = getTopChartArtists' Nothing Nothing []

getTopChartArtists' :: Maybe Int -> Maybe Int -> [ArtistExtra] -> Rdio (Either String [Artist])
getTopChartArtists' start count extras =
    runRequest $ [("method", "getTopCharts"), ("type", "Artist"), mkExtras extras]
                   <+> ("start", start)
                   <+> ("count", count)

getTopChartAlbums :: Rdio (Either String [Album])
getTopChartAlbums = getTopChartAlbums' Nothing Nothing []

getTopChartAlbums' :: Maybe Int -> Maybe Int -> [AlbumExtra] -> Rdio (Either String [Album])
getTopChartAlbums' start count extras =
    runRequest $ [("method", "getTopCharts"), ("type", "Album"), mkExtras extras]
                   <+> ("start", start)
                   <+> ("count", count)


getTopChartTracks :: Rdio (Either String [Track])
getTopChartTracks = getTopChartTracks' Nothing Nothing []

getTopChartTracks' :: Maybe Int -> Maybe Int -> [TrackExtra] -> Rdio (Either String [Track])
getTopChartTracks' start count extras =
    runRequest $ [("method", "getTopCharts"), ("type", "Track"), mkExtras extras]
                   <+> ("start", start)
                   <+> ("count", count)


getTopChartPlaylists :: Rdio (Either String [Playlist])
getTopChartPlaylists = getTopChartPlaylists' Nothing Nothing []

getTopChartPlaylists' :: Maybe Int -> Maybe Int -> [PlaylistExtra] -> Rdio (Either String [Playlist])
getTopChartPlaylists' start count extras =
    runRequest $ [("method", "getTopCharts"), ("type", "Playlist"), mkExtras extras]
                   <+> ("start", start)
                   <+> ("count", count)

-- | Takes: the domain that the playback SWF will be embedded in
-- (optional).
getPlaybackToken :: Maybe String -> Rdio (Either String String)
getPlaybackToken domain = runRequest $ [("method", "getPlaybackToken")] <+> ("domain", domain)