--
-- Module : HSClient
-- Author : Wu Xingbo
-- Copyright (c) 2011 Wu Xingbo (wuxb45@gmail.com)
-- New BSD License
--
-- |
-- Haskell implementation of a HandlerSocket client (API).
-- HandlerSocket is a MySQL plugin with better performance then SQL.
-- HandlerSocket site: https://github.com/ahiguti/HandlerSocket-Plugin-for-MySQL
--


module Database.HandlerSocketClient (
  -- * Basic Type
  HandSock (..),
  -- * Connection
  connectTo,
  closeHS,
  -- * Do Generic Request
  runReqNoRespData,
  runReqGeneric,
  -- * Do Special Request
  runReqOpen,
  runReqFind,
  runReqFindN,
  runReqInsert,
  runReqUpdate,
  runReqDelete
  ) where
import Data.Word
import Data.Char
import Data.List
import Network.Socket hiding (send,sendTo,recv,recvFrom)
import Network.Socket.ByteString
import System.IO
import qualified Data.ByteString as B

-- | basic data structure for one Socket and one Handle (or Index).
data HandSock = HandSock {getSocket :: Socket, getHost :: String, getPort :: String}

recvOne :: Socket -> Int -> IO B.ByteString
recvOne sock maxlen = do
  s <- recvOneR sock maxlen
  if B.elem 10 s
    then return s
    else return B.empty
  
recvOneR :: Socket -> Int -> IO B.ByteString
recvOneR sock maxlen = do
  s1 <- recv sock maxlen
  if B.null s1
    then return s1
    else
      if B.elem 10 s1
        then return s1
        else fmap (B.append s1) $ recvOneR sock maxlen
recvN :: Socket -> Int -> Int -> IO [B.ByteString]
recvN sock maxlen count = do
  all <- recvNR sock maxlen count
  return $ init $ B.split 10 all

recvNR :: Socket -> Int -> Int -> IO B.ByteString
recvNR sock maxlen todo
  | todo <= 0 = return B.empty
  | otherwise = do
      s1 <- recv sock maxlen
      if B.null s1
        then return s1
        else fmap (B.append s1) $ recvNR sock maxlen $ todo - (B.count 10 s1)

escapeW :: [Word8] -> [Word8]
escapeW [] = []
escapeW (x:xs) = if x < 16 then 1:(0x40+x):(escapeW xs) else x:(escapeW xs)

escape :: String -> [Word8]
escape str = escapeW $ map (fromIntegral . ord) str

unescapeW :: [Word8] -> [Word8]
unescapeW [] = []
unescapeW (x:xs) = 
  if x == 1 
  then case xs of
    (y:ys) -> (y-0x40):(unescapeW ys)
    _ -> [] -- error
  else x:(unescapeW xs)

unescape :: [Word8] -> String
unescape xs = map (chr . fromIntegral) $ unescapeW xs

decodeResp :: B.ByteString -> [String]
decodeResp = map unescape . mysplit . B.unpack
  where
    isspace' = (\x -> (x == 9) || (x == 10)) -- \t \n
    mysplit s = case dropWhile isspace' s of -- drop leading space'
      [] -> []
      (10:_) -> []
      s1 -> w : (mysplit s2) -- first : rest.
            where
              (w,s2) = break isspace' s1

makeReq :: [String] -> B.ByteString
makeReq = B.pack . (++ [10]) . intercalate [9] . map escape

makeReqOpen :: String -> String -> String -> String -> B.ByteString
makeReqOpen db tb ind cs = makeReq ["P","0",db,tb,ind,cs]

makeReqFind :: (Integral a) => String -> String -> a -> B.ByteString
makeReqFind op v lim = makeReq ["0",op,"1",v,show $ toInteger lim,"0"]

makeReqInsert :: [String] -> B.ByteString
makeReqInsert vs = makeReq $ ["0","+",show (length vs)] ++ vs

makeReqUpdate :: String -> [String] -> B.ByteString
makeReqUpdate key newvs = makeReq $ ["0","=","1",key,"1","0","U"] ++ newvs

makeReqDelete :: String -> B.ByteString
makeReqDelete key = makeReq ["0","=","1",key,"1","0","D"]

-- | Connect to specific address & port.
-- Returns the 'HandSock' for later request or close.
connectTo :: String -> String -> IO HandSock
connectTo host port = do
  addrinfos <- getAddrInfo Nothing (Just host) (Just port)
  let addr = head addrinfos
  let addr' = addr {addrFamily = AF_INET, addrSocketType = Stream, addrProtocol = 6}
  sock <- socket AF_INET Stream 6
  connect sock (addrAddress addr')
  return $ HandSock sock host port

runReqBool :: HandSock -> B.ByteString -> IO Bool
runReqBool hs req = do
  sendAll (getSocket hs) req
  resp <- recvOne (getSocket hs) 1024
  if B.null resp
    then return False
    else return $ 48 == (B.head resp)

-- | Do Generic Request with given Strings. Return True or False, discarding all other messages.
runReqNoRespData :: HandSock -> [String] -> IO Bool
runReqNoRespData hs vs = runReqBool hs $ makeReq vs

-- | Do Generic Request, Returns all values received, \"0\" in the head means SUCCESS.
runReqGeneric :: HandSock -> [String] -> IO [String]
runReqGeneric hs vs = do
  let req = makeReq vs
  sendAll (getSocket hs) req
  fmap decodeResp $ recvOne (getSocket hs) 4096

-- | Do \'open_index\' request.
runReqOpen :: HandSock -> String -> String -> String -> String -> IO Bool
runReqOpen hs db tb ind cs = runReqBool hs $ makeReqOpen db tb ind cs

mygroup :: Int -> [a] -> [[a]]
mygroup _ [] = []
mygroup i xs = l:(mygroup i r) 
  where (l,r) = splitAt i xs

splitFindResult :: [String] -> [[String]]
splitFindResult ss =
  case ss of
    ("0":i:xs) -> mygroup (read i) xs
    _ -> []
    
-- | Do \'find\' request. Returns at most lim results.
runReqFind :: (Integral a) => HandSock -> String -> String -> a -> IO [[String]]
runReqFind hs op v lim = do
  let req = makeReqFind op v lim
  sendAll (getSocket hs) req
  fmap (splitFindResult . decodeResp) $ recvOne (getSocket hs) 4096

runReqFindN :: (Integral a) => HandSock -> String -> [String] -> a -> IO [[[String]]]
runReqFindN hs op vs lim = do
  mapM_ (\v -> let req = makeReqFind op v lim in sendAll (getSocket hs) req) vs
  map (splitFindResult . decodeResp) `fmap` recvN (getSocket hs) 4096 (length vs)
  
-- | Do \'insert\' request, insert one record at a time.
runReqInsert :: HandSock -> [String] -> IO Bool
runReqInsert hs vs = runReqBool hs $ makeReqInsert vs

-- | Do \'update\' request, give one value to be compared with the KEY, then provide the new data.
-- Only ONE record will be updated.
runReqUpdate :: HandSock -> String -> [String] -> IO Bool
runReqUpdate hs v0 vs = runReqBool hs $ makeReqUpdate v0 vs

-- | Do \'delete\' request, give one value to be compared with the KEY. 
-- Only ONE record will be deleted.
runReqDelete :: HandSock -> String -> IO Bool
runReqDelete hs key = runReqBool hs $ makeReqDelete key

-- | Close the connection.
closeHS :: HandSock -> IO ()
closeHS hs = sClose (getSocket hs)

reConnect :: HandSock -> IO HandSock
reConnect hs = do
  closeHS hs
  connectTo (getHost hs) (getPort hs)