{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}

module Data.Morpheus.Types.Internal.AST.Name
  ( Name,
    packName,
    unpackName,
    FieldName,
    TypeName,
    unitTypeName,
    unitFieldName,
    isNotSystemTypeName,
    isNotSystemFieldName,
    intercalate,
    NAME (..),
    FragmentName,
    isValidName,
  )
where

import Data.Aeson
  ( FromJSON,
    ToJSON (..),
  )
#if MIN_VERSION_aeson(2,0,0)
import Data.Aeson.Key (Key)
import qualified Data.Aeson.Key as A
#endif

#if MIN_VERSION_template_haskell(2,17,0)
import Language.Haskell.TH
  ( Quote,
    Code,
    unsafeCodeCoerce,
    stringE
  )
import Language.Haskell.TH.Syntax(
    Lift(..)
  )
# else
import Language.Haskell.TH
  ( stringE,
  )
import Language.Haskell.TH.Syntax
  ( Lift (..),
    Q,
    TExp,
    unsafeTExpCoerce,
  )
#endif
import Data.Char (isLetter, isNumber)
import qualified Data.List as L
import Data.Morpheus.Rendering.RenderGQL
  ( RenderGQL (..),
    fromText,
    renderGQL,
  )
import Data.Morpheus.Types.Internal.AST.Error
  ( Msg (..),
  )
import qualified Data.Text as T
import qualified Language.Haskell.TH.Syntax as TH
import Relude hiding
  ( ByteString,
    decodeUtf8,
    intercalate,
  )

data NAME
  = TYPE
  | FIELD
  | FRAGMENT

newtype Name (t :: NAME) = Name {_unpackName :: Text}
  deriving
    (Generic)
  deriving newtype
    ( Show,
      Ord,
      Eq,
      IsString,
      ToString,
      Hashable,
      Semigroup,
      FromJSON,
      ToJSON
    )

instance Msg (Name t) where
  msg name = msg $ "\"" <> _unpackName name <> "\""

isValidName :: Name t -> Bool
isValidName n = T.all isStart (T.take 1 name) && T.all isContinue (T.drop 1 name)
  where
    name = unpackName n
    isStart c = c == '_' || isLetter c
    isContinue c = isStart c || isNumber c

class NamePacking a where
  packName :: a -> Name t
  unpackName :: Name t -> a

instance NamePacking TH.Name where
  packName (TH.Name name _) = Name $ T.pack (occName name)
    where
      occName (TH.OccName x) = maybe x (takeWhile (/= ':')) (L.stripPrefix "$sel:" x)
  unpackName = TH.mkName . toString . _unpackName

instance NamePacking Text where
  packName = Name
  unpackName = _unpackName

#if MIN_VERSION_aeson(2,0,0)
instance NamePacking Key where
  packName = Name . A.toText
  unpackName = A.fromText . _unpackName
#endif

instance Lift (Name t) where
  lift = stringE . T.unpack . unpackName

#if MIN_VERSION_template_haskell(2,17,0)
  liftTyped = liftTypedString . unpackName
    where
      liftTypedString :: (Quote m) => Text -> Code m (Name t)
      liftTypedString = unsafeCodeCoerce . stringE . T.unpack
      {-# INLINE liftTypedString #-}
#elif MIN_VERSION_template_haskell(2,16,0)
  liftTyped = liftTypedString . unpackName
    where
      liftTypedString :: IsString a => Text -> Q (TExp a)
      liftTypedString = unsafeTExpCoerce . stringE . T.unpack
      {-# INLINE liftTypedString #-}
#endif

instance RenderGQL (Name a) where
  renderGQL = fromText . unpackName

type FieldName = Name 'FIELD

type TypeName = Name 'TYPE

type FragmentName = Name 'FRAGMENT

intercalate :: Name t1 -> [Name t2] -> Name t3
intercalate (Name x) = Name . T.intercalate x . fmap unpackName
{-# INLINE intercalate #-}

unitTypeName :: TypeName
unitTypeName = "Unit"
{-# INLINE unitTypeName #-}

unitFieldName :: FieldName
unitFieldName = "_"
{-# INLINE unitFieldName #-}

isNotSystemTypeName :: TypeName -> Bool
isNotSystemTypeName =
  ( `notElem`
      [ "__Schema",
        "__Type",
        "__Directive",
        "__TypeKind",
        "__Field",
        "__DirectiveLocation",
        "__InputValue",
        "__EnumValue",
        "String",
        "Float",
        "Int",
        "Boolean",
        "ID"
      ]
  )
{-# INLINE isNotSystemTypeName #-}

isNotSystemFieldName :: FieldName -> Bool
isNotSystemFieldName =
  ( `notElem`
      [ "__typename",
        "__schema",
        "__type"
      ]
  )
{-# INLINE isNotSystemFieldName #-}
