{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

module Python(
    module Str,
    defO,
    defV,
    defOO,
    defOV,
    defVO,
    defVV,
    defOOO,
    defOOV,
    defOVO,
    defOVV,
    defVOO,
    defVOV,
    defVVO,
    defVVV,
    defOOOO,
    PyObject,
    )
where

import Str
import Data.Digest.Pure.MD5 (md5)
import Foreign.C
import Foreign (FunPtr, ForeignPtr)
import Foreign.Ptr (Ptr, nullPtr)
import Foreign.ForeignPtr (newForeignPtr, withForeignPtr)
import Data.Aeson (FromJSON, ToJSON, toJSON, encode, decode)
import Data.ByteString.Lazy.Char8 (unpack, pack)
import Data.IORef (IORef, newIORef, writeIORef, readIORef)
import System.IO.Unsafe (unsafePerformIO)
import qualified Data.Map as Map
import Data.Maybe (fromJust)

modules :: IORef (Map.Map String RawPyObject)
modules = unsafePerformIO $ newIORef (Map.empty)

data P
type RawPyObject = (Ptr P)
data PyObject a = PyObject (ForeignPtr P) deriving Show
    

foreign import ccall "getObject" c_getObject:: CString -> IO (RawPyObject)
foreign import ccall "getObjectInModule" c_getObjectInModule :: CString -> CString -> IO (RawPyObject)
foreign import ccall "execInModule" c_execInModule :: CString -> CString -> IO ()
foreign import ccall "PyRun_SimpleString" pyRun_SimpleString :: CString -> IO ()
foreign import ccall "Py_DecRef" py_DecRef :: RawPyObject -> IO ()
foreign import ccall "Py_Initialize" py_initialize :: IO ()
foreign import ccall "Py_Finalize" finalize :: IO ()
foreign import ccall "Py_BuildValue" py_BuildValueString :: CString -> CString -> IO (RawPyObject)
foreign import ccall "Py_BuildValue" py_BuildValueString2 :: CString -> CString -> CString -> IO (RawPyObject)
foreign import ccall "Py_BuildValue" py_BuildValueObject :: CString -> RawPyObject -> IO (RawPyObject)
foreign import ccall "Py_BuildValue" py_BuildValueObject2 :: CString -> RawPyObject -> RawPyObject -> IO (RawPyObject)
foreign import ccall "Py_BuildValue" py_BuildValueObject3 :: CString -> RawPyObject -> RawPyObject -> RawPyObject -> IO (RawPyObject)
foreign import ccall "PyObject_CallObject" pyObject_CallObject :: RawPyObject -> RawPyObject -> IO (RawPyObject)
foreign import ccall "PyString_AsString" pyString_AsString :: RawPyObject -> IO CString
foreign import ccall unsafe "gimmeFunc" gimmeFunc :: CInt -> IO (FunPtr (RawPyObject -> IO ()))

exec :: String -> IO ()
exec s = withCString s pyRun_SimpleString

withCString2 :: String -> String -> (CString -> CString -> IO a) -> IO a
withCString2 s1 s2 f = withCString s1 f' where
    f' cs1 = withCString s2 (f cs1)

execInModule :: String -> String -> IO ()
execInModule moduleName payload = 
    withCString2 payload moduleName c_execInModule

initialize :: IO ()
initialize = do
  py_initialize

getObjectInModule :: String -> String -> IO (RawPyObject)
getObjectInModule moduleName objectName = 
    withCString2 objectName moduleName c_getObjectInModule

getObject :: String -> IO (RawPyObject)
getObject s = withCString s c_getObject

jsonfunc :: String
jsonfunc = [str|
def jsonfunc(argformats):
    def wrapper(f):
        import simplejson as json
        import traceback
        def new_f(*args):
            new_args = []
            for (x, format) in zip(args, argformats):
                if format == 'J':
                    try:
                        new_x = json.loads(x)
                    except:
                        print('error processing %s' % x)  
                    new_args.append(new_x)
                else:
                    new_args.append(x)
            try:
                result = f(*new_args)
                if argformats[-1] == 'J':
                    result = json.dumps(result)
            except Exception as ex:
                print(traceback.format_exc())
                result = None
                if argformats[-1] == 'J':
                    result = json.dumps(result)
            return result
        return new_f
    return wrapper
|]

hash :: String -> String
hash contents = show . md5 $ pack contents

mydecode :: (FromJSON a) => String -> Maybe a
mydecode s = do
    x <- (decode . pack . (\x -> "[" ++ x ++ "]") ) s
    return $ head x


toPyObject :: (ToJSON a) => a -> IO (PyObject b)
toPyObject x = do
    y <- return (unpack . encode $ x)
    p <- withCString "s" (\cs -> 
           withCString y (\cy -> 
             py_BuildValueString cs cy) )
    newForeignPyPtr p

fromPyObject :: (FromJSON a) => PyObject b -> IO a
fromPyObject (PyObject fr) = do
    r2 <- withForeignPtr fr $ \r -> peekCString =<< pyString_AsString r
    return $ fromJust $ mydecode r2

getFunc :: String -> IO RawPyObject
getFunc s = do
    currentModules <- readIORef $ modules
    key <- return $ hash s
    if Map.member "initialized" currentModules then
      return ()
    else initialize
    case Map.lookup key currentModules of
      Just p -> return p
      Nothing -> do 
        execInModule key jsonfunc
        execInModule key s
        execInModule key "export = jsonfunc('JJ')(export)"
        f' <- getObjectInModule key "export"
        writeIORef modules (Map.insert (hash s) f' currentModules)
        return f'

newForeignPyPtr :: RawPyObject -> IO (PyObject b)
newForeignPyPtr r = do
    finalizer <- gimmeFunc 0
    return . PyObject =<< newForeignPtr finalizer r

defO :: String -> IO (PyObject b)
defO s = do
    f <- getFunc s
    r <- pyObject_CallObject f nullPtr
    newForeignPyPtr r

defOO :: String -> (PyObject a) -> IO (PyObject b)
defOO s (PyObject fx1) = do
    f <- getFunc s
    p1 <- withForeignPtr fx1 $ \x1 -> withCString "(O)" (\cs -> py_BuildValueObject cs x1) 
    (PyObject fp) <- newForeignPyPtr p1
    r <- withForeignPtr fp $ \p -> pyObject_CallObject f p
    newForeignPyPtr r

defOOO :: String -> (PyObject a1) -> (PyObject a2) -> IO (PyObject b)
defOOO s (PyObject fx1) (PyObject fx2) = do
    f <- getFunc s
    p1 <- withForeignPtr fx1 $ \x1 -> (
        withForeignPtr fx2 $ \x2 -> (
            withCString "(OO)" (\cs -> py_BuildValueObject2 cs x1 x2) 
        ))
    (PyObject fp) <- newForeignPyPtr p1
    r <- withForeignPtr fp $ \p -> pyObject_CallObject f p
    newForeignPyPtr r

defOOOO :: String -> (PyObject a1) -> (PyObject a2) -> (PyObject a3) -> IO (PyObject b)
defOOOO s (PyObject fx1) (PyObject fx2) (PyObject fx3) = do
    f <- getFunc s
    p1 <- withForeignPtr fx1 $ \x1 -> (
        withForeignPtr fx2 $ \x2 -> (
            withForeignPtr fx3 $ \x3 -> (
                withCString "(OOO)" (\cs -> py_BuildValueObject3 cs x1 x2 x3) 
        )))
    (PyObject fp) <- newForeignPyPtr p1
    r <- withForeignPtr fp $ \p -> pyObject_CallObject f p
    newForeignPyPtr r

defV :: (FromJSON b) => String -> IO b
defV s = do
    r <- defO s
    b <- fromPyObject r
    return b

defOV :: (FromJSON b) => String -> (PyObject a1) -> IO b
defOV s x1 = do
    r <- defOO s x1
    b <- fromPyObject r
    return b

defVO :: (ToJSON a1) => String -> a1 -> IO (PyObject b)
defVO s input1 = do
    x1 <- toPyObject input1
    r <- defOO s x1
    return r

defVV :: (ToJSON a1, FromJSON b) => String -> a1 -> IO b
defVV s input1 = do
    x1 <- toPyObject input1
    fr <- defOO s x1
    b <- fromPyObject fr
    return b

defOOV :: (FromJSON b) => String -> (PyObject a1) -> (PyObject a2) -> IO b
defOOV s x1 x2 = do
    fr <- defOOO s x1 x2
    b <- fromPyObject fr
    return b

defOVO :: (ToJSON a2) => String -> (PyObject a1) -> a2 -> IO (PyObject b)
defOVO s x1 input2 = do
    x2 <- toPyObject input2
    fr <- defOOO s x1 x2
    return fr

defOVV :: (ToJSON a2, FromJSON b) => String -> (PyObject a1) -> a2 -> IO b
defOVV s x1 input2 = do
    x2 <- toPyObject input2
    fr <- defOOO s x1 x2
    b <- fromPyObject fr
    return b

defVOO :: (ToJSON a1) => String -> a1 -> (PyObject a2) -> IO (PyObject b)
defVOO s input1 x2 = do
    x1 <- toPyObject input1
    fr <- defOOO s x1 x2
    return fr

defVOV :: (ToJSON a1, FromJSON b) => String -> a1 -> (PyObject a2) -> IO b
defVOV s input1 x2 = do
    x1 <- toPyObject input1
    fr <- defOOO s x1 x2
    b <- fromPyObject fr
    return b

defVVO :: (ToJSON a1, ToJSON a2) => String -> a1 -> a2 -> IO (PyObject b)
defVVO s input1 input2 = do
    x1 <- toPyObject input1
    x2 <- toPyObject input2
    fr <- defOOO s x1 x2
    return fr

defVVV :: (ToJSON a1, ToJSON a2, FromJSON b) => String -> a1 -> a2 -> IO b
defVVV s input1 input2 = do
    x1 <- toPyObject input1
    x2 <- toPyObject input2
    fr <- defOOO s x1 x2
    b <- fromPyObject fr
    return b
