{-# language QuasiQuotes #-}
{-# language TemplateHaskell #-}

module OpenCV.Internal.Core.Types.Point.TH
  ( mkPointType
  ) where

import "base" Data.List ( intercalate )
import "base" Data.Monoid ( (<>) )
import "base" Foreign.Marshal.Alloc ( alloca )
import "base" Foreign.Storable ( peek )
import "base" System.IO.Unsafe ( unsafePerformIO )
import qualified "inline-c" Language.C.Inline.Unsafe as CU
import "linear" Linear ( V2(..), V3(..) )
import "template-haskell" Language.Haskell.TH
import "template-haskell" Language.Haskell.TH.Quote ( quoteExp )
import "this" OpenCV.Internal.C.PlacementNew.TH ( mkPlacementNewInstance )
import "this" OpenCV.Internal.C.Types
import "this" OpenCV.Internal.Core.Types.Point
import "this" OpenCV.Internal

mkPointType
    :: String  -- ^ Point type name, for both Haskell and C
    -> Integer -- ^ Point dimension
    -> String  -- ^ Point template name in C
    -> Name    -- ^ Depth type name in Haskell
    -> String  -- ^ Depth type name in C
    -> Q [Dec]
mkPointType pTypeNameStr dim cTemplateStr depthTypeName cDepthTypeStr
    | dim < 2 || dim > 3 = fail $ "mkPointType: Unsupported dimension: " <> show dim
    | otherwise =
      fmap concat . sequence $
        [ pure <$> pointTySynD
        , fromPtrDs
        , isPointOpenCVInstanceDs
        , isPointHaskellInstanceDs
        , mkPlacementNewInstance pTypeName
        ]
  where
    pTypeName :: Name
    pTypeName = mkName pTypeNameStr

    cPointTypeStr :: String
    cPointTypeStr = pTypeNameStr

    pTypeQ :: Q Type
    pTypeQ = conT pTypeName

    depthTypeQ :: Q Type
    depthTypeQ = conT depthTypeName

    dimTypeQ :: Q Type
    dimTypeQ = litT (numTyLit dim)

    pointTySynD :: Q Dec
    pointTySynD =
        tySynD pTypeName
               []
               ([t|Point|] `appT` dimTypeQ `appT` depthTypeQ)

    fromPtrDs :: Q [Dec]
    fromPtrDs =
        [d|
        instance FromPtr $(pTypeQ) where
          fromPtr = objFromPtr Point $ $(finalizerExpQ)
        |]
      where
        finalizerExpQ :: Q Exp
        finalizerExpQ = do
          ptr <- newName "ptr"
          lamE [varP ptr] $
            quoteExp CU.exp $
              "void { delete $(" <> cPointTypeStr <> " * " <> nameBase ptr <> ") }"

    isPointOpenCVInstanceDs :: Q [Dec]
    isPointOpenCVInstanceDs =
        [d|
        instance IsPoint (Point $(dimTypeQ)) $(depthTypeQ) where
          toPoint   = id
          toPointIO = pure
          fromPoint = id
        |]

    isPointHaskellInstanceDs :: Q [Dec]
    isPointHaskellInstanceDs =
        let ix = fromInteger dim - 2
        in withLinear (linearTypeQs   !! ix)
                      (linearConNames !! ix)
      where
        linearTypeQs :: [Q Type]
        linearTypeQs = map conT [''V2, ''V3]

        linearConNames :: [Name]
        linearConNames = ['V2, 'V3]

        withLinear :: Q Type -> Name -> Q [Dec]
        withLinear lpTypeQ lvConName =
            [d|
            instance IsPoint $(lpTypeQ) $(depthTypeQ) where
              toPoint   = unsafePerformIO . toPointIO
              toPointIO = $(toPointIOExpQ)
              fromPoint = $(fromPointExpQ)
            |]
          where
            toPointIOExpQ :: Q Exp
            toPointIOExpQ = do
                ns <- mapM newName elemNames
                lamE [conP lvConName $ map varP ns]
                 $ appE [e|fromPtr|]
                 $ quoteExp CU.exp
                 $ inlineCStr ns
              where
                inlineCStr :: [Name] -> String
                inlineCStr ns = concat
                    [ cPointTypeStr
                    , " * { new cv::" <> cTemplateStr
                    , "<" <> cDepthTypeStr <> ">"
                    , "(" <> intercalate ", " (map elemQuote ns) <> ")"
                    , " }"
                    ]
                  where
                    elemQuote :: Name -> String
                    elemQuote n = "$(" <> cDepthTypeStr <> " " <> nameBase n <> ")"

            fromPointExpQ :: Q Exp
            fromPointExpQ = do
                point <- newName "point"
                pointPtr <- newName "pointPtr"
                ptrNames <- mapM (newName . (<> "Ptr")) elemNames
                withPtrNames point pointPtr ptrNames
              where
                withPtrNames :: Name -> Name -> [Name] -> Q Exp
                withPtrNames point pointPtr ptrNames =
                    lamE [varP point]
                      $ appE [e|unsafePerformIO|]
                      $ withPtrVarsExpQ ptrNames
                  where

                    withPtrVarsExpQ :: [Name] -> Q Exp
                    withPtrVarsExpQ = foldr (\p -> appE [e|alloca|] . lamE [varP p]) withAllocatedVars

                    withAllocatedVars :: Q Exp
                    withAllocatedVars =
                        appE ([e|withPtr|] `appE` varE point)
                          $ lamE [varP pointPtr]
                          $ doE
                            [ noBindS $ quoteExp CU.block inlineCStr
                            , noBindS extractExpQ
                            ]

                    inlineCStr :: String
                    inlineCStr = unlines $
                        concat
                          [ "void {"
                          , "const cv::" <> cTemplateStr
                          , "<" <> cDepthTypeStr <> ">"
                          , " & p = *$("
                          , cPointTypeStr
                          , " * "
                          , nameBase pointPtr
                          , ");"
                          ]
                        : map ptrLine (zip [0..] ptrNames)
                        <> ["}"]
                      where
                        ptrLine :: (Int, Name) -> String
                        ptrLine (ix, ptrName) =
                            "*$(" <> cDepthTypeStr <> " * " <> nameBase ptrName <> ") = p." <> elemNames !! ix <> ";"

                    -- Applies the constructor to the values that are
                    -- read from the pointers.
                    extractExpQ :: Q Exp
                    extractExpQ = foldl (\acc peekExp -> [e|(<*>)|] `appE` acc `appE` peekExp)
                                        ([e|pure|] `appE` conE lvConName)
                                        peekExpQs
                      where
                        peekExpQs :: [Q Exp]
                        peekExpQs = map (\p -> [e|peek|] `appE` varE p) ptrNames

            elemNames :: [String]
            elemNames = take (fromInteger dim)
                             ["x", "y", "z"]
