{-|
Module      : OCaml.BuckleScript.Internal.Module
Description : Build OCaml Modules from Haskell Types
Copyright   : Plow Technologies, 2017
License     : BSD3
Maintainer  : mchaver@gmail.com
Stability   : experimental

-}

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings  #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC  -fno-warn-redundant-constraints #-}

module OCaml.BuckleScript.Internal.Module
  (
  -- define an OCamlModule with servant style types
    OCamlModule
  , OCamlSubModule
  , OCamlTypeInFile
  , HaskellTypeName

  , EmbeddedOCamlFiles (..)

  , HasOCamlType (..)

  -- template haskell
  -- load OCamlTypeInFile files at compile time
  , HasEmbeddedFile (..)
  ) where

-- base
import Data.Monoid ((<>))
import Data.Proxy (Proxy (..))
import Data.Typeable (Typeable, typeRep, typeRepTyCon, tyConName, splitTyConApp)
import GHC.TypeLits (Nat, Symbol, KnownSymbol, symbolVal)

-- bytestring
import Data.ByteString (ByteString)

-- containers
import qualified Data.Map.Strict as Map

-- directory
-- import System.Directory (createDirectoryIfMissing)

-- file-embed
import Data.FileEmbed (embedFile)

-- filepath
import System.FilePath.Posix ((<.>)) -- ((</>), (<.>))

-- ocaml-export
import OCaml.Internal.Common hiding ((</>))
import OCaml.BuckleScript.Decode
import OCaml.BuckleScript.Encode
import OCaml.BuckleScript.Record
import OCaml.BuckleScript.Spec
import OCaml.BuckleScript.Types

-- servant
import Servant.API ((:>), (:<|>))

-- template-haskell
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (addDependentFile)

-- text
import Data.Text (Text)
import qualified Data.Text as T
-- import qualified Data.Text.IO as T
import Data.Text.Encoding (decodeUtf8)


-- ==============================================
-- Types
-- ==============================================

-- | An OCamlModule as a Haskell type. File level 'modules' is relative to a
--   root directory prvoiided in the 'mkPackage' function. 
data OCamlModule (modules :: [Symbol])
  deriving Typeable

-- | Symobl will be expaneded to 
--   "module SymbolName = struct ... end".
data OCamlSubModule (subModules :: Symbol)
  deriving Typeable

-- | A handwritten OCaml type, encoder and decoder from a file.
data OCamlTypeInFile a (filePath :: Symbol)
  deriving Typeable

-- | In case Generic is not derived, manually provide the type name
--   user is responsible for making sure it is correct. otherwise it
--   may lead to a compile error.
data HaskellTypeName (name :: Symbol) a

-- ==============================================
-- Data Types
-- ==============================================

-- | Store OCamlFileInType data.
data EmbeddedOCamlFiles =
  EmbeddedOCamlFiles
    { eocDeclaration :: ByteString
    , eocInterface   :: Maybe ByteString
    , eocSpec        :: Maybe ByteString
    } deriving (Show)


-- ==============================================
-- Type Level Functions
-- ==============================================

-- | Convert a Haskell type into OCaml source code.
class HasOCamlType api where
  mkType :: Proxy api -> Options -> Bool -> Map.Map String EmbeddedOCamlFiles -> [Text]
  mkInterface :: Proxy api -> Options -> Map.Map String EmbeddedOCamlFiles -> [Text]
  mkSpec :: Proxy api -> Options -> [Text] -> Maybe Text -> Text -> Map.Map String EmbeddedOCamlFiles -> [Text]

instance (HasOCamlTypeFlag a ~ flag, HasOCamlType' flag (a :: *)) => HasOCamlType a where
  mkType = mkType' (Proxy :: Proxy flag)
  mkInterface = mkInterface' (Proxy :: Proxy flag)
  mkSpec = mkSpec' (Proxy :: Proxy flag)

type family (HasOCamlTypeFlag a) :: Nat where
  HasOCamlTypeFlag (OCamlSubModule a :> b) = 4
  HasOCamlTypeFlag (a :> b) = 3
  HasOCamlTypeFlag (HaskellTypeName a (OCamlTypeInFile b c)) = 2
  HasOCamlTypeFlag (OCamlTypeInFile a b) = 2
  HasOCamlTypeFlag a = 1

-- | Helper function to work avoid overlapped instances.
class HasOCamlType' (flag :: Nat) api where
  mkType' :: Proxy flag -> Proxy api -> Options -> Bool -> Map.Map String EmbeddedOCamlFiles -> [Text]
  mkInterface' :: Proxy flag -> Proxy api -> Options -> Map.Map String EmbeddedOCamlFiles -> [Text]
  mkSpec' :: Proxy flag -> Proxy api -> Options -> [Text] -> Maybe Text -> Text -> Map.Map String EmbeddedOCamlFiles -> [Text]

instance (KnownSymbol subModule, HasOCamlType b) => HasOCamlType' 4 (OCamlSubModule subModule :> b) where
  mkType' _ Proxy options interface fileMap = ["module " <> (T.pack $ symbolVal (Proxy :: Proxy subModule)) <> " = struct\n"] <> (mkType (Proxy :: Proxy b) options interface fileMap) <> ["\nend"]

  mkInterface' _ Proxy options fileMap = ["module " <> (T.pack $ symbolVal (Proxy :: Proxy subModule)) <> " : sig\n"] <> (mkInterface (Proxy :: Proxy b) options fileMap) <> ["\nend"]

  mkSpec' _ Proxy options modules url goldendir fileMap = mkSpec (Proxy :: Proxy b) options modules url goldendir fileMap

instance (HasOCamlType a, HasOCamlType b) => HasOCamlType' 3 (a :> b) where
  mkType' _ Proxy options interface fileMap = (mkType (Proxy :: Proxy a) options interface fileMap) <> (mkType (Proxy :: Proxy b) options interface fileMap)
  mkInterface' _ Proxy options fileMap = (mkInterface (Proxy :: Proxy a) options fileMap) <> (mkInterface (Proxy :: Proxy b) options fileMap)
  mkSpec' _ Proxy options modules url goldendir fileMap = (mkSpec (Proxy :: Proxy a) options modules url goldendir fileMap) <> (mkSpec (Proxy :: Proxy b) options modules url goldendir fileMap)

instance (HasOCamlType (OCamlTypeInFile a b)) => HasOCamlType' 2 (HaskellTypeName typSymbol (OCamlTypeInFile a b)) where
  mkType' _ Proxy options interface fileMap = (mkType (Proxy :: Proxy (OCamlTypeInFile a b)) options interface fileMap)
  mkInterface' _ Proxy options fileMap = (mkInterface (Proxy :: Proxy (OCamlTypeInFile a b)) options fileMap)
  mkSpec' _ Proxy options modules url goldendir fileMap = (mkSpec (Proxy :: Proxy (OCamlTypeInFile a b)) options modules url goldendir fileMap)

instance (OCamlType a, Typeable a) => HasOCamlType' 2 (OCamlTypeInFile a b) where
  mkType' _ Proxy _options _ fileMap = do
    let typeName = tyConName . typeRepTyCon $ typeRep (Proxy :: Proxy a)
    case eocDeclaration <$> Map.lookup typeName fileMap of
      Just v -> [decodeUtf8 v]
      _ -> fail $ "Unable to find the embedded file for " ++ typeName

  mkInterface' _ Proxy _options fileMap = do
    let typeName = tyConName . typeRepTyCon $ typeRep (Proxy :: Proxy a)
    case eocInterface <$> Map.lookup typeName fileMap of
      Just (Just v) -> [decodeUtf8 v]
      _ -> fail $ "Unable to find the embedded file for " ++ typeName

  mkSpec' _ Proxy _options modules url goldendir _fileMap =
    [typeInFileToOCamlSpec (Proxy :: Proxy a) typeParameterRefCount modules url goldendir]
    where
      parameters = fmap show . snd . splitTyConApp $ typeRep (Proxy :: Proxy a)
      inParameters = flip elem parameters
      xs =
        [ inParameters "TypeParameterRef0"
        , inParameters "TypeParameterRef1"
        , inParameters "TypeParameterRef2"
        , inParameters "TypeParameterRef3"
        , inParameters "TypeParameterRef4"
        , inParameters "TypeParameterRef5"
        , inParameters "TypeParameterRef6"
        ]
      countTrues = foldl (flip ((+) . fromEnum)) 0
      typeParameterRefCount = countTrues xs


instance (OCamlType a) => HasOCamlType' 1 a where
  mkType' _ a options interface _ = body
    where
      body = [(toOCamlTypeSourceWith options a)
          , (toOCamlEncoderSourceWith (options {includeOCamlInterface = interface}) a)
          , (toOCamlDecoderSourceWith (options {includeOCamlInterface = interface}) a)]

  mkInterface' _ a options _ = body
    where
      body = [(toOCamlTypeSourceWith options a)
          , (toOCamlEncoderInterfaceWith options a)
          , (toOCamlDecoderInterfaceWith options a)]

  mkSpec' _ a _options modules url goldendir _ = [toOCamlSpec a modules url goldendir]




-- ==============================================
-- Template Haskell
-- ==============================================

-- | Use Template Haskell to load OCaml files for an OCaml Module at compile time.
--   '$(mkFile (Proxy :: Proxy Package))'.
class HasEmbeddedFile api where
  mkFiles :: Bool -> Bool -> Proxy api -> Q Exp

instance (HasEmbeddedFile' api) => HasEmbeddedFile api where
  mkFiles includeInterface includeSpec Proxy = ListE <$> mkFiles' includeInterface includeSpec (Proxy :: Proxy api)

class HasEmbeddedFile' api where
  mkFiles' :: Bool -> Bool -> Proxy api -> Q [Exp]

instance (HasEmbeddedFileFlag a ~ flag, HasEmbeddedFile'' flag (a :: *)) => HasEmbeddedFile' a where
  mkFiles' = mkFiles'' (Proxy :: Proxy flag)

type family (HasEmbeddedFileFlag a) :: Nat where
  HasEmbeddedFileFlag (a :<|> b) = 5
  HasEmbeddedFileFlag (a :> b) = 4
  HasEmbeddedFileFlag (HaskellTypeName a (OCamlTypeInFile b c)) = 3
  HasEmbeddedFileFlag (OCamlTypeInFile b c) = 2
  HasEmbeddedFileFlag a = 1

-- | Helper function to work avoid overlapped instances.
class HasEmbeddedFile'' (flag :: Nat) api where
  mkFiles'' :: Proxy flag -> Bool -> Bool -> Proxy api -> Q [Exp]

instance (HasEmbeddedFile' a, HasEmbeddedFile' b) => HasEmbeddedFile'' 5 (a :<|> b) where
  mkFiles'' _ includeInterface includeSpec Proxy =
    (<>) <$> mkFiles' includeInterface includeSpec (Proxy :: Proxy a)
         <*> mkFiles' includeInterface includeSpec (Proxy :: Proxy b)

instance (HasEmbeddedFile' a, HasEmbeddedFile' b) => HasEmbeddedFile'' 4 (a :> b) where
  mkFiles'' _ includeInterface includeSpec Proxy =
    (<>) <$> mkFiles' includeInterface includeSpec (Proxy :: Proxy a)
         <*> mkFiles' includeInterface includeSpec (Proxy :: Proxy b)

instance (HasEmbeddedFile' (OCamlTypeInFile a b)) => HasEmbeddedFile'' 3 (HaskellTypeName typSymbol (OCamlTypeInFile a b)) where
  mkFiles'' _ includeInterface includeSpec Proxy =
    mkFiles' includeInterface includeSpec (Proxy :: Proxy (OCamlTypeInFile a b))

instance (Typeable a, KnownSymbol b) => HasEmbeddedFile'' 2 (OCamlTypeInFile a b) where
  mkFiles'' _ includeInterface includeSpec Proxy = do
    let typeFilePath = symbolVal (Proxy :: Proxy b)
    let typeName = tyConName . typeRepTyCon $ typeRep (Proxy :: Proxy a)
    addDependentFile (typeFilePath <.> "ml")
    ml  <- embedFile (typeFilePath <.> "ml")

    mli <- if includeInterface
      then do
        addDependentFile (typeFilePath <.> "mli")
        (\f -> AppE (ConE $ mkName "Just") f) <$> embedFile (typeFilePath <.> "mli")
      else pure $ ConE $ mkName "Nothing"

    spec <- if includeSpec
      then do
        addDependentFile (typeFilePath <> "_spec" <.> "ml")
        (\f -> AppE (ConE $ mkName "Just") f) <$> embedFile (typeFilePath <> "_spec" <.> "ml")
      else pure $ ConE $ mkName "Nothing"

    pure [TupE [LitE $ StringL typeName, AppE (AppE (AppE (ConE $ mkName "EmbeddedOCamlFiles") ml) mli) spec]]

instance HasEmbeddedFile'' 1 a where
  mkFiles'' _ _ _ Proxy = pure []