{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE NamedFieldPuns #-} 


-- | Common functionality for uploading and downloading data
--   from Google FusionTables.  


module HSBencher.Internal.Fusion
      ( initialize
      , FusionConfig(..)
        -- Experiments
      , getSomething
        -- replaces getSomething
      , getWithSQLQuery
      , init
      , ColData(..) -- export from hgdata
      , FTValue(..) -- export from hgdata 
      )
       where


-- Google API
import Network.Google.OAuth2 
import Network.Google.FusionTables hiding (createTable)
import qualified Network.Google.FusionTables as FT

-- Network
import Network.HTTP.Conduit (HttpException)

-- Control
import Control.Monad.Reader
import Control.Concurrent (threadDelay)
import qualified Control.Exception as E 

-- Date and Time
import Data.Time.Format () 

-- Data Structures
import qualified Data.ByteString.Char8 as B
import Data.Dynamic 
import Data.Default (Default(def))

-- Print to stderr
import System.IO (hPutStrLn, stderr)

-- Prelude
import Prelude hiding (init) 

-- TEMPORARY
import System.Environment (getEnvironment)
import System.IO.Unsafe (unsafePerformIO)

---------------------------------------------------------------------------
-- Exception
data FusionException = FusionException String
                     | TableNotFoundException
                     | FailedCreateTableException
                     | MoreThanOneTableFoundException 
                     deriving (Show, Typeable)

instance E.Exception FusionException 
 


---------------------------------------------------------------------------
--
fusionTag :: String -> String
fusionTag str = "[HSBencher.Internal.Fusion] " ++ str


---------------------------------------------------------------------------
-- Initialization and Authorization

initialize :: String -> String -> String -> IO (TableId, [String])
initialize cid sec table_name = do
  hPutStrLn stderr $ fusionTag "Initializing"
  let auth = OAuth2Client { clientId=cid, clientSecret=sec }
  table_id <- getTableId auth table_name
  cols     <- getTableColumns auth table_id 
  hPutStrLn stderr $ fusionTag (table_id ++ " " ++ show cols)
  return (table_id,cols)

-- ////  experimenting  Needs to be updated! 
 
init :: String -> String -> String -> IO (TableId, OAuth2Client)
init cid sec table_name = do
  hPutStrLn stderr $ fusionTag "Initializing"
  let auth = OAuth2Client { clientId=cid, clientSecret=sec }
  table_id <- getTableId auth table_name
  cols     <- getTableColumns auth table_id 
  hPutStrLn stderr $ fusionTag (table_id ++ " " ++ show cols)
  return (table_id, auth)


getSomething :: OAuth2Client -> String -> String -> Maybe String -> IO ColData
getSomething auth table_id col_name cond = do
  tokens <- getCachedTokens auth
  let atok = B.pack $ accessToken tokens
  tableSelect atok table_id col_name cond


getWithSQLQuery :: OAuth2Client -> String -> String -> IO ColData
getWithSQLQuery auth table_id query = do
  tokens <- getCachedTokens auth
  let atok = B.pack $ accessToken tokens
  tableSQLQuery atok table_id query



-- \\\\

-- | Obtain the id of a FusionTable identified by name.
getTableId :: OAuth2Client 
           -> String  -- ^ Human-readable name of the fusion table
           -> IO TableId
getTableId auth table_name = do
  hPutStrLn stderr $ fusionTag "Fetching information from Google"

  tokens <- getCachedTokens auth

  -- hPutStrLn stderr $ fusionTag $ "retrieved tokens: " ++ show tokens

  let atok = B.pack $ accessToken tokens

  -- error check here
  Just allTables <- stdRetry "listTables" auth tokens $ listTables atok 

  -- hPutStrLn stderr $ fusionTag $ "Found " ++ show (length allTables) ++ " tables."

  case filter (\t -> tab_name t == table_name) allTables of
    [] -> E.throwIO TableNotFoundException
          -- Replace with an exception and let user of this library handle that
    [t] -> do
      let table_id = tab_tableId t
      return table_id
    _ -> E.throwIO MoreThanOneTableFoundException 

-- | Obtain the column headings from a FusionTable 
getTableColumns :: OAuth2Client -> TableId -> IO [String]
getTableColumns auth table_id = do
  tokens <- getCachedTokens auth
  let atok = B.pack $ accessToken tokens

  columns <- fmap (map col_name) $ listColumns atok table_id
  return columns 
  

-- | Create a FusionTable with a column schema
_createTable :: OAuth2Client -> String -> [(String,CellType)] -> IO TableId
_createTable auth table_name schema = do
  tokens <- getCachedTokens auth
  let atok = B.pack $ accessToken tokens 
  result  <- stdRetry "createTable" auth tokens $
             FT.createTable atok table_name schema
  case result of
    Just TableMetadata{tab_tableId} -> return tab_tableId
    Nothing -> E.throwIO FailedCreateTableException



---------------------------------------------------------------------------
-- Internal: HTTP requests retry behaviour
    
stdRetry :: String -> OAuth2Client -> OAuth2Tokens -> IO a ->
            IO (Maybe a)
stdRetry _msg client toks action = do
  let retryHook _num _exn = do
        -- datetime <- getDateTime
        _ <- stdRetry "refresh tokens" client toks (refreshTokens client toks)
        return ()
        
  retryIORequest action retryHook $
          [1,2,4,4,4,4,4,4,8,16] --- 32,64,
          ++ replicate 30 5
          ++ replicate 10 10 -- Adding this.  Still failing a few on Google API's [2014.10.15]
  
  
retryIORequest :: IO a -> (Int -> HttpException -> IO ()) -> [Double] -> IO (Maybe a)
retryIORequest req retryHook times = loop 0 times
  where
    loop _ [] = return Nothing
    loop !num (delay:tl) = 
      E.catch (fmap Just req) $ \ (exn::HttpException) -> do 
        retryHook num exn
        threadDelay (round$ delay * 1000 * 1000) -- Microseconds
        loop (num+1) tl


---------------------------------------------------------------------------
-- Upload



---------------------------------------------------------------------------
-- Download 





---------------------------------------------------------------------------
-- Data   

-- | Configuration options for Google Fusion Table uploading.
data FusionConfig = 
  FusionConfig
  { fusionTableID  :: Maybe TableId -- ^ This must be Just whenever doFusionUpload is true.
  , fusionClientID :: Maybe String
  , fusionClientSecret :: Maybe String
  , serverColumns  :: [String] -- ^ Record the ordering of columns server side.
  }
  deriving (Show,Read,Ord,Eq, Typeable)

instance Default FusionConfig where
 def = FusionConfig 
    { fusionTableID  = Nothing 
    , fusionClientID     = lookup "HSBENCHER_GOOGLE_CLIENTID" theEnv
    , fusionClientSecret = lookup "HSBENCHER_GOOGLE_CLIENTSECRET" theEnv
    , serverColumns      = []
    }

theEnv :: [(String,String)] 
theEnv = unsafePerformIO getEnvironment