-- | Simple pasting API for CodePad.org.
module Web.Codepad where

import           Control.Applicative ((<$>))
import           Control.Monad.Trans (MonadIO,liftIO)
import           Data.Char           (isSpace)
import           Data.Monoid         (mconcat)
import qualified Network.Curl        as C
import           Network.URI         (escapeURIString)
import           Text.HTML.TagSoup   (Tag(..),parseTags)

-- | A URL.
type URL = String

-- | Code to be pasted.
type Code = String

-- | A CodePad paste id e.g. HZPquoIO.
type PasteId = String

-- | Paste output
type PasteOutput = String

-- | Alias for supported CodePad languages.
type LangName = String

-- | CodePad's domain.
codepadUrl :: URL
codepadUrl = "http://codepad.org/"

-- | A CodePad URL of a page containing a list of supported languages.
codepadLangsURL :: URL
codepadLangsURL = "http://hpaste.codepad.org/"

-- | Make a CodePad URL for the given paste id.
pasteURL :: PasteId -- ^ ID of the CodePad paste to construct a URL for. 
         -> URL     -- ^ A CodePad URL to the paste.
pasteURL pid = codepadUrl ++ pid

-- | Paste some code and get the run output too.
pasteAndRun :: MonadIO m
            => Code                             -- ^ Code to paste. 
            -> LangName                         -- ^ Language of the code. 
            -> Bool                             -- ^ Private? 
            -> m (Maybe (PasteId,PasteOutput))  -- ^ The paste id and the run output.
pasteAndRun code lang private = do
  result <- pasteCode code lang True private
  case result of
    Nothing  -> return Nothing
    Just pid -> do
      out <- pasteOutput pid
      case out of
        Nothing     -> return Nothing
        Just output -> return $ Just (pid,output)

-- | Perform a paste.
pasteCode :: MonadIO m
          => Code               -- ^ Code to paste. 
          -> LangName           -- ^ Language of the code. 
          -> Bool               -- ^ Run it?
          -> Bool               -- ^ Private?
          -> m (Maybe PasteId)  -- ^ The pasted id.
pasteCode code lang run private = do
    r <- liftIO $ C.withCurlDo $ getResponse codepadUrl [C.CurlPostFields assocs] 
    if C.respStatus r == 302
       then return $ getId <$> lookup "Location" (C.respHeaders r)
       else return Nothing
  where getResponse :: C.URLString -> [C.CurlOption]
                       -> IO (C.CurlResponse_ [(String,String)] String)
        getResponse = C.curlGetResponse_
        getId = reverse . takeWhile (/='/') . reverse
        assocs = ["code=" ++ encode code
                 ,"lang=" ++ encode lang
                 ,"run=" ++ show run
                 ,"private=" ++ show private
                 ,"submit=Submit"]
        encode = escapeURIString (const False)

-- | Get the run output for a paste id.
pasteOutput :: MonadIO m
            => PasteId               -- ^ A CodePad paste id.
            -> m (Maybe PasteOutput) -- ^ Maybe the run output of that paste.
pasteOutput pid = do
  (code,t) <- liftIO $ C.withCurlDo $ C.curlGetString_ (pasteURL pid) []
  case code of
    C.CurlOK -> return $ parseOutput t
    _        -> return Nothing

-- | Get the list of supported languages.
supportedLangs :: MonadIO m => m (Maybe [LangName])
supportedLangs = do
  (code,t) <- liftIO $ C.withCurlDo $ C.curlGetString_ codepadLangsURL []
  case code of
    C.CurlOK -> return $ Just $ parseLangs t
    _        -> return Nothing  

-- | Get the paste output.
parseOutput :: String            -- ^ Parse a paste page for the output.
            -> Maybe PasteOutput -- ^ Maybe the paste output.
parseOutput = toHeading . parseTags where
  toHeading (TagOpen "span" [("class","heading")]
             :TagText "Output:":xs) = skipLines xs
  toHeading (_:xs)                  = toHeading xs
  toHeading []                      = Nothing
  skipLines (TagClose "pre":xs) = toPre xs
  skipLines (_:xs)              = skipLines xs
  skipLines []                  = Nothing
  toPre (TagOpen "pre" _:xs) = cleanUp <$> mconcat (collect xs)
  toPre (_:xs)               = toPre xs
  toPre []                   = Nothing
  collect (TagText t:xs)     = Just t : collect xs
  collect (TagClose "pre":_) = []
  collect (_:xs)             = collect xs
  collect []                 = []
  -- Codepad adds extra space at the start and end of the pre.
  cleanUp s = dropWhile isSpace $ take (length s-1) s

-- | Extract the available languages from the CodePad page.
parseLangs :: String     -- ^ The HTML page. 
           -> [LangName] -- ^ Any parsed language names.
parseLangs = toSelect . parseTags where 
  toSelect (TagOpen "select" ((_,"lang"):_):xs) = options xs
  toSelect (_:xs)                               = toSelect xs
  toSelect []                                   = []
  options (TagOpen "option" _:TagText lang:xs) = lang : options xs
  options (_:xs)                               = options xs
  options []                                   = []