module Database.MetaHDBC.SqlExpr
    ( runStmt, prepareStmt, strict
    
    , ExprParts(..), Parameter(..), PrepareParts(..)
    , rethrowDoing, sqlInfo, makeExprParts
    
    , runStmtLHS, runStmtRHS
    
    , makePrepStmtParts, prepStmtLHS, prepStmtQ, execPrepStmtRHS, returnExecPrepStmtLHS
    
    , runStmtML, multiline
    )
where
import Database.MetaHDBC.Connection
import Database.MetaHDBC.SqlTypeIdExpQ
import Database.MetaHDBC.SimpleSqlParser
import Database.MetaHDBC.OdbcInferTypes
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Database.HDBC
import Control.Monad(when)
strict :: IO [a] -> IO [a]
strict xs = do xs' <- xs
               strictList xs'
data ExprParts = ExprParts
    { parameters     :: [Parameter]  
    , returnTypes    :: [SqlColDesc] 
    , dbSqlExpr      :: String       
    , connectionName :: Name         
    }
data Parameter = Parameter
    { parmName :: Name
    , typeID   :: SqlColDesc
    , isBound  :: Bool
    }
data PrepareParts = PrepareParts
    { exprParts :: ExprParts
    , stmtName  :: Name
    }
unboundParameters :: ExprParts -> [Parameter]
unboundParameters parts = filter (not . isBound) $ parameters parts
makeExprParts :: String -> String -> Q ExprParts
makeExprParts dsn extendedSql =
    do (varNames, sqlExpr, paramInfo, returnInfo) <- runIO $ inferTypes dsn extendedSql
       parameters' <- sequence $ zipWith3 makeParameter [0..] varNames paramInfo
       connName    <- newName "connection"
       return $ ExprParts parameters' returnInfo sqlExpr connName
    where
      makeParameter :: Int -> String -> SqlColDesc -> Q Parameter
      makeParameter pos "" typeID' = do n <- newName ("x" ++ show pos)
                                        return $ Parameter n typeID' False
      makeParameter _ xs typeID' = return $ Parameter (mkName xs) typeID' True
runStmt :: String    
        -> String    
        -> ExpQ      
                     
                     
                     
                     
                     
                     
runStmt dsn extendedSql =
    do parts <- makeExprParts dsn extendedSql
       runStmtLHS parts (runStmtRHS parts)
runStmtLHS :: ExprParts
           -> ExpQ       
           -> ExpQ
runStmtLHS parts expr = lamE (map varP (connectionName parts:sqlExprParms)) expr
    where sqlExprParms = map parmName $ unboundParameters parts
runStmtRHS :: ExprParts -> ExpQ
runStmtRHS parts =
    let sql = dbSqlExpr parts
    in if null (returnTypes parts)
          then [| run $(varE (connectionName parts)) sql $(convertParams parts) |]
          else [| do rows <- quickQuery $(varE (connectionName parts)) sql $(convertParams parts)
                     return (map $(fromRow (returnTypes parts)) rows)
                |]
prepareStmt :: String     
            -> String     
            -> ExpQ       
                          
                          
                          
                          
                          
                          
prepareStmt dsn extendedSql =
    do parts <- makePrepStmtParts dsn extendedSql
       prepStmtLHS parts [ prepStmtQ parts
                         , returnExecPrepStmtLHS parts [execPrepStmtRHS parts]
                         ]
makePrepStmtParts :: String -> String -> Q PrepareParts
makePrepStmtParts dsn extendedSql =
    do parts <- makeExprParts dsn extendedSql
       preStmtName <- newName "preStmt"
       return $ PrepareParts parts preStmtName
prepStmtLHS :: PrepareParts -> [StmtQ] -> ExpQ
prepStmtLHS (PrepareParts parts _) stmtQs =
    lam1E (varP (connectionName parts)) (doE stmtQs)
prepStmtQ :: PrepareParts
          -> StmtQ
prepStmtQ (PrepareParts parts preStmtName) =
    let sql = dbSqlExpr parts
    in bindS (varP preStmtName)
           [| prepare $(varE (connectionName parts)) sql `rethrowDoing` "calling prepare" |]
execPrepStmtRHS :: PrepareParts -> StmtQ
execPrepStmtRHS (PrepareParts parts preStmtName) =
    let sql = dbSqlExpr parts
        expr = if null (returnTypes parts)
                  then [| execute $(varE preStmtName) $(convertParams parts) |]
                  else [| do rows <- fetchRows $(varE preStmtName) $(convertParams parts)
                             return (map $(fromRow (returnTypes parts)) rows)
                        |]
    in noBindS expr
returnExecPrepStmtLHS :: PrepareParts -> [StmtQ] -> StmtQ
returnExecPrepStmtLHS (PrepareParts parts _) statements =
    noBindS $ appE [|return|] (lamE pattern (doE statements))
        where pattern = map (varP . parmName) $ unboundParameters parts
convertParams :: ExprParts
           -> ExpQ
convertParams parts = listE $ map convertParam (parameters parts)
    where convertParam p = appE (toSqlColDesc $ typeID p) (varE $ parmName p)
sqlInfo :: String -> String -> IO String
sqlInfo dsn extendedSql =
    do (varNames, sqlExpr, paramInfo, columnInfo) <- inferTypes dsn extendedSql
       let varsString       = show $ zip varNames paramInfo
           columnInfoString = show columnInfo
       return ("Extended sql: " ++ extendedSql        ++ "\n" ++
               "Parsed sql:   " ++ sqlExpr            ++ "\n" ++
               "Variables:    " ++ varsString         ++ "\n" ++
               "Column info:  " ++ columnInfoString ++ "\n"
              )
fetchRows :: Statement -> [SqlValue] -> IO [[SqlValue]]
fetchRows preStmt params =
    do _ <- execute preStmt params  `rethrowDoing` "executing prepared statement"
       fetchAllRows preStmt         `rethrowDoing` "fetch all rows"
rethrowDoing :: IO a -> String -> IO a
rethrowDoing command doing =
    command `catchSql` (\e -> fail ("Exception when trying \"" ++ doing ++
                                    "\" : " ++ seErrorMsg e))
inferTypes :: String -> String
           -> IO ([String], String, [SqlColDesc], [SqlColDesc])
inferTypes dsn extendedSql =
    do let (varNames, sqlExpr) = simpleSqlParser extendedSql
       (paramInfo, columnInfo) <- dbInferTypes dsn sqlExpr
       when (length varNames /= length paramInfo)
                (fail "Database server and MetaHDBC disagrees about number of placeholder arguments")
       return (varNames, sqlExpr, paramInfo, columnInfo)
fromRow :: [SqlColDesc] -> ExpQ
fromRow xs =
    do es <- mapM fromSqlColDesc xs
       
       names <- mapM (\i -> newName ("p" ++ show i)) [0..(length es  1)]
       return $ LamE [ListP (map VarP names)] (TupE $ map (\(e, n) -> AppE e (VarE n)) $ zip es names)
runStmtML :: String -> QuasiQuoter
runStmtML conn = QuasiQuoter
    { quoteExp  = \s -> runStmt conn s
    , quotePat  = error "Not implemented"
    , quoteType = error "Not implemented"
    , quoteDec  = error "Not implemented"
    }
multiline :: QuasiQuoter
multiline = QuasiQuoter
    { quoteExp  = litE . stringL
    , quotePat  = error "Not implemented"
    , quoteType = error "Not implemented"
    , quoteDec  = error "Not implemented"
    }
cachingStmt :: String 
            -> String 
            -> ExpQ   
                      
                      
                      
                      
                      
                      
cachingStmt dsn extendedSql =
    do (conn, params, prepareStmtQ, executeExpQ) <- prepareParts' dsn extendedSql
       lamE (map varP (conn:params)) (doE [prepareStmtQ, executeExpQ])
prepareParts' :: String
              -> String
              -> Q (Name, [Name], StmtQ, StmtQ)
prepareParts' dsn extendedSql =
    do (varNames, sqlExpr, paramInfo, columnInfo) <- runIO $ inferTypes dsn extendedSql
       (parmNames, parmExpr) <- fromParams' (zip varNames paramInfo)
       connName    <- newName "connection"
       preStmtName <- newName "preStmt"
       let prepareExpQ =
               bindS (varP preStmtName) [| cachingPrepare $(varE connName) sqlExpr
                                                `rethrowDoing` "calling cachingPrepare" |]
           executeExpQ = noBindS $
               [| do rows <- fetchRows $(varE preStmtName) $( parmExpr )
                     $( if null columnInfo
                           then [| return () |]
                           else [| return $ map ( $(fromRow columnInfo) ) rows |]
                      )
                |]
       return (connName, parmNames, prepareExpQ, executeExpQ)
fromParams' :: [(String, SqlColDesc)]   
            -> Q ([Name], ExpQ)
fromParams' xs =
    do toFuns <- mapM toSqlColDesc (map snd xs)
       let newNameOrBoundVar ("", i) = do n <- newName ("x" ++ show i)
                                          return (n, True)
           newNameOrBoundVar (ys, _) = return (mkName ys, False)
       names <- mapM newNameOrBoundVar $ zip (map fst xs) [0::Int ..]
       
       return ( map fst $ filter snd names
              , listE $ map (\(n, f) -> appE f (varE n)) (zip (map fst names) (map return toFuns)))