{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE PolyKinds #-}
-- |
-- This module provides functions to modify 'Symbol's and type-level lists.
--
-- The workflow to define a modification function can be as follows.
--
-- 1. Use 'ToList' to convert a 'Symbol' to a type-level list of 'Char's.
--
-- 1. Use functions from here and from the package [first-class-families](https://hackage.haskell.org/package/first-class-families) for type-level lists (@Fcf.Data.List@).
--
-- 1. Use 'FromList' to convert a type-level list of 'Char's back to a 'Symbol'.
--
-- Example (see 'DropPrefix'):
--
-- >>> :kind! DropPrefix "_userParams_category"
-- DropPrefix "_userParams_category" :: Symbol
-- = "category"
--
-- Modules that use the 'Eval' type family (e.g., "Servant.Record") must be imported together with modules that export instances of 'Eval'
-- (see the @GHC@ documentation on Type families).
module Servant.TypeLevel
  ( -- * Re-exports from @first-class-families@
    Eval,
    Exp,

    -- * List functions
    FromList,
    FromList1,
    ToList,
    ToList1,

    -- * Comparison functions
    TyEq,
    NotTyEq,

    -- * Examples
    DropUnderscores,
    DropNonUnderscores,
    DropPrefix,
  )
where

import Fcf
import Fcf.Data.List
import GHC.TypeLits

-- | Convert a 'Symbol' to a list of 'Char's.
type family ToList (sym :: Symbol) :: [Char] where
  ToList sym = ToList1 (UnconsSymbol sym)

-- | Convert a possibly unconsed 'Symbol' to a list of 'Char's.
type family ToList1 (sym :: Maybe (Char, Symbol)) :: [Char] where
  ToList1 'Nothing = '[]
  ToList1 ('Just '(c, sym)) = c : ToList1 (UnconsSymbol sym)

-- | Convert a list of 'Char's to a 'Symbol'.
--
-- >>> :kind! FromList ['a', '+', 'c']
-- FromList ['a', '+', 'c'] :: Symbol
-- = "a+c"
type family FromList (cs :: [Char]) :: Symbol where
  FromList cs = FromList1 (Eval (Reverse cs)) ""

-- | Convert a list of 'Char's to a 'Symbol'.
--
-- In this list, 'Chars' go in a reverse order.
--
-- >>> :kind! FromList1 ['a', 'b', 'c'] ""
-- FromList1 ['a', 'b', 'c'] "" :: Symbol
-- = "cba"
type family FromList1 (syms :: [Char]) (sym :: Symbol) :: Symbol where
  FromList1 '[] s = s
  FromList1 (x : xs) s = FromList1 xs (ConsSymbol x s)

-- $ lists

-- | Type inequality
data NotTyEq :: a -> b -> Exp Bool

type instance Eval (NotTyEq a b) = NotTyEqImpl a b

-- | Check types aren't equal
type family NotTyEqImpl (a :: k) (b :: k) :: Bool where
  NotTyEqImpl a a = 'False
  NotTyEqImpl a b = 'True

-- $ examples

-- | Drop leading underscores.
--
-- >>> :kind! Eval (DropUnderscores ['_', '_', 'a'])
-- Eval (DropUnderscores ['_', '_', 'a']) :: [Char]
-- = '['a']
type DropUnderscores = DropWhile (TyEq '_')

-- | Drop leading non-underscores.
--
-- >>> :kind! Eval (DropNonUnderscores ['a', 'a', '_'])
-- Eval (DropNonUnderscores ['a', 'a', '_']) :: [Char]
-- = '['_']
type DropNonUnderscores = DropWhile (NotTyEq '_')

-- | Drop the prefix of a 'Symbol'.
--
-- >>> :kind! DropPrefix "_userParams_category"
-- DropPrefix "_userParams_category" :: Symbol
-- = "category"
type family DropPrefix (sym :: Symbol) :: Symbol where
  DropPrefix sym = FromList (Eval (DropUnderscores (Eval (DropNonUnderscores (Eval (DropUnderscores (ToList sym)))))))