-- SPDX-FileCopyrightText: 2022 Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
--
-- SPDX-License-Identifier: AGPL-3.0-or-later

module Data.Universe.TH
  ( finiteEnum
  , deriveUniverse
  , deriveFinite
  ) where

import Prelude

import Language.Haskell.TH
import Language.Haskell.TH.Datatype

import Data.Universe
import Data.Universe.Helpers (interleave)

import Control.Monad (unless)

import Data.List (elemIndex)
import Data.Containers.ListUtils
  
import Control.Lens hiding (universe)
import Data.Generics.Product.Types


-- | Get type var bind name
--
-- Stolen from https://hackage.haskell.org/package/template-haskell-util-0.1.1.0
getTVBName :: TyVarBndr -> Name
getTVBName (PlainTV  name  ) = name
getTVBName (KindedTV name _) = name



finiteEnum :: Name -> DecsQ
-- ^ Declare generic `Enum`- and `Bounded`-Instances given `Finite`- and `Eq`-Instances
finiteEnum tName = do
  DatatypeInfo{..} <- reifyDatatype tName

  let datatype = foldl appT (conT datatypeName) $ map (varT . getTVBName) datatypeVars
      tUniverse = [e|universeF :: [$(datatype)]|]

  [d|
    instance Bounded $(datatype) where
      minBound = head $(tUniverse)
      maxBound = last $(tUniverse)

    instance Enum $(datatype) where
      toEnum n
        | n >= 0
        , n < length $(tUniverse)
        = $(tUniverse) !! n
        | otherwise = error $ "toEnum " ++ $(stringE $ nameBase tName) ++ ": out of bounds"
      fromEnum = fromMaybe (error $ "fromEnum " ++ $(stringE $ nameBase tName) ++ ": invalid `universeF`") . flip elemIndex $(tUniverse)

      enumFrom x = map toEnum [fromEnum x .. fromEnum (maxBound :: $(datatype))]
      enumFromThen x y = map toEnum [fromEnum x, fromEnum y .. fromEnum (maxBound :: $(datatype))]
    |]

deriveUniverse, deriveFinite :: Name -> DecsQ
deriveUniverse tName = view _1 <$> deriveUniverse' [e|interleave|] [e|universe|] ([t|Universe|] `appT`) tName
deriveFinite tName = do
  (decs, iCxt) <- deriveUniverse' [e|concat|] [e|universeF|] ([t|Finite|] `appT`) tName
  fmap concat . sequence $
    [ pure decs
    , do
        DatatypeInfo{..} <- reifyDatatype tName
        pure <$> instanceD (pure iCxt) (appT [t|Finite|] . foldl appT (conT datatypeName) $ map (varT . getTVBName) datatypeVars) []
    ]

deriveUniverse' :: ExpQ -> ExpQ -> (TypeQ -> TypeQ) -> Name -> Q ([Dec], Cxt)
deriveUniverse' interleaveExp universeExp mkCxt tName = do
  DatatypeInfo{..} <- reifyDatatype tName

  let consUniverse ConstructorInfo{..} = do
        unless (null constructorVars) $
          fail "Constructors with variables no supported"

        foldl (\f t -> [e|ap|] `appE` f `appE` sigE universeExp (listT `appT` t)) [e|pure $(conE constructorName)|] $ map pure constructorFields

      typ = foldl (\t bndr -> t `appT` varT (getTVBName bndr)) (conT tName) datatypeVars
      iCxt = map (mkCxt . pure) $ filter (\t -> any (flip (elemOf types) t) usedTVars) fieldTypes
        where usedTVars = filter (\n -> any (`usesVar` n) datatypeCons) $ map getTVBName datatypeVars
              usesVar ConstructorInfo{..} n
                | n `elem` map getTVBName constructorVars = False
                | otherwise = any (elemOf types n) constructorFields
              fieldTypes = nubOrd $ concatMap constructorFields datatypeCons

  iCxt' <- cxt iCxt

  (, iCxt') . pure <$> instanceD (pure iCxt') [t|Universe $(typ)|]
    [ funD 'universe
      [ clause [] (normalB . appE interleaveExp . listE $ map consUniverse datatypeCons) []
      ]
    ]
