{-# LANGUAGE
    ScopedTypeVariables,
    TypeFamilies,
    FlexibleContexts,
    FlexibleInstances
  #-}

-- | Facilities for defining new object types which can be marshalled between
-- Haskell and QML.
module Graphics.QML.Objects (
  -- * Class Definition
  Object (
    classDef),
  ClassDef,
  Member,
  defClass,

  -- * Methods
  defMethod,
  MethodSuffix,

  -- * Properties
  defPropertyRO,
  defPropertyRW,

  -- * Signals
  defSignal,
  fireSignal,
  SignalKey (
    type SignalParams),
  SignalSuffix,

  -- * Object References
  ObjRef,
  newObject,
  fromObjRef,

  -- * Dynamic Object References
  AnyObjRef,
  anyObjRef,
  fromAnyObjRef,

  -- * Customer Marshallers
  objSimpleMarshaller,
  objBidiMarshaller
) where

import Graphics.QML.Internal.BindCore
import Graphics.QML.Internal.BindObj
import Graphics.QML.Internal.JobQueue
import Graphics.QML.Internal.Marshal
import Graphics.QML.Internal.MetaObj
import Graphics.QML.Internal.Objects

import Control.Concurrent.MVar
import Control.Monad.Trans.Maybe
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe
import Data.Tagged
import Data.Typeable
import Data.IORef
import Foreign.Ptr
import Foreign.Storable
import Foreign.Marshal.Alloc
import Foreign.Marshal.Array
import System.IO.Unsafe
import Numeric

--
-- ObjRef
--

-- | Represents an instance of the QML class which wraps the type @tt@.
data ObjRef tt = ObjRef {
  objHndl :: HsQMLObjectHandle
}

instance (Object tt) => Marshal (ObjRef tt) where
  type MarshalMode (ObjRef tt) = ValObjBidi tt
  marshaller = MValObjBidi {
    mValObjBidi_typeName = Tagged $ TypeName "QObject*",
    mValObjBidi_valToHs = \ptr -> do
      any <- mValToHs ptr
      MaybeT $ return $ fromAnyObjRef any,
    mValObjBidi_hsToVal = \obj ptr ->
      mHsToVal (AnyObjRef $ objHndl obj) ptr,
    mValObjBidi_hsToAlloc = \obj f ->
      mHsToAlloc (AnyObjRef $ objHndl obj) f,
    mValObjBidi_objToHs = \hndl ->
      return $ ObjRef hndl,
    mValObjBidi_hsToObj = \obj ->
      return $ objHndl obj}

-- | Creates an instance of a QML class given a value of the underlying Haskell 
-- type @tt@.
newObject :: forall tt. (Object tt) => tt -> IO (ObjRef tt)
newObject obj = do
  cRec <- getClassRec (classDef :: ClassDef tt)
  oHndl <- hsqmlCreateObject obj $ crecHndl cRec
  return $ ObjRef oHndl

-- | Returns the associated value of the underlying Haskell type @tt@ from an
-- instance of the QML class which wraps it.
fromObjRef :: ObjRef tt -> tt
fromObjRef =
    unsafePerformIO . fromObjRefIO

fromObjRefIO :: ObjRef tt -> IO tt
fromObjRefIO =
    hsqmlObjectGetHaskell . objHndl 

-- | Represents an instance of a QML class which wraps an arbitrary Haskell
-- type. Unlike 'ObjRef', an 'AnyObjRef' only carries the type of its Haskell
-- value dynamically and does not encode it into the static type.
data AnyObjRef = AnyObjRef {
  anyObjHndl :: HsQMLObjectHandle
}

instance Marshal AnyObjRef where
  type MarshalMode AnyObjRef = ValObjBidi ()
  marshaller = MValObjBidi {
    mValObjBidi_typeName = Tagged $ TypeName "QObject*",
    mValObjBidi_valToHs = \ptr -> MaybeT $ do
      objPtr <- peek (castPtr ptr)
      hndl <- hsqmlGetObjectHandle objPtr
      return $ if isNullObjectHandle hndl
        then Nothing else Just $ AnyObjRef hndl,
    mValObjBidi_hsToVal = \obj ptr -> do
      objPtr <- hsqmlObjectGetPointer $ anyObjHndl obj
      poke (castPtr ptr) objPtr,
    mValObjBidi_hsToAlloc = \obj f ->
      alloca $ \(ptr :: Ptr (Ptr ())) ->
        flip mHsToVal (castPtr ptr) obj >> f (castPtr ptr),
    mValObjBidi_objToHs = \hndl ->
      return $ AnyObjRef hndl,
    mValObjBidi_hsToObj = \obj ->
      return $ anyObjHndl obj}

-- | Upcasts an 'ObjRef' into an 'AnyObjRef'.
anyObjRef :: ObjRef tt -> AnyObjRef
anyObjRef (ObjRef hndl) = AnyObjRef hndl

-- | Attempts to downcast an 'AnyObjRef' into an 'ObjRef' with the specific
-- underlying Haskell type @tt@.
fromAnyObjRef :: (Object tt) => AnyObjRef -> Maybe (ObjRef tt)
fromAnyObjRef = unsafePerformIO . fromAnyObjRefIO

fromAnyObjRefIO :: forall tt. (Object tt) => AnyObjRef -> IO (Maybe (ObjRef tt))
fromAnyObjRefIO (AnyObjRef hndl) = do
    let srcRep = typeOf (undefined :: tt)
    dstRep <- hsqmlObjectGetHsTyperep hndl
    if srcRep == dstRep
        then return $ Just $ ObjRef hndl
        else return Nothing

-- | Provides a QML-to-Haskell 'Marshaller' which allows you to define
-- instances of 'Marshal' for custom 'Object' types. This allows a custom types
-- to be passed into Haskell code as method parameters without having to
-- manually deal with 'ObjRef's.
--
-- For example, an instance for 'MyObjectType' would be defined as follows:
--
-- @
-- instance Marshal MyObjectType where
--     type MarshalMode MyObjectType = ValObjToOnly MyObjectType
--     marshaller = objSimpleMarshaller
-- @
objSimpleMarshaller ::
  forall obj. (Object obj) => Marshaller obj (ValObjToOnly obj)
objSimpleMarshaller = MValObjToOnly {
  mValObjToOnly_typeName = retag (mTypeName :: Tagged (ObjRef obj) TypeName),
  mValObjToOnly_valToHs = \ptr -> (errIO . fromObjRefIO) =<< mValToHs ptr,
  mValObjToOnly_objToHs = hsqmlObjectGetHaskell}

-- | Provides a bidirectional QML-to-Haskell and Haskell-to-QML 'Marshaller'
-- which allows you to define instances of 'Marshal' for custom object types.
-- This allows a custom type to be passed in and out of Haskell code via
-- methods, properties, and signals, without having to manually deal with
-- 'ObjRef's. Unlike the simple marshaller, this one must be given a function
-- which specifies how to obtain an 'ObjRef' given a Haskell value.
--
-- For example, an instance for 'MyObjectType' which simply creates a new
-- object whenever one is required would be defined as follows:
--
-- @
-- instance Marshal MyObjectType where
--     type MarshalMode MyObjectType = ValObjBidi MyObjectType
--     marshaller = objBidiMarshaller newObject
-- @
objBidiMarshaller ::
  forall obj. (Object obj) =>
  (obj -> IO (ObjRef obj)) -> Marshaller obj (ValObjBidi obj) 
objBidiMarshaller newFn = MValObjBidi {
  mValObjBidi_typeName = retag (mTypeName :: Tagged (ObjRef obj) TypeName),
  mValObjBidi_valToHs = \ptr -> (errIO . fromObjRefIO) =<< mValToHs ptr,
  mValObjBidi_hsToVal = \obj ptr -> flip mHsToVal ptr =<< newFn obj,
  mValObjBidi_hsToAlloc = \obj f -> flip mHsToAlloc f =<< newFn obj,
  mValObjBidi_objToHs = hsqmlObjectGetHaskell,
  mValObjBidi_hsToObj = fmap objHndl . newFn}

--
-- ClassDef
--

-- | Generates a 'ClassDef' from a list of 'Member's.
defClass :: forall tt. (Object tt) => [Member tt] -> ClassDef tt
defClass = ClassDef

data MemoStore k v = MemoStore (MVar (Map k v)) (IORef (Map k v))

newMemoStore :: IO (MemoStore k v)
newMemoStore = do
    let m = Map.empty
    mr <- newMVar m
    ir <- newIORef m
    return $ MemoStore mr ir

getFromMemoStore :: (Ord k) => MemoStore k v -> k -> IO v -> IO (Bool, v)
getFromMemoStore (MemoStore mr ir) key fn = do
    fstMap <- readIORef ir
    case Map.lookup key fstMap of
        Just val -> return (False, val)
        Nothing  -> modifyMVar mr $ \sndMap -> do
            case Map.lookup key sndMap of
                Just val -> return (sndMap, (False, val))
                Nothing  -> do
                    val <- fn
                    let newMap = Map.insert key val sndMap
                    writeIORef ir newMap
                    return (newMap, (True, val))

data ClassRec = ClassRec {
    crecHndl :: HsQMLClassHandle,
    crecSigs :: Map TypeRep Int
}

{-# NOINLINE classRecDb #-}
classRecDb :: MemoStore TypeRep ClassRec
classRecDb = unsafePerformIO $ newMemoStore

getClassRec :: forall tt. (Object tt) => ClassDef tt -> IO ClassRec
getClassRec cdef = do
    let typ = typeOf (undefined :: tt)
    (_, val) <- getFromMemoStore classRecDb typ (createClass typ cdef)
    return val

createClass :: forall tt. (Object tt) => TypeRep -> ClassDef tt -> IO ClassRec
createClass typRep cdef = do
  hsqmlInit
  classId <- hsqmlGetNextClassId
  let constrs t = typeRepTyCon t : (concatMap constrs $ typeRepArgs t)
      name = foldr (\c s -> showString (tyConName c) .
          showChar '_' . s) id (constrs typRep) $ showInt classId ""
      ms = classMembers cdef
      moc = compileClass name ms
      sigs = filterMembers SignalMember ms
      sigMap = Map.fromList $ flip zip [0..] $ map (fromJust . memberKey) sigs
      maybeMarshalFunc = maybe (return nullFunPtr) marshalFunc
  metaDataPtr <- crlToNewArray return (mData moc)
  metaStrDataPtr <- crlToNewArray return (mStrData moc)
  methodsPtr <- crlToNewArray maybeMarshalFunc (mFuncMethods moc)
  propsPtr <- crlToNewArray maybeMarshalFunc (mFuncProperties moc)
  maybeHndl <- hsqmlCreateClass
      metaDataPtr metaStrDataPtr typRep methodsPtr propsPtr
  case maybeHndl of
      Just hndl -> return $ ClassRec hndl sigMap
      Nothing -> error ("Failed to create QML class '"++name++"'.")

--
-- Method
--

data MethodTypeInfo = MethodTypeInfo {
  methodParamTypes :: [TypeName],
  methodReturnType :: TypeName
}

newtype MSHelp a = MSHelp a

-- | Supports marshalling Haskell functions with an arbitrary number of
-- arguments.
class MethodSuffix a where
  mkMethodFunc  :: Int -> a -> Ptr (Ptr ()) -> ErrIO ()
  mkMethodTypes :: Tagged a MethodTypeInfo

instance (Marshal a, MarshalToHs (MarshalMode a), MethodSuffix (MSHelp b)) =>
  MethodSuffix (MSHelp (a -> b)) where
  mkMethodFunc n (MSHelp f) pv = do
    ptr <- errIO $ peekElemOff pv n
    val <- mValToHs ptr
    mkMethodFunc (n+1) (MSHelp $ f val) pv
    return ()
  mkMethodTypes =
    let (MethodTypeInfo p r) =
          untag (mkMethodTypes :: Tagged (MSHelp b) MethodTypeInfo)
        typ = untag (mTypeName :: Tagged a TypeName)
    in Tagged $ MethodTypeInfo (typ:p) r

instance (Marshal a, MarshalToValRaw (MarshalMode a)) =>
  MethodSuffix (MSHelp (IO a)) where
  mkMethodFunc _ (MSHelp f) pv = errIO $ do
    ptr <- peekElemOff pv 0
    val <- f
    if nullPtr == ptr
    then return ()
    else mHsToVal val ptr
  mkMethodTypes =
    let typ = untag (mTypeName :: Tagged a TypeName)
    in Tagged $ MethodTypeInfo [] typ

mkUniformFunc :: forall tt ms.
  (Marshal tt, MarshalFromObj (MarshalMode tt), MethodSuffix (MSHelp ms)) =>
  (tt -> ms) -> UniformFunc
mkUniformFunc f = \pt pv -> do
  hndl <- hsqmlGetObjectHandle pt
  this <- mObjToHs hndl
  runErrIO $ mkMethodFunc 1 (MSHelp $ f this) pv

newtype VoidIO = VoidIO {runVoidIO :: (IO ())}

instance MethodSuffix (MSHelp VoidIO) where
    mkMethodFunc _ (MSHelp f) pv = errIO $ runVoidIO f
    mkMethodTypes = Tagged $ MethodTypeInfo [] (TypeName "")

class IsVoidIO a
instance (IsVoidIO b) => IsVoidIO (a -> b)
instance IsVoidIO VoidIO

mkSpecialFunc :: forall tt ms.
    (Marshal tt, MarshalFromObj (MarshalMode tt), MethodSuffix (MSHelp ms),
        IsVoidIO ms) => (tt -> ms) -> UniformFunc
mkSpecialFunc f = \pt pv -> do
    hndl <- hsqmlGetObjectHandle pt
    this <- mObjToHs hndl
    runErrIO $ mkMethodFunc 0 (MSHelp $ f this) pv

-- | Defines a named method using a function @f@ in the IO monad.
--
-- The first argument to @f@ receives the \"this\" object and hence must match
-- the type of the class on which the method is being defined. Subsequently,
-- there may be zero or more parameter arguments followed by an optional return
-- argument in the IO monad.
defMethod :: forall tt ms.
  (Marshal tt, MarshalFromObj (MarshalMode tt), MethodSuffix (MSHelp ms)) =>
  String -> (tt -> ms) -> Member (ThisObj tt)
defMethod name f =
  let crude = untag (mkMethodTypes :: Tagged (MSHelp ms) MethodTypeInfo)
  in Member MethodMember
       name
       (methodReturnType crude)
       (methodParamTypes crude)
       (mkUniformFunc f)
       Nothing
       Nothing

--
-- Property
--

-- | Defines a named read-only property using an accessor function in the IO
-- monad.
defPropertyRO ::
  forall tt tr. (Marshal tt, MarshalFromObj (MarshalMode tt),
    Marshal tr, MarshalToVal (MarshalMode tr)) =>
  String -> (tt -> IO tr) -> Member (ThisObj tt)
defPropertyRO name g = Member PropertyMember
  name
  (untag (mTypeName :: Tagged tr TypeName))
  []
  (mkUniformFunc g)
  Nothing
  Nothing

-- | Defines a named read-write property using a pair of accessor and mutator
-- functions in the IO monad.
defPropertyRW ::
  forall tt tr. (Marshal tt, MarshalFromObj (MarshalMode tt),
    Marshal tr, MarshalToHs (MarshalMode tr), MarshalToVal (MarshalMode tr)) =>
  String -> (tt -> IO tr) -> (tt -> tr -> IO ()) -> Member (ThisObj tt)
defPropertyRW name g s = Member PropertyMember
  name
  (untag (mTypeName :: Tagged tr TypeName))
  []
  (mkUniformFunc g)
  (Just $ mkSpecialFunc (\a b -> VoidIO $ s a b))
  Nothing

--
-- Signal
--

data SignalTypeInfo = SignalTypeInfo {
  signalParamTypes :: [TypeName]
}

-- | Defines a named signal using a 'SignalKey'.
defSignal ::
  forall obj sk. (Object obj, SignalKey sk) => Tagged sk String -> Member obj
defSignal tn =
  let crude = untag (mkSignalTypes :: Tagged (SignalParams sk) SignalTypeInfo)
  in Member SignalMember
       (untag tn)
       (TypeName "")
       (signalParamTypes crude)
       (\_ _ -> return ())
       Nothing
       (Just $ typeOf (undefined :: sk))

-- | Fires a signal on an 'Object', specified using a 'SignalKey'.
fireSignal ::
    forall tt sk. (
        Marshal tt, MarshalToObj (MarshalMode tt),
        Object (ThisObj tt), SignalKey sk) =>
    Tagged sk tt -> SignalParams sk 
fireSignal this =
   let start cnt = do
           crec <- getClassRec (classDef :: ClassDef (ThisObj tt))
           let keyRep = typeOf (undefined :: sk)
               slotMay = Map.lookup keyRep $ crecSigs crec
           case slotMay of
                Just slotIdx -> postJob $ do
                    hndl <- mHsToObj $ untag this
                    withActiveObject hndl $ cnt $ SignalData hndl slotIdx
                Nothing ->
                    error ("Attempt to fire undefined signal on class '"++
                    (typeName $ untag (mTypeName :: Tagged tt TypeName))++"'.")
       cont ps (SignalData hndl slotIdx) =
           withArray (nullPtr:ps) (\pptr ->
                hsqmlFireSignal hndl slotIdx pptr)
    in mkSignalArgs start cont

data SignalData = SignalData HsQMLObjectHandle Int

-- | Instances of the 'SignalKey' class identify distinct signals. The
-- associated 'SignalParams' type specifies the signal's signature.
class (SignalSuffix (SignalParams sk), Typeable sk) => SignalKey sk where
  type SignalParams sk

-- | Supports marshalling an arbitrary number of arguments into a QML signal.
class SignalSuffix ss where
    mkSignalArgs  :: forall usr.
        ((usr -> IO ()) -> IO ()) -> ([Ptr ()] -> usr -> IO ()) -> ss
    mkSignalTypes :: Tagged ss SignalTypeInfo

instance (Marshal a, MarshalToVal (MarshalMode a), SignalSuffix b) =>
    SignalSuffix (a -> b) where
    mkSignalArgs start cont param =
        mkSignalArgs start (\ps usr ->
            mHsToAlloc param (\ptr ->
                cont (ptr:ps) usr))
    mkSignalTypes =
        let (SignalTypeInfo p) =
                untag (mkSignalTypes :: Tagged b SignalTypeInfo)
            typ = untag (mTypeName :: Tagged a TypeName)
        in Tagged $ SignalTypeInfo (typ:p)

instance SignalSuffix (IO ()) where
    mkSignalArgs start cont =
        start $ cont []
    mkSignalTypes =
        Tagged $ SignalTypeInfo []
