{-|
Module      : OCaml.BuckleScript.Types
Description : Tree representation of Haskell datatypes in OCaml
Copyright   : Plow Technologies, 2017
License     : BSD3
Maintainer  : mchaver@gmail.com
Stability   : experimental

OCaml datatype representation of a Haskell datatype. A recursive tree that
can be interpreted to output OCaml code. It is meant to encode a Haskell type
into OCaml and make json seraliazers that match the output from Generic aeson
instances.
-}

{-# LANGUAGE DataKinds    #-}
{-# LANGUAGE DefaultSignatures    #-}
{-# LANGUAGE DeriveGeneric        #-}
{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE FlexibleInstances    #-}
{-# LANGUAGE OverloadedStrings    #-}
{-# LANGUAGE ScopedTypeVariables  #-}
{-# LANGUAGE TypeOperators        #-}
{-# LANGUAGE TypeSynonymInstances #-}

module OCaml.BuckleScript.Types
  ( OCamlDatatype (..)
  , OCamlPrimitive (..)
  , OCamlConstructor (..)
  , ValueConstructor (..)
  , EnumeratorConstructor (..)
  , OCamlValue (..)
  , OCamlType (..)
  , HaskellTypeMetaData (..)
  , OCamlTypeMetaData (..)

  , typeableToOCamlType
  -- fill type parameters of a proxy when calling toOCamlType
  -- so the kind is *
  -- e.g. `toOCamlType (Proxy :: Proxy (Either TypeParameterRef0 TypeParameterRef1))`
  , TypeParameterRef0(..)
  , TypeParameterRef1(..)
  , TypeParameterRef2(..)
  , TypeParameterRef3(..)
  , TypeParameterRef4(..)
  , TypeParameterRef5(..)

  -- functions for manipulating and querying the data type tree
  , getTypeParameterRefNames
  , getOCamlValues
  , getTypeParameters
  , isTypeParameterRef
  , mkModulePrefix
  , oCamlValueIsFloat

  -- Typeable functions
  , typeRepToHaskellTypeMetaData
  , tyConToHaskellTypeMetaData
  ) where

-- base
import Data.Int (Int16, Int32, Int64, Int8)
import Data.List (nub)
import Data.Maybe (catMaybes)
import Data.Monoid ((<>))
import Data.Proxy
import Data.Time
import Data.Typeable
import Data.Word (Word, Word8, Word16, Word32, Word64)
import GHC.Generics
import GHC.TypeLits (symbolVal, KnownSymbol)
import Prelude

import qualified Data.Map as Map

-- aeson
import Data.Aeson (ToJSON, FromJSON)
-- bytestring
import Data.ByteString (ByteString)
-- text
import Data.Text (Text)
import qualified Data.Text as T

-- QuickCheck
import Test.QuickCheck

-- quicheck-arbitrary-adt
import Test.QuickCheck.Arbitrary.ADT

-- | Top level of an OCaml datatype. A data type may be composed of
--   primitives and/or a combination of constructors and primitives.
--   OCamlDatatype is recursive via OCamlConstructor -> ValueConstructor
--   -> OCamlValue -> OCamlPrimitive -> OCamlDatatype.
data OCamlDatatype
  = OCamlDatatype HaskellTypeMetaData Text OCamlConstructor -- ^ The name of a type and its type constructor
  | OCamlPrimitive OCamlPrimitive -- ^ A primitive value
  deriving (Show, Eq)

-- | Store data about the Haskell origin of a type.
data HaskellTypeMetaData =
  HaskellTypeMetaData
    Text -- "TypeName"
    Text -- "Module.Name"
    Text -- "package-name"
    deriving (Show, Eq, Ord)

-- | Store data about the OCaml destination of a type.
data OCamlTypeMetaData =
  OCamlTypeMetaData
    Text -- "typeName"
    [Text] -- ["File","Path"]
    [Text] -- ["Sub","Module"]
    deriving (Show, Eq, Ord)

-- | Smallest unit of computation in OCaml.
data OCamlPrimitive
  = OBool -- ^ bool, boolean
  | OChar -- ^ char, it gets interpreted as a string because OCaml char does not support UTF-8
  | ODate -- ^ Js_date.t
  | OFloat -- ^ float
  | OInt -- ^ int
  | OInt32 -- ^ int32
  | OString -- ^ string
  | OUnit -- ^ ()
  | OList OCamlDatatype -- ^ 'a list, 'a Js_array.t
  | OOption OCamlDatatype -- ^ 'a option
  | OEither OCamlDatatype OCamlDatatype -- ^ 'l 'r Aeson.Compatibility.Either.t
  | OTuple2 OCamlDatatype OCamlDatatype -- ^ (*)
  | OTuple3 OCamlDatatype OCamlDatatype OCamlDatatype -- ^ (**)
  | OTuple4 OCamlDatatype OCamlDatatype OCamlDatatype OCamlDatatype -- ^ (***)
  | OTuple5 OCamlDatatype OCamlDatatype OCamlDatatype OCamlDatatype OCamlDatatype -- ^ (****)
  | OTuple6 OCamlDatatype OCamlDatatype OCamlDatatype OCamlDatatype OCamlDatatype OCamlDatatype -- ^ (*****)
  deriving (Show, Eq)

-- | OCamlConstructor take values to create a new instances of a type.
data OCamlConstructor
  = OCamlValueConstructor ValueConstructor -- ^ Sum, record (product with named fields) or product without named fields
  | OCamlEnumeratorConstructor [EnumeratorConstructor] -- ^ Sum of enumerations only. If a sum contains enumerators and at least one constructor with a value then it is an OCamlValueConstructor
  | OCamlSumOfRecordConstructor Text ValueConstructor -- ^ Sum that contains at least one record. This construction is unique to Haskell. It has special Encoding and Decoding rules in order to output a valid OCaml program. i.e. `data A = A {a :: Int} | B {b :: String}`
  deriving (Show, Eq)

-- | OCamlConstructor of one RecordConstructor is a record type.
--   OCamlConstructor of one NamedConstructor that has one value is a Haskell newtype.
--   OCamlConstructor of one NamedConstructor is a product without field names.
--   OCamlConstructor of multiple NamedConstructors is a sum type.
--   OCamlConstructor of at least one RecordConstructor and any other amount of ValueConstructors greater than one is a OCamlSumWithRecordConstructor.
data ValueConstructor
  = NamedConstructor Text OCamlValue -- ^ Product without named fields
  | RecordConstructor Text OCamlValue -- ^ Product with named fields
  | MultipleConstructors [ValueConstructor] -- ^ Sum type
  deriving (Show, Eq)

-- | Enumerators have no values, only tags.
data EnumeratorConstructor
  = EnumeratorConstructor Text -- ^ Enumerator and its tag
  deriving (Show, Eq)

-- | Expected types of a constructor
data OCamlValue
  = OCamlRef HaskellTypeMetaData Text -- ^ The name of a non-primitive data type
  | OCamlRefApp TypeRep OCamlValue -- ^ A type constructor that has at least one type parameter filled
  | OCamlTypeParameterRef Text -- ^ Type parameters like `a` in `Maybe a`
  | OCamlEmpty -- ^ a place holder for OCaml value. It can represent the end of a list or an Enumerator in a mixed sum
  | OCamlPrimitiveRef OCamlPrimitive -- ^ A primitive OCaml type like `int`, `string`, etc.
  | OCamlField Text OCamlValue -- ^ A field name and its type from a record.
  | Values OCamlValue OCamlValue -- ^ Used for multiple types in a NameConstructor or a RecordConstructor.
  | OCamlRefAppValues OCamlValue OCamlValue -- ^ User for multiple types in an OCamlRefApp. These are rendered in a different way from Values.
  deriving (Show, Eq)
--  -- ^
------------------------------------------------------------
-- | Create an OCaml type from a Haskell type. Use the Generic
--   definition when possible. It also expects `ToJSON` and `FromJSON`
--   to be derived generically.
class OCamlType a where
  toOCamlType :: a -> OCamlDatatype
  toOCamlType = genericToOCamlDatatype . from
  default toOCamlType :: (Generic a, GenericOCamlDatatype (Rep a)) =>
    a -> OCamlDatatype

------------------------------------------------------------
class GenericOCamlDatatype f where
  genericToOCamlDatatype :: f a -> OCamlDatatype

-- | Capture the Haskell type at the left side declaration `data Maybe a`, `data Person`, etc..
--   Transform the constructor, depending on its values, if necessary.
instance (KnownSymbol typ, KnownSymbol package, KnownSymbol modul, GenericValueConstructor f) => GenericOCamlDatatype (M1 D ('MetaData typ modul package 'False) f) where
  genericToOCamlDatatype datatype =
    OCamlDatatype
      (HaskellTypeMetaData
       (T.pack $ symbolVal (Proxy :: Proxy typ))
       (T.pack $ symbolVal (Proxy :: Proxy modul))
       (T.pack $ symbolVal (Proxy :: Proxy package)))
      (T.pack (datatypeName datatype))
      (transform (OCamlValueConstructor (genericToValueConstructor (unM1 datatype))))
    where
      transform ocamlConstructor =
        if isEnumeration ocamlConstructor
          then transformToEnumeration ocamlConstructor
          else
            if isSumWithRecord ocamlConstructor
              then transformToSumOfRecord (T.pack (datatypeName datatype)) ocamlConstructor
              else ocamlConstructor

instance (KnownSymbol typ, KnownSymbol package, KnownSymbol modul, GenericValueConstructor f) => GenericOCamlDatatype (M1 D ('MetaData typ modul package 'True) f) where
  genericToOCamlDatatype datatype =
    OCamlDatatype
      (HaskellTypeMetaData
       (T.pack $ symbolVal (Proxy :: Proxy typ))
       (T.pack $ symbolVal (Proxy :: Proxy modul))
       (T.pack $ symbolVal (Proxy :: Proxy package)))
      (T.pack (datatypeName datatype))
      (transform (OCamlValueConstructor (genericToValueConstructor (unM1 datatype))))
    where
      transform ocamlConstructor =
        if isEnumeration ocamlConstructor
          then transformToEnumeration ocamlConstructor
          else
            if isSumWithRecord ocamlConstructor
              then transformToSumOfRecord (T.pack (datatypeName datatype)) ocamlConstructor
              else ocamlConstructor

------------------------------------------------------------
class GenericValueConstructor f where
  genericToValueConstructor :: f a -> ValueConstructor

-- | Capture the Haskell type at the constructor. `Just` or `Nothing` from
--   `data Maybe a = Just a | Nothing`. 
instance (Constructor c, GenericOCamlValue f) => GenericValueConstructor (C1 c f) where
  genericToValueConstructor constructor =
    if conIsRecord constructor
      then RecordConstructor name (genericToOCamlValue (unM1 constructor))
      else NamedConstructor name (genericToOCamlValue (unM1 constructor))
    where
      name = T.pack $ conName constructor

-- | Capture the Haskell right side at the sum partition `|`.
instance (GenericValueConstructor f, GenericValueConstructor g) =>
         GenericValueConstructor (f :+: g) where
  genericToValueConstructor _ =
    MultipleConstructors
      [ genericToValueConstructor (undefined :: f p)
      , genericToValueConstructor (undefined :: g p)
      ]

------------------------------------------------------------
class GenericOCamlValue f where
  genericToOCamlValue :: f a -> OCamlValue

-- | Capture the constructor field.
instance (Selector s, GenericOCamlValue a) =>
         GenericOCamlValue (S1 s a) where
  genericToOCamlValue selector =
    case selName selector of
      ""   -> genericToOCamlValue (undefined :: a p)
      name -> OCamlField (T.pack name) (genericToOCamlValue (undefined :: a p))

-- | Capture the product comma.
instance (GenericOCamlValue f, GenericOCamlValue g) =>
         GenericOCamlValue (f :*: g) where
  genericToOCamlValue _ =
    Values
      (genericToOCamlValue (undefined :: f p))
      (genericToOCamlValue (undefined :: g p))

-- | Enumerator, constructor with no values.
instance GenericOCamlValue U1 where
  genericToOCamlValue _ = OCamlEmpty

-- | Handle type parameter. There are found in the order of declaration on the right hand side of a type.
--   Reordering may be necessary for TypeParameterRefs.
instance Typeable a => GenericOCamlValue (Rec0 a) where
  genericToOCamlValue _ = typeRepToOCamlValue $ typeRep (Proxy :: Proxy a)

typeRepToOCamlValue :: TypeRep -> OCamlValue
typeRepToOCamlValue t =
  -- check if the type is a primitive
  case Map.lookup hd typeParameterRefTyConToOCamlTypeText of
    Just p -> OCamlTypeParameterRef p
    Nothing ->
      case primitiveTypeRepToOCamlPrimitive t of
        Just primitive -> OCamlPrimitiveRef primitive
        Nothing ->
          -- if it has no typeParams then it mkRef
          if length typeParams == 0
          then mkRef (tyConToHaskellTypeMetaData hd) (T.pack . show $ hd)
          else OCamlRefApp t (mkValues)
  where
    (hd, typeParams) = splitTyConApp t

    typeParameterRefs = (T.append) <$> ["a"] <*> (T.pack . show <$> ([0..5] :: [Int]))

    mkRef haskellTypeMetaData n =
      if n `elem` typeParameterRefs
      then OCamlTypeParameterRef n
      else OCamlRef haskellTypeMetaData n

    mkValues =
      if length typeParams == 0
      then OCamlEmpty
      else
        if length typeParams == 1
        then typeRepToOCamlValue $ head typeParams -- Values (typeRepToOCamlValue $ head typeParams) OCamlEmpty
        else
          if length typeParams == 2
          then OCamlRefAppValues (typeRepToOCamlValue $ head typeParams) (typeRepToOCamlValue $ head $ tail typeParams)
          else OCamlRefAppValues (typeRepToOCamlValue $ head typeParams) (foldl (\b a -> OCamlRefAppValues b (typeRepToOCamlValue a)) (typeRepToOCamlValue $ head $ tail typeParams) (tail $ tail typeParams))

primitiveTypeRepToOCamlPrimitive :: TypeRep -> Maybe OCamlPrimitive
primitiveTypeRepToOCamlPrimitive t =
  mkOCamlPrimitive $ length typeParams
  where
    (hd, typeParams) = splitTyConApp t
    mkOCamlPrimitive l
      | l == 0 = Map.lookup hd zero
      | l == 1 = one hd (typeParams !! 0)
      | l == 2 = two hd (typeParams !! 0) (typeParams !! 1)
      | l == 3 = three hd (typeParams !! 0) (typeParams !! 1) (typeParams !! 2)
      | l == 4 = four hd (typeParams !! 0) (typeParams !! 1) (typeParams !! 2) (typeParams !! 3)
      | l == 5 = five hd (typeParams !! 0) (typeParams !! 1) (typeParams !! 2) (typeParams !! 3) (typeParams !! 4)
      | l == 6 = six hd (typeParams !! 0) (typeParams !! 1) (typeParams !! 2) (typeParams !! 3) (typeParams !! 4) (typeParams !! 5)
      | otherwise = Nothing

    zero :: Map.Map TyCon OCamlPrimitive
    zero = Map.fromList
      [ ( typeRepTyCon $ typeRep (Proxy :: Proxy Int), OInt)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Int8), OInt)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Int16), OInt)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Int32), OInt32)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Int64), OInt)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Integer), OInt)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Word), OInt)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Word8), OInt)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Word16), OInt)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Word32), OInt)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Word64), OInt)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Bool), OBool)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Char), OChar)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy UTCTime), ODate)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Float), OFloat)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Double), OFloat)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy Text), OString)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy ByteString), OString)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy String), OString)
      , ( typeRepTyCon $ typeRep (Proxy :: Proxy ()), OUnit)
      ]

    one :: TyCon -> TypeRep -> Maybe OCamlPrimitive
    one tyCon t0 =
      if tyCon == (typeRepTyCon $ typeRep (Proxy :: Proxy []))
      then Just $ OList $ mkOCamlDatatype t0
      else
        if tyCon == (typeRepTyCon $ typeRep (Proxy :: Proxy Maybe))
        then Just $ OOption $ mkOCamlDatatype t0
        else Nothing

    two :: TyCon -> TypeRep -> TypeRep -> Maybe OCamlPrimitive
    two tyCon t0 t1 =
      if tyCon == (typeRepTyCon $ typeRep (Proxy :: Proxy Either))
      then Just $ OEither (mkOCamlDatatype t0) (mkOCamlDatatype t1)
      else
        if tyCon == (typeRepTyCon $ typeRep (Proxy :: Proxy (,)))
        then Just $ OTuple2 (mkOCamlDatatype t0) (mkOCamlDatatype t1)
        else Nothing

    three :: TyCon -> TypeRep -> TypeRep -> TypeRep -> Maybe OCamlPrimitive
    three tyCon t0 t1 t2 =
      if tyCon == (typeRepTyCon $ typeRep (Proxy :: Proxy (,,)))
      then Just $ OTuple3 (mkOCamlDatatype t0) (mkOCamlDatatype t1) (mkOCamlDatatype t2)
      else Nothing

    four :: TyCon -> TypeRep -> TypeRep -> TypeRep -> TypeRep -> Maybe OCamlPrimitive
    four tyCon t0 t1 t2 t3 =
      if tyCon == (typeRepTyCon $ typeRep (Proxy :: Proxy (,,,)))
      then Just $ OTuple4 (mkOCamlDatatype t0) (mkOCamlDatatype t1) (mkOCamlDatatype t2) (mkOCamlDatatype t3)
      else Nothing

    five :: TyCon -> TypeRep -> TypeRep -> TypeRep -> TypeRep -> TypeRep -> Maybe OCamlPrimitive
    five tyCon t0 t1 t2 t3 t4 =
      if tyCon == (typeRepTyCon $ typeRep (Proxy :: Proxy (,,,,)))
      then Just $ OTuple5 (mkOCamlDatatype t0) (mkOCamlDatatype t1) (mkOCamlDatatype t2) (mkOCamlDatatype t3) (mkOCamlDatatype t4)
      else Nothing

    six :: TyCon -> TypeRep -> TypeRep -> TypeRep -> TypeRep -> TypeRep -> TypeRep -> Maybe OCamlPrimitive
    six tyCon t0 t1 t2 t3 t4 t5 =
      if tyCon == (typeRepTyCon $ typeRep (Proxy :: Proxy (,,,,,)))
      then Just $ OTuple6 (mkOCamlDatatype t0) (mkOCamlDatatype t1) (mkOCamlDatatype t2) (mkOCamlDatatype t3) (mkOCamlDatatype t4) (mkOCamlDatatype t5)
      else Nothing

    typeParameterRefMap = Map.fromList
      [ ("TypeParameterRef0", toOCamlType (Proxy :: Proxy TypeParameterRef0))
      , ("TypeParameterRef1", toOCamlType (Proxy :: Proxy TypeParameterRef1))
      , ("TypeParameterRef2", toOCamlType (Proxy :: Proxy TypeParameterRef2))
      , ("TypeParameterRef3", toOCamlType (Proxy :: Proxy TypeParameterRef3))
      , ("TypeParameterRef4", toOCamlType (Proxy :: Proxy TypeParameterRef4))
      , ("TypeParameterRef5", toOCamlType (Proxy :: Proxy TypeParameterRef5))
      ]

    mkOCamlDatatype x =
      case primitiveTypeRepToOCamlPrimitive x of
        Just primitive -> OCamlPrimitive primitive
        Nothing ->
          case Map.lookup aTyConName typeParameterRefMap of
            Just tref -> tref
            Nothing ->
              OCamlDatatype
                (tyConToHaskellTypeMetaData tyc)
                aTyConName
                (OCamlValueConstructor . NamedConstructor aTyConName $ typeRepToOCamlValue x)
      where
        tyc = typeRepTyCon x
        aTyConName = T.pack . show $ tyc

-- OCamlType instances for primitives

instance OCamlType a => OCamlType [a] where
  toOCamlType _ = OCamlPrimitive (OList (toOCamlType (Proxy :: Proxy a)))

instance OCamlType a => OCamlType (Maybe a) where
  toOCamlType _ = OCamlPrimitive (OOption (toOCamlType (Proxy :: Proxy a)))

instance (OCamlType l, OCamlType r) => OCamlType (Either l r) where
  toOCamlType _ = OCamlPrimitive (OEither (toOCamlType (Proxy :: Proxy l)) (toOCamlType (Proxy :: Proxy r)))

instance OCamlType () where
  toOCamlType _ = OCamlPrimitive OUnit

instance OCamlType Text where
  toOCamlType _ = OCamlPrimitive OString

instance OCamlType ByteString where
  toOCamlType _ = OCamlPrimitive OString

instance OCamlType Day where
  toOCamlType _ = OCamlPrimitive ODate

instance OCamlType UTCTime where
  toOCamlType _ = OCamlPrimitive ODate

instance OCamlType Float where
  toOCamlType _ = OCamlPrimitive OFloat

instance OCamlType Double where
  toOCamlType _ = OCamlPrimitive OFloat

instance OCamlType Int8 where
  toOCamlType _ = OCamlPrimitive OInt

instance OCamlType Int16 where
  toOCamlType _ = OCamlPrimitive OInt

instance OCamlType Int32 where
  toOCamlType _ = OCamlPrimitive OInt32

instance OCamlType Int64 where
  toOCamlType _ = OCamlPrimitive OInt

instance OCamlType Word where
  toOCamlType _ = OCamlPrimitive OInt

instance OCamlType Word8 where
  toOCamlType _ = OCamlPrimitive OInt

instance OCamlType Word16 where
  toOCamlType _ = OCamlPrimitive OInt

instance OCamlType Word32 where
  toOCamlType _ = OCamlPrimitive OInt

instance OCamlType Word64 where
  toOCamlType _ = OCamlPrimitive OInt

instance OCamlType Int where
  toOCamlType _ = OCamlPrimitive OInt

instance OCamlType Integer where
  toOCamlType _ = OCamlPrimitive OInt

instance OCamlType Char where
  toOCamlType _ = OCamlPrimitive OChar

instance OCamlType Bool where
  toOCamlType _ = OCamlPrimitive OBool

instance (OCamlType a, OCamlType b) => OCamlType (a, b) where
  toOCamlType _ =
    OCamlPrimitive $
    OTuple2 (toOCamlType (Proxy :: Proxy a)) (toOCamlType (Proxy :: Proxy b))

instance (OCamlType a, OCamlType b, OCamlType c) => OCamlType (a, b, c) where
  toOCamlType _ =
    OCamlPrimitive $
    OTuple3 (toOCamlType (Proxy :: Proxy a)) (toOCamlType (Proxy :: Proxy b))
            (toOCamlType (Proxy :: Proxy c))

instance (OCamlType a, OCamlType b, OCamlType c, OCamlType d) => OCamlType (a, b, c, d) where
  toOCamlType _ =
    OCamlPrimitive $
    OTuple4 (toOCamlType (Proxy :: Proxy a)) (toOCamlType (Proxy :: Proxy b))
            (toOCamlType (Proxy :: Proxy c)) (toOCamlType (Proxy :: Proxy d))

instance (OCamlType a, OCamlType b, OCamlType c, OCamlType d, OCamlType e) => OCamlType (a, b, c, d, e) where
  toOCamlType _ =
    OCamlPrimitive $
    OTuple5 (toOCamlType (Proxy :: Proxy a)) (toOCamlType (Proxy :: Proxy b))
            (toOCamlType (Proxy :: Proxy c)) (toOCamlType (Proxy :: Proxy d))
            (toOCamlType (Proxy :: Proxy e))

instance (OCamlType a, OCamlType b, OCamlType c, OCamlType d, OCamlType e, OCamlType f) => OCamlType (a, b, c, d, e, f) where
  toOCamlType _ =
    OCamlPrimitive $
    OTuple6 (toOCamlType (Proxy :: Proxy a)) (toOCamlType (Proxy :: Proxy b))
            (toOCamlType (Proxy :: Proxy c)) (toOCamlType (Proxy :: Proxy d))
            (toOCamlType (Proxy :: Proxy e)) (toOCamlType (Proxy :: Proxy f))

instance (OCamlType a) =>
         OCamlType (Proxy a) where
  toOCamlType _ = toOCamlType (undefined :: a)

{-
-- ToJSON and FromJSON instances are provided for the following types in aeson
-- not currently defined here
-- Map, LocalTime, ZonedTime, IntSet, CTime, Version, Natural
-- TimeOfDay, NominalDiffTime, Day, DiffTime, UUID, DotNetTime
-- Value, Dual, First, Last, IntMap, Tree, Seq, Vector, HashSet, Proxy
-- Const Tagged, Dual, First, Last, tuple up to length of 15
-}

-- | for any type that does not use the same serialization as Generic Aeson
--   and has a manually written OCaml definition, should manually derive OCamlType
--   using this function for convenience.
-- 
--   instance OCamlType X where
--      toOCamlType _ = typeableToOCamlType (Proxy :: Proxy X)
--

typeableToOCamlType :: forall a. Typeable a => Proxy a -> OCamlDatatype
typeableToOCamlType Proxy =
  OCamlDatatype
    (HaskellTypeMetaData aTyConName aTyConModule aTyConPackage)
    aTyConName
    (OCamlValueConstructor . NamedConstructor aTyConName $ OCamlEmpty)
  where
    aTyCon = typeRepTyCon $ typeRep (Proxy :: Proxy a)
    aTyConName = T.pack . tyConName $ aTyCon
    aTyConModule = T.pack . tyConModule $ aTyCon
    aTyConPackage = T.pack . tyConPackage $ aTyCon

-- | Used to fill the type parameters of proxy types. `Proxy :: Proxy (Maybe TypeParameterRef0)`, `Proxy :: Proxy Either TypeParameterRef0 TypeParameterRef1`. JSON representation is as an Int to simplify the automated tests.
newtype TypeParameterRef0 = TypeParameterRef0 Int deriving (Read, Show, Eq, Generic)
instance Arbitrary TypeParameterRef0 where arbitrary = TypeParameterRef0 <$> arbitrary
instance ToADTArbitrary TypeParameterRef0
instance FromJSON TypeParameterRef0
instance ToJSON TypeParameterRef0
instance OCamlType TypeParameterRef0 where
  toOCamlType _ = OCamlDatatype (HaskellTypeMetaData "a0" "OCaml.BuckleScript.Types" "ocaml-export") "a0" $ OCamlValueConstructor $ NamedConstructor "a0" $ OCamlTypeParameterRef "a0"

-- | Second unique TypeParameterRef.
newtype TypeParameterRef1 = TypeParameterRef1 Int deriving (Read, Show, Eq, Generic)
instance Arbitrary TypeParameterRef1 where arbitrary = TypeParameterRef1 <$> arbitrary
instance ToADTArbitrary TypeParameterRef1
instance FromJSON TypeParameterRef1
instance ToJSON TypeParameterRef1
instance OCamlType TypeParameterRef1 where
  toOCamlType _ = OCamlDatatype (HaskellTypeMetaData "a1" "OCaml.BuckleScript.Types" "ocaml-export") "a1" $ OCamlValueConstructor $ NamedConstructor "a1" $ OCamlTypeParameterRef "a1"

-- | Third unique TypeParameterRef.
data TypeParameterRef2 = TypeParameterRef2 Int deriving (Read, Show, Eq, Generic)
instance Arbitrary TypeParameterRef2 where arbitrary = TypeParameterRef2 <$> arbitrary
instance ToADTArbitrary TypeParameterRef2
instance FromJSON TypeParameterRef2
instance ToJSON TypeParameterRef2
instance OCamlType TypeParameterRef2 where
  toOCamlType _ = OCamlDatatype (HaskellTypeMetaData "a2" "OCaml.BuckleScript.Types" "ocaml-export") "a2" $ OCamlValueConstructor $ NamedConstructor "a2" $ OCamlTypeParameterRef "a2"

-- | Fourth unique TypeParameterRef.
data TypeParameterRef3 = TypeParameterRef3 Int deriving (Read, Show, Eq, Generic)
instance Arbitrary TypeParameterRef3 where arbitrary = TypeParameterRef3 <$> arbitrary
instance ToADTArbitrary TypeParameterRef3
instance FromJSON TypeParameterRef3
instance ToJSON TypeParameterRef3
instance OCamlType TypeParameterRef3 where
  toOCamlType _ = OCamlDatatype (HaskellTypeMetaData "a3" "OCaml.BuckleScript.Types" "ocaml-export") "a3" $ OCamlValueConstructor $ NamedConstructor "a3" $ OCamlTypeParameterRef "a3"

-- | Fifth unique TypeParameterRef.
data TypeParameterRef4 = TypeParameterRef4 Int deriving (Read, Show, Eq, Generic)
instance Arbitrary TypeParameterRef4 where arbitrary = TypeParameterRef4 <$> arbitrary
instance ToADTArbitrary TypeParameterRef4
instance FromJSON TypeParameterRef4
instance ToJSON TypeParameterRef4
instance OCamlType TypeParameterRef4 where
  toOCamlType _ = OCamlDatatype (HaskellTypeMetaData "a4" "OCaml.BuckleScript.Types" "ocaml-export") "a4" $ OCamlValueConstructor $ NamedConstructor "a4" $ OCamlTypeParameterRef "a4"

-- | Sixth unique TypeParameterRef.
data TypeParameterRef5 = TypeParameterRef5 Int deriving (Read, Show, Eq, Generic)
instance Arbitrary TypeParameterRef5 where arbitrary = TypeParameterRef5 <$> arbitrary
instance ToADTArbitrary TypeParameterRef5
instance FromJSON TypeParameterRef5
instance ToJSON TypeParameterRef5
instance OCamlType TypeParameterRef5 where
  toOCamlType _ = OCamlDatatype (HaskellTypeMetaData "a5" "OCaml.BuckleScript.Types" "ocaml-export") "a5" $ OCamlValueConstructor $ NamedConstructor "a5" $ OCamlTypeParameterRef "a5"

-- Utility functions

-- | Whether a set of constructors is an enumeration, i.e. whether they lack
--   values. data A = A | B | C would be simple data A = A Int | B | C would not
--   be simple.
isEnumeration :: OCamlConstructor -> Bool
isEnumeration (OCamlValueConstructor (NamedConstructor _ OCamlEmpty)) = True
isEnumeration (OCamlValueConstructor (MultipleConstructors cs)) = all isEnumeration (OCamlValueConstructor <$> cs)
isEnumeration _ = False


-- | Tranform an OCamlConstructor to EnumeratorConstructors 
transformToEnumeration :: OCamlConstructor -> OCamlConstructor
transformToEnumeration (OCamlValueConstructor (NamedConstructor name OCamlEmpty)) =
  OCamlEnumeratorConstructor [EnumeratorConstructor name]

transformToEnumeration (OCamlValueConstructor (MultipleConstructors cs)) =
  -- wrap cs in OCamlValueConstructor so the type matches
  -- getEnumeratorConstructor to make sure it only returns OCamlEnumeratorConstructor
  -- then concat the results and rewrap it in OCamlEnumeratorConstructor
  OCamlEnumeratorConstructor . concat . catMaybes
    $ getEnumeratorConstructor . transformToEnumeration . OCamlValueConstructor
      <$> cs
  where
    getEnumeratorConstructor constructor =
      case constructor of
        (OCamlEnumeratorConstructor c) -> Just c
        _ -> Nothing

transformToEnumeration cs = cs

-- | transform a OCamlConstructor to OCamlSumOfRecordConstructor
transformToSumOfRecord :: Text -> OCamlConstructor -> OCamlConstructor
transformToSumOfRecord typeName (OCamlValueConstructor value@(MultipleConstructors _cs)) = OCamlSumOfRecordConstructor typeName value
transformToSumOfRecord _ constructor = constructor


-- | Haskell allows you to directly declare a sum of records,
-- i.e. data A = A {a :: Int} | B {b :: String}. This does not exist in
-- OCaml so we have to work around it.

isSumWithRecord :: OCamlConstructor -> Bool
isSumWithRecord (OCamlValueConstructor (MultipleConstructors cs)) =
  -- if there is only one constructor then it is not a SumWithRecords.
  -- if there are multiple constructors and at least one is a record constructor
  -- then it is a SumWithRecords
  (\x -> length x > 1 && or x) $ isSumWithRecordsAux . OCamlValueConstructor <$> cs
  where
    isSumWithRecordsAux :: OCamlConstructor -> Bool
    isSumWithRecordsAux (OCamlValueConstructor (MultipleConstructors cs')) = or $ isSumWithRecordsAux . OCamlValueConstructor <$> cs'
    isSumWithRecordsAux (OCamlValueConstructor (RecordConstructor _ _)) = True
    isSumWithRecordsAux _ = False
isSumWithRecord _ = False

-- | Convert OCamlValues to the type parameter names of a data type.
--   `Either a0 a1` -> `["a0","a1"]`
getTypeParameterRefNames :: [OCamlValue] -> [Text]
getTypeParameterRefNames = nub . concat . (fmap match)
  where
    lift (OCamlDatatype _ _ constructor) = getTypeParameters constructor
    lift _ = []

    match value =
      case value of
        (OCamlRefApp typRep _) -> getTypeParameterRefNameForTypeRep typRep
        (OCamlTypeParameterRef name) -> [name]
        (Values v1 v2) -> match v1 ++ match v2
        (OCamlField _ v1) -> match v1
        (OCamlPrimitiveRef (OList v1)) -> lift v1
        (OCamlPrimitiveRef (OOption v1)) -> lift v1
        (OCamlPrimitiveRef (OTuple2 v1 v2)) -> lift v1 ++ lift v2
        (OCamlPrimitiveRef (OTuple3 v1 v2 v3)) -> lift v1 ++ lift v2 ++ lift v3
        (OCamlPrimitiveRef (OTuple4 v1 v2 v3 v4)) -> lift v1 ++ lift v2 ++ lift v3 ++ lift v4
        (OCamlPrimitiveRef (OTuple5 v1 v2 v3 v4 v5)) -> lift v1 ++ lift v2 ++ lift v3 ++ lift v4 ++ lift v5
        (OCamlPrimitiveRef (OTuple6 v1 v2 v3 v4 v5 v6)) -> lift v1 ++ lift v2 ++ lift v3 ++ lift v4 ++ lift v5 ++ lift v6
        _ -> []

-- | getOCamlValues flatten the values from MultipleConstructors into a list and remove ValueConstructor.
getOCamlValues :: ValueConstructor -> [OCamlValue]
getOCamlValues (NamedConstructor     _ value) = [value]
getOCamlValues (RecordConstructor    _ value) = [value]
getOCamlValues (MultipleConstructors cs)      = concat $ getOCamlValues <$> cs

-- | get all of the type parameters from an OCamlConstructor.
getTypeParameters :: OCamlConstructor -> [Text]
getTypeParameters (OCamlValueConstructor vc) = getTypeParameterRefNames . getOCamlValues $ vc
getTypeParameters (OCamlSumOfRecordConstructor _ vc) = getTypeParameterRefNames . getOCamlValues $ vc
getTypeParameters _ = []

-- | Matches all of the TypeParameterRefs (TypeParameterRef0 to TypeParameterRef5).
--   This function is needed to work around the tree structure for special rules for rendering type parameters.
isTypeParameterRef :: OCamlDatatype -> Bool
isTypeParameterRef (OCamlDatatype _ _ (OCamlValueConstructor (NamedConstructor _ (OCamlTypeParameterRef _)))) = True
isTypeParameterRef _ = False

-- | When there is a record that has its type parameters partially filled, it will should have TypeParameterRef0-5 as the
--   unfilled type parameters. This function properly pushes the TypeParameterRef0-5 to the type signature of an OCaml
--   type.
getTypeParameterRefNameForTypeRep :: TypeRep -> [Text]
getTypeParameterRefNameForTypeRep t =
  if length rst == 0
  then typeParamterRefText
  else typeParamterRefText <> concat (getTypeParameterRefNameForTypeRep <$> rst)
  where
  (hd,rst) = splitTyConApp $ t
  typeParamterRefText =
    case Map.lookup hd typeParameterRefTyConToOCamlTypeText of
      Just typeParamterRefText' -> [typeParamterRefText']
      Nothing -> []

-- | Make OCaml module prefix for a value based on the declaration's and parameter's meta data.
mkModulePrefix :: OCamlTypeMetaData -> OCamlTypeMetaData -> Text
mkModulePrefix (OCamlTypeMetaData _ decModules decSubModules) (OCamlTypeMetaData _ parModules parSubModules) =
  if prefix /= "" then prefix <> "." else ""
  where
    (l,r) = zipWithRightRemainder (decModules <> decSubModules) (parModules <> parSubModules)
    prefix = T.intercalate "." $ (removeMatchingHead l) <> r

-- | Iterate through the beginning of a list, remove values as long as they are equal.
--   When one inequality is found, return the value and its tail.
removeMatchingHead :: Eq a => [(a,a)] -> [a]
removeMatchingHead [] = []
removeMatchingHead (hd:tl) =
  if fst hd == snd hd
  then removeMatchingHead tl
  else [snd hd] <> (snd <$> tl)

-- | Zip two lists. If the right hand side is longer, then return the remaining right side.
zipWithRightRemainder :: [a] -> [b] -> ([(a,b)], [b])
zipWithRightRemainder [] bs = ([], bs)
zipWithRightRemainder _ab [] = ([], [])
zipWithRightRemainder (a:as) (b:bs) = ([(a,b)], []) <> zipWithRightRemainder as bs

-- | BuckleScript has a 'float' type that conflicts when you do 'open Aeson.Decode'
--   float must be appended with 'Aeson.Decode'. 
oCamlValueIsFloat :: OCamlValue -> Bool
oCamlValueIsFloat (OCamlPrimitiveRef OFloat) = True
oCamlValueIsFloat _ = False


-- Typeable related functions
-- when a row is a type with type parameters and those type parameters are filled,
-- we need a way to extract what those type parameters are. This is not possible with
-- Generics, but it can be done with Typeable.

-- | match type parameter reference 'TyCon's (accessible from a TypeRep) to their equivalent OCaml types.
typeParameterRefTyConToOCamlTypeText :: Map.Map TyCon Text
typeParameterRefTyConToOCamlTypeText = Map.fromList
  [ ( typeRepTyCon $ typeRep (Proxy :: Proxy TypeParameterRef0), "a0")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy TypeParameterRef1), "a1")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy TypeParameterRef2), "a2")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy TypeParameterRef3), "a3")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy TypeParameterRef4), "a4")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy TypeParameterRef5), "a5")
  ]

-- | convert TypeRep to HaskellTypeMetaData
typeRepToHaskellTypeMetaData :: TypeRep -> HaskellTypeMetaData
typeRepToHaskellTypeMetaData = tyConToHaskellTypeMetaData . typeRepTyCon

-- | convert TyCon to HaskellTypeMetaData
tyConToHaskellTypeMetaData :: TyCon -> HaskellTypeMetaData
tyConToHaskellTypeMetaData aTypeCon =
  HaskellTypeMetaData
    (T.pack . tyConName    $ aTypeCon)
    (T.pack . tyConModule  $ aTypeCon)
    (T.pack . tyConPackage $ aTypeCon)

{-
-- | match 'TyCon's (accessible from a TypeRep) to their equivalent OCaml types.
primitiveTyConToOCamlTypeText :: Map.Map TyCon Text
primitiveTyConToOCamlTypeText = Map.fromList
  [ ( typeRepTyCon $ typeRep (Proxy :: Proxy []        ), "list")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Maybe     ), "option")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Either    ), "either")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy ()        ), "unit")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Text      ), "string")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy ByteString), "string")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Day       ), "Js_date.t")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy UTCTime   ), "Js_date.t")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Float     ), "float")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Double    ), "float")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Int8      ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Int16     ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Int32     ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Int64     ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Int       ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Integer   ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Word      ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Word8     ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Word16    ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Word32    ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Word64    ), "int")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Char      ), "string")
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy Bool      ), "boolean")
  ]

-- | convert a TyCon of a tuple to its size
tupleTyConToSize :: Map.Map TyCon Int
tupleTyConToSize = Map.fromList
  [ ( typeRepTyCon $ typeRep (Proxy :: Proxy (,)      ), 2)
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy (,,)     ), 3)
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy (,,,)    ), 4)
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy (,,,,)   ), 5)
  , ( typeRepTyCon $ typeRep (Proxy :: Proxy (,,,,,)  ), 6)
  ]

-- | necessary because the TypeRep for 'String' is '([], [Char])', but we want
--   it to be reduced to an OCaml 'string'.
typeRepIsString :: TypeRep -> Bool
typeRepIsString t =
  let (hd, rst) = splitTyConApp t in
  show hd == "[]" && length rst == 1 && ((show $ head rst) == "Char")

typeParameterToRef :: Map.Map TypeRep Text
typeParameterToRef = Map.fromList
  [ ( typeRep (Proxy :: Proxy TypeParameterRef0), "a0")
  , ( typeRep (Proxy :: Proxy TypeParameterRef1), "a1")
  , ( typeRep (Proxy :: Proxy TypeParameterRef2), "a2")
  , ( typeRep (Proxy :: Proxy TypeParameterRef3), "a3")
  , ( typeRep (Proxy :: Proxy TypeParameterRef4), "a4")
  , ( typeRep (Proxy :: Proxy TypeParameterRef5), "a5")
  ]

ocamlDatatypeHasTypeParameter :: OCamlDatatype -> Int -> Bool
ocamlDatatypeHasTypeParameter ocamlDatatype index = ocamlDatatypeHasTypeParameter' ocamlDatatype
  where
    typeParameter = OCamlTypeParameterRef $ "a" <> (T.pack . show $ index)
    
    ocamlDatatypeHasTypeParameter' :: OCamlDatatype -> Bool
    ocamlDatatypeHasTypeParameter' (OCamlDatatype _ _ ocamlConstructor) = ocamlConstructorHasTypeParameter ocamlConstructor
    ocamlDatatypeHasTypeParameter' (OCamlPrimitive ocamlPrimitive) = ocamlPrimitiveHasTypeParameter ocamlPrimitive

    ocamlPrimitiveHasTypeParameter :: OCamlPrimitive -> Bool
    ocamlPrimitiveHasTypeParameter (OList d0) = ocamlDatatypeHasTypeParameter' d0
    ocamlPrimitiveHasTypeParameter _ = False

    ocamlConstructorHasTypeParameter :: OCamlConstructor -> Bool
    ocamlConstructorHasTypeParameter (OCamlValueConstructor valueConstructor) = valueConstructorHasTypeParameter valueConstructor
    ocamlConstructorHasTypeParameter (OCamlSumOfRecordConstructor _ valueConstructor) = valueConstructorHasTypeParameter valueConstructor
    ocamlConstructorHasTypeParameter _ = False

    valueConstructorHasTypeParameter :: ValueConstructor -> Bool
    valueConstructorHasTypeParameter (NamedConstructor _ ocamlValue) = ocamlValueHasTypeParameter ocamlValue
    valueConstructorHasTypeParameter (RecordConstructor _ ocamlValue) = ocamlValueHasTypeParameter ocamlValue
    valueConstructorHasTypeParameter (MultipleConstructors ocamlValues) = or $ valueConstructorHasTypeParameter <$> ocamlValues

    ocamlValueHasTypeParameter :: OCamlValue -> Bool
    ocamlValueHasTypeParameter (OCamlPrimitiveRef ocamlPrimitive) = ocamlPrimitiveHasTypeParameter ocamlPrimitive
    ocamlValueHasTypeParameter o@(OCamlTypeParameterRef _) = o == typeParameter
    ocamlValueHasTypeParameter (OCamlField _ v) = ocamlValueHasTypeParameter v
    ocamlValueHasTypeParameter (Values v0 v1) = ocamlValueHasTypeParameter v0 || ocamlValueHasTypeParameter v1
    ocamlValueHasTypeParameter _ = False
-}