-- | This package allows to use @bit.ly@ and @j.mp@ URL shortening service
-- from Haskell programs. See also "Network.TinyURL" module.

module Network.Bitly
  (Account(..), bitlyAccount, jmpAccount, shorten, expand, Result)
where

import Network.HTTP
import Text.XML.HaXml
import Text.XML.HaXml.Pretty (element, content)

-- | Service credentials.
data Account = Account
  { login  :: String,   -- ^ bit.ly login name
    apikey :: String,   -- ^ API key as found at <http://bit.ly/account/>
    server :: String    -- ^ Server to use, e.g. @http:\/\/api.j.mp@
  } deriving (Read, Show)

-- | Account to use with bit.ly
bitlyAccount :: Account
bitlyAccount = Account
  { login = "", apikey = "", server = "http://api.bit.ly" }

-- | Account to use with j.mp
jmpAccount :: Account
jmpAccount = Account
  { login = "", apikey = "", server = "http://api.j.mp" }

-- | Either an error message or a modified URL
type Result = Either String String

-- | Given a long URL, @shorten@ encodes it as a shorter one.
shorten :: Account   -- ^ Account to use
        -> String    -- ^ Long URL
        -> IO Result -- ^ Either error or short bit.ly URL
shorten acc url = request acc "shorten" [("longUrl", url)]
                              [ "bitly", "results", "nodeKeyVal", "shortUrl" ]

-- | Given a short bit.ly URL, @expand@ decodes it back into a long source URL.
expand :: Account   -- ^ Account to use
       -> String    -- ^ Short bit.ly URL
       -> IO Result -- ^ Either error or long source URL
expand acc url = request acc "expand" [("shortUrl", url)]
                             [ "bitly", "results", code, "longUrl" ]
  where
    ending d = foldr (\x xs -> if d `elem` xs then xs else x:xs) ""
    code = dropHeadIf (== '/') $ ending '/' url

dropHeadIf :: (a -> Bool) -> [a] -> [a]
dropHeadIf _ [] = []
dropHeadIf p all@(x:xs)
  | p x        = xs
  | otherwise  = all

-- | Internal function to accomodate all types of requests
request :: Account    -- ^ Account to use
        -> String     -- ^ Name of the API request (e.g. @shorten@ or @expand@)
        -> [(String,String)]  -- ^ Alist of the parameters specific to the request
        -> [String]   -- ^ Path to the node with the result in the XML response
        -> IO Result
request acc path params xmlpath = do
  let baseURL = server acc ++ "/" ++ path
  let params' = loginParams ++ params :: [ (String, String) ]
  let reqURL  = baseURL ++ "?" ++ urlEncodeVars params'
  let req     = getRequest reqURL :: Request String
  resp <- simpleHTTP req
  case resp of
    Left _ -> return $ Left "Network error"
    Right resp' -> do
      let Document _ _ xmlroot _ = xmlParse "" . rspBody $ resp'
      return $ errorOrResult (CElem xmlroot) xmlpath
  where
    loginParams =
             [ ("login", login acc)
             , ("apiKey", apikey acc)
             , ("format", "xml")
             , ("version", "2.0.1")
             ]

-- | Analyze XML response
errorOrResult :: Content  -- ^ XML root element
              -> [String] -- ^ Path to the node with the result
              -> Result
errorOrResult root xmlpath = do
  let cs = tag "bitly" /> tag "statusCode" /> txt $ root
  case cs of
    [] -> Left "No statusCode in response"
    (CString _ code:_) ->
          if code /= "OK"
            then
              let err = concatMap (render . content) $
                        tag "bitly" /> tag "errorMessage" /> txt $ root
              in  Left $ "Bit.ly error: " ++ err
            else
              let url' = concatMap (render . content) $
                         foldr (/>) txt (map tag xmlpath) root
              in  if null url'
                    then Left "Result not found"
                    else Right url'
    _  -> Left "Unexpected statusCode in response"