{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE Trustworthy #-}

-- | Generic implementation of Read
--
-- === Warning
--
-- This is an internal module: it is not subject to any versioning policy,
-- breaking changes can happen at any time.
--
-- If something here seems useful, please report it or create a pull request to
-- export it from an external module.

module Generic.Data.Internal.Read where

import Data.Coerce
import Data.Functor.Classes (Read1(..))
import Data.Functor.Identity
import Data.Proxy
import Generic.Data.Internal.Utils (isSymDataCon, isSymVar)
import GHC.Generics hiding (prec)
import GHC.Read (expectP, list)
import GHC.Show (appPrec, appPrec1)
import Text.ParserCombinators.ReadPrec
import Text.Read (Read(..), parens)
import Text.Read.Lex (Lexeme(..))

-- | Generic 'readPrec'.
--
-- @
-- instance 'Read' MyType where
--   'readPrec' = 'Text.Read.greadPrec'
--   'readListPrec' = 'Text.Read.readListPrecDefault'
-- @
greadPrec :: (Generic a, GRead0 (Rep a)) => ReadPrec a
greadPrec = to <$> gPrecRead Proxy

-- | Generic representation of 'Read' types.
type GRead0 = GRead Proxy

-- | Generic 'liftReadPrec'.
gliftReadPrec
  :: (Generic1 f, GRead1 (Rep1 f))
  => ReadPrec a -> ReadPrec [a]
  -> ReadPrec (f a)
gliftReadPrec readPrec' readList' =
  to1 <$> gPrecRead (Identity (readPrec', readList'))

-- | Generic representation of 'Data.Functor.Classes.Read1' types.
type GRead1 = GRead Identity

class GRead p f where
  gPrecRead :: p (ReadPrec a, ReadPrec [a]) -> ReadPrec (f a)

instance (GRead p f, IsNullaryDataType f) => GRead p (M1 D d f) where
  gPrecRead p = coerceM1 (parensIfNonNullary (gPrecRead p))
    where
      x :: f a
      x = undefined

      parensIfNonNullary :: ReadPrec a -> ReadPrec a
      parensIfNonNullary = if isNullaryDataType x
                              then id
                              else parens

instance (GRead p f, GRead p g) => GRead p (f :+: g) where
  gPrecRead p = fmap L1 (gPrecRead p) +++ fmap R1 (gPrecRead p)

instance (Constructor c, GReadC p c f) => GRead p (M1 C c f) where
  gPrecRead p = gPrecReadC p (conName x) (conFixity x)
    where
      x :: M1 C c f a
      x = undefined

instance GRead p V1 where
  gPrecRead _ = pfail

class IsNullaryDataType f where
  isNullaryDataType :: f a -> Bool

instance IsNullaryDataType (f :+: g) where
  isNullaryDataType _ = False

instance IsNullaryDataType (C1 c f) where
  isNullaryDataType _ = False

instance IsNullaryDataType V1 where
  isNullaryDataType _ = True

class GReadC p c f where
  gPrecReadC :: p (ReadPrec a, ReadPrec [a]) -> String -> Fixity -> ReadPrec (M1 C c f a)

instance GReadFields p f => GReadC p ('MetaCons s y 'False) f where
  gPrecReadC :: forall a. p (ReadPrec a, ReadPrec [a]) -> String -> Fixity
             -> ReadPrec (M1 C ('MetaCons s y 'False) f a)
  gPrecReadC p name fixity
    | Infix _ fy <- fixity, Branch k1 k2 <- fields
    = coerceM1 $ prec fy $ do
        k1' <- toReadPrec k1
        if isSymDataCon name
           then expectP (Symbol name)
           else mapM_ expectP ([Punc "`"] ++ identHLexemes name ++ [Punc "`"])
        k2' <- toReadPrec k2
        pure (k1' :*: k2')
    | otherwise
    = coerceM1 $ prec appPrec $ do
        readPrefixCon name
        toReadPrec fields
    where
      fields :: ReadPrecTree (f a)
      fields = gPrecReadFields p

instance GReadNamed p f => GReadC p ('MetaCons s y 'True) f where
  gPrecReadC p name _fixity = coerceM1 $ prec appPrec1 $ do
    readPrefixCon name
    readSurround '{' fields '}'
    where
      fields = gPrecReadNamed p

class GReadFields p f where
  gPrecReadFields :: p (ReadPrec a, ReadPrec [a]) -> ReadPrecTree (f a)

instance (GReadFields p f, GReadFields p g) => GReadFields p (f :*: g) where
  gPrecReadFields p = Branch (gPrecReadFields p) (gPrecReadFields p)

instance GReadSingle p f => GReadFields p (M1 S c f) where
  gPrecReadFields p = M1Leaf (step (gPrecReadSingle p))

instance GReadFields p U1 where
  gPrecReadFields _ = U1Leaf

class GReadNamed p f where
  gPrecReadNamed :: p (ReadPrec a, ReadPrec [a]) -> ReadPrec (f a)

instance (GReadNamed p f, GReadNamed p g) => GReadNamed p (f :*: g) where
  gPrecReadNamed p = do
    l <- gPrecReadNamed p
    expectP (Punc ",")
    r <- gPrecReadNamed p
    pure (l :*: r)

instance (Selector c, GReadSingle p f) => GReadNamed p (M1 S c f) where
  gPrecReadNamed p = coerceM1 $ do
    mapM_ expectP snameLexemes
    expectP (Punc "=")
    reset (gPrecReadSingle p)
    where
      x :: M1 S c f a
      x = undefined

      sname :: String
      sname = selName x

      snameLexemes :: [Lexeme]
      snameLexemes | isSymVar sname
                   = [Punc "(", Symbol sname, Punc ")"]
                   | otherwise
                   = identHLexemes sname

instance GReadNamed p U1 where
  gPrecReadNamed _ = pure U1

class GReadSingle p f where
  gPrecReadSingle :: p (ReadPrec a, ReadPrec [a]) -> ReadPrec (f a)

instance Read a => GReadSingle p (K1 i a) where
  gPrecReadSingle _ = coerceK1 readPrec
    where
      coerceK1 :: ReadPrec a -> ReadPrec (K1 i a x)
      coerceK1 = coerce

instance Read1 f => GReadSingle Identity (Rec1 f) where
  gPrecReadSingle (Identity p) = coerceRec1 (liftReadPrecCompat p)
    where
      coerceRec1 :: ReadPrec (f a) -> ReadPrec (Rec1 f a)
      coerceRec1 = coerce

instance GReadSingle Identity Par1 where
  gPrecReadSingle (Identity (readPrec', _)) = coercePar1 readPrec'
    where
      coercePar1 :: ReadPrec p -> ReadPrec (Par1 p)
      coercePar1 = coerce

instance (Read1 f, GReadSingle p g) => GReadSingle p (f :.: g) where
  gPrecReadSingle :: forall a. p (ReadPrec a, ReadPrec [a]) -> ReadPrec ((f :.: g) a)
  gPrecReadSingle p = coerceComp1 (liftReadPrecCompat (readPrec_, readList_))
    where
      readPrec_ :: ReadPrec (g a)
      readPrec_ = gPrecReadSingle p

      readList_ :: ReadPrec [g a]
      readList_ = list readPrec_

      coerceComp1 :: ReadPrec (f (g a)) -> ReadPrec ((f :.: g) a)
      coerceComp1 = coerce

-- Helpers

coerceM1 :: ReadPrec (f p) -> ReadPrec (M1 i c f p)
coerceM1 = coerce

-- | A backwards-compatible version of 'liftReadPrec'. This is needed for
-- compatibility with @base-4.9@, where 'Read1' only offers 'liftReadsPrec',
-- not 'liftReadPrec'.
liftReadPrecCompat :: Read1 f => (ReadPrec a, ReadPrec [a]) -> ReadPrec (f a)
liftReadPrecCompat (readPrec', readList') =
#if MIN_VERSION_base(4,10,0)
    liftReadPrec readPrec' readList'
#else
    readS_to_Prec (liftReadsPrec (readPrec_to_S readPrec')
                                 (readPrec_to_S readList' 0))
#endif

data ReadPrecTree a where
  U1Leaf :: ReadPrecTree (U1 a)
  M1Leaf :: ReadPrec (f a) -> ReadPrecTree (M1 i c f a)
  Branch :: ReadPrecTree (f a) -> ReadPrecTree (g a) -> ReadPrecTree ((f :*: g) a)

toReadPrec :: ReadPrecTree a -> ReadPrec a
toReadPrec U1Leaf       = pure U1
toReadPrec (M1Leaf f)   = coerceM1 f
toReadPrec (Branch f g) = (:*:) <$> toReadPrec f <*> toReadPrec g

identHLexemes :: String -> [Lexeme]
identHLexemes s | Just (ss, '#') <- snocView s = [Ident ss, Symbol "#"]
                | otherwise                    = [Ident s]

readPrefixCon :: String -> ReadPrec ()
readPrefixCon name
  | isSymDataCon name
  = readSurround '(' (expectP (Symbol name)) ')'
  | otherwise
  = mapM_ expectP (identHLexemes name)

readSurround :: Char -> ReadPrec a -> Char -> ReadPrec a
readSurround c1 r c2 = do
  expectP (Punc [c1])
  r' <- r
  expectP (Punc [c2])
  pure r'

-- Split off the last element.
snocView :: [a] -> Maybe ([a], a)
snocView [] = Nothing
snocView xs = go [] xs
  where
    -- Invariant: second arg is non-empty
    go acc [a]    = Just (reverse acc, a)
    go acc (a:as) = go (a:acc) as
    go _   []     = error "Util: snocView"
