{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}

module IceAndFire
    ( Book(..)
    , Character(..)
    , House(..)
    , getBookById
    , getBookByName
    , getAllBooks
    , getCharacterById
    , getCharactersByName
    , getCharactersByCulture
    , getCharactersByGender
    , getHouseById
    , getHouseByName
    , getHousesByRegion
    , getHouseByWords
    ) where

import Network.Wreq
import Data.Aeson
import Data.Aeson.Types
import Data.ByteString.Char8 (unpack)
import Control.Lens

data Book = Book
    { bookUrl :: String
    , bookName :: String
    , isbn :: String
    , authors :: [String]
    , numberOfPages :: Integer
    , publisher :: String
    , country :: String
    , mediaType :: String
    , released :: String
    , characters :: [String]
    , povCharacters :: [String] 
    } deriving Show

instance FromJSON Book where
    parseJSON (Object v) = Book <$>
                           v .: "url" <*>
                           v .: "name" <*>
                           v .: "isbn" <*>
                           v .: "authors" <*>
                           v .: "numberOfPages" <*>
                           v .: "publisher" <*>
                           v .: "country" <*>
                           v .: "mediaType" <*>
                           v .: "released" <*>
                           v .: "characters" <*>
                           v .: "povCharacters"
    parseJSON invalid    = typeMismatch "Book" invalid

data Character = Character
    { charUrl :: String
    , charName :: String
    , gender :: String
    , culture :: String
    , born :: String
    , died :: String
    , charTitles :: [String]
    , aliases :: [String]
    , father :: String
    , mother :: String
    , spouse :: String
    , allegiances :: [String]
    , books :: [String]
    , povBooks :: [String]
    , tvSeries :: [String]
    , playedBy :: [String]
    } deriving Show

instance FromJSON Character where
    parseJSON (Object v) = Character <$>
                           v .: "url" <*>
                           v .: "name" <*>
                           v .: "gender" <*>
                           v .: "culture" <*>
                           v .: "born" <*>
                           v .: "died" <*>
                           v .: "titles" <*>
                           v .: "aliases" <*>
                           v .: "father" <*>
                           v .: "mother" <*>
                           v .: "spouse" <*>
                           v .: "allegiances" <*>
                           v .: "books" <*>
                           v .: "povBooks" <*>
                           v .: "tvSeries" <*>
                           v .: "playedBy"
    parseJSON invalid    = typeMismatch "Character" invalid

data House = House
    { houseUrl :: String
    , houseName :: String
    , region :: String
    , coatOfArms :: String
    , words :: String
    , houseTitles :: [String]
    , seats :: [String]
    , currentLord :: String
    , heir :: String
    , overlord :: String
    , founded :: String
    , founder :: String
    , diedOut :: String
    , ancestralWeapons :: [String]
    , cadetBranches :: [String]
    , swornMembers :: [String]
    } deriving Show

instance FromJSON House where
    parseJSON (Object v) = House <$>
                           v .: "url" <*>
                           v .: "name" <*>
                           v .: "region" <*>
                           v .: "coatOfArms" <*>
                           v .: "words" <*>
                           v .: "titles" <*>
                           v .: "seats" <*>
                           v .: "currentLord" <*>
                           v .: "heir" <*>
                           v .: "overlord" <*>
                           v .: "founded" <*>
                           v .: "founder" <*>
                           v .: "diedOut" <*>
                           v .: "ancestralWeapons" <*>
                           v .: "cadetBranches" <*>
                           v .: "swornMembers"
    parseJSON invalid    = typeMismatch "House" invalid

baseUrl :: String
baseUrl = "http://www.anapioficeandfire.com/api"

-- | Get Book by id number
getBookById :: Int -> IO (Maybe Book)
getBookById = loadSingleById "books"

-- | Get Book by name
getBookByName :: String -> IO [Book]
getBookByName bName = entityQuery "books" [("name", bName)]

-- | Get all books
getAllBooks :: IO [Book]
getAllBooks = entityQuery "books" []

-- | Get Character by id number
getCharacterById :: Int -> IO (Maybe Character)
getCharacterById = loadSingleById "characters"

-- | Get Characters by name
getCharactersByName :: String -> IO [Character]
getCharactersByName cName = entityQuery "characters" [("name", cName)]

-- | Get Character by culture
--
-- Example:
--
-- @
-- 'getCharactersByCulture' \"dothraki\"
-- @
--
-- >>> d <- getCharactersByCulture "dothraki"
-- >>> List.length d
-- 23
getCharactersByCulture :: String -> IO [Character]
getCharactersByCulture cCulture = entityQuery "characters" [("culture", cCulture)]

-- | Get Character by gender
--
-- Example:
--
-- @
-- 'getCharactersByGender' \"female\"
-- @
--
-- >>> f <- getCharactersByGender "female"
-- >>> List.length f
-- 461
getCharactersByGender :: String -> IO [Character]
getCharactersByGender cGender = entityQuery "characters" [("gender", cGender)]

-- | Get House by id number
getHouseById :: Int -> IO (Maybe House)
getHouseById = loadSingleById "houses"

-- | Get House by name
getHouseByName :: String -> IO [House]
getHouseByName hName = entityQuery "houses" [("name", hName)]

-- | Get House by region
--
-- Example:
--
-- @
-- 'getHousesByRegion' \"The Crownlands\"
-- @
--
-- >>> c <- getHousesByRegion "The Crownlands"
-- >>> List.length c
-- 49
getHousesByRegion :: String -> IO [House]
getHousesByRegion hRegion = entityQuery "houses" [("region", hRegion)]

-- | Get a House by its words
getHouseByWords :: String -> IO [House]
getHouseByWords hWords = entityQuery "houses" [("hasWords", "true"), ("words", hWords)]

loadSingleById :: (FromJSON a) => String -> Int -> IO (Maybe a)
loadSingleById entityType entityId = do
    response <- get (baseUrl ++ "/" ++ entityType ++ "/" ++ (show entityId) :: String)
    let entityJson = decode (view responseBody response)
    return entityJson

--Mainly used to control common query paramaters like pageSize
--Probably where caching should go
entityQuery :: (FromJSON a) => String -> [(String,String)] -> IO [a]
entityQuery entityType qParams = 
    loadFromQueryUrl (baseUrl ++ "/" ++ entityType ++ "/?pageSize=50" ++ paramsToStr qParams)
        where paramsToStr = foldl (\ acc (k,v) -> acc ++ "&" ++ k ++ "=" ++ v) ""

loadFromQueryUrl :: (FromJSON a) => String -> IO [a]
loadFromQueryUrl url = do
    response <- get url
    let entity = decode (view responseBody response)
    let unpacked = case entity of (Just [])   -> []
                                  (Just list) -> list
                                  Nothing     -> []
    -- Greedily consume and append 'next' links until none more
    let nextLink = (response ^? responseLink "rel" "next" . linkURL)
    let next = case nextLink of (Just linkUrl) -> loadFromQueryUrl (unpack linkUrl)
                                Nothing        -> return ([] :: [a])
    nextList <- next
    return (unpacked ++ nextList)