----------------------------------------------------------------------------
-- |
-- Module      :  Prettyprinter.Data
-- Copyright   :  (c) Sergey Vinokurov 2018
-- License     :  Apache-2.0 (see LICENSE)
-- Maintainer  :  serg.foo@gmail.com
----------------------------------------------------------------------------

{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE PatternGuards       #-}
{-# LANGUAGE Rank2Types          #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Prettyprinter.Data
  ( ppData
  , ppDataSimple
  ) where

import Data.Data
import Data.Generics qualified
import Prettyprinter
import Prettyprinter qualified as PP
import Prettyprinter.Combinators
import Prettyprinter.MetaDoc

-- $setup
-- >>> :set -XDeriveDataTypeable
-- >>> :set -XImportQualifiedPost
-- >>> import Data.Data
-- >>> import Data.List.NonEmpty (NonEmpty(..))
-- >>> import Data.List.NonEmpty qualified as NonEmpty
-- >>> import Data.Map.Strict (Map)
-- >>> import Data.Map.Strict qualified as Map
--
-- >>> :{
-- data Test =
--     Foo Int [Int] Double (Maybe Test)
--   | Bar (String, Int, Int) (Map String Int) (Map String Int) (Maybe Test) (NonEmpty Int)
--   deriving (Data)
-- :}

-- | Prettyprint using 'Data.Data' instance.
--
-- >>> :{
-- test =
--   Bar
--     ("foo", 10, 20)
--     (Map.fromList (zip ["foo", "bar", "baz"] [1..]))
--     (Map.fromList (zip ["foo", "bar", "baz", "quux", "fizz", "buzz", "frob", "wat"] [1..]))
--     (Just
--       (Foo
--          1
--          []
--          3.14159265358979323846264338327950288
--          (Just
--             (Foo
--                1
--                [2]
--                2.71828182
--                (Just (Bar ("x", 1, 2) mempty mempty Nothing (NonEmpty.fromList [42])))))))
--     (NonEmpty.fromList [1..42])
-- :}
--
-- >>> ppData test
-- Bar
--   (foo, 10, 20)
--   {bar -> 2, baz -> 3, foo -> 1}
--   { bar -> 2
--   , baz -> 3
--   , buzz -> 6
--   , fizz -> 5
--   , foo -> 1
--   , frob -> 7
--   , quux -> 4
--   , wat -> 8
--   }
--   Just Foo
--          1
--          {}
--          3.141592653589793
--          Just (Foo 1 [2] 2.71828182 (Just (Bar (x, 1, 2) {} {} Nothing [42])))
--   [ 1
--   , 2
--   , 3
--   , 4
--   , 5
--   , 6
--   , 7
--   , 8
--   , 9
--   , 10
--   , 11
--   , 12
--   , 13
--   , 14
--   , 15
--   , 16
--   , 17
--   , 18
--   , 19
--   , 20
--   , 21
--   , 22
--   , 23
--   , 24
--   , 25
--   , 26
--   , 27
--   , 28
--   , 29
--   , 30
--   , 31
--   , 32
--   , 33
--   , 34
--   , 35
--   , 36
--   , 37
--   , 38
--   , 39
--   , 40
--   , 41
--   , 42
--   ]
ppData :: Data a => a -> Doc ann
ppData = mdPayload . gpretty

ppDataSimple :: Data a => a -> Doc ann
ppDataSimple = pretty . Data.Generics.gshow

gpretty :: forall a ann. Data a => a -> MetaDoc ann
gpretty =
  render
    `Data.Generics.extQ` stringMetaDoc
    `Data.Generics.extQ` strictTextMetaDoc
    `Data.Generics.extQ` lazyTextMetaDoc
    `Data.Generics.extQ` metaDocInt
    `Data.Generics.extQ` metaDocFloat
    `Data.Generics.extQ` metaDocDouble
    `Data.Generics.extQ` metaDocInteger
    `Data.Generics.extQ` metaDocWord
    `Data.Generics.extQ` metaDocWord8
    `Data.Generics.extQ` metaDocWord16
    `Data.Generics.extQ` metaDocWord32
    `Data.Generics.extQ` metaDocWord64
    `Data.Generics.extQ` metaDocInt8
    `Data.Generics.extQ` metaDocInt16
    `Data.Generics.extQ` metaDocInt32
    `Data.Generics.extQ` metaDocInt64
    `Data.Generics.extQ` metaDocUnit
    `Data.Generics.extQ` metaDocBool
    `Data.Generics.extQ` metaDocChar
    -- Probably requires qualified constrtains...
    -- `Data.Generics.extQ`
    --   ((atomicMetaDoc . ppMapWith (mdPayload . gpretty) (mdPayload . gpretty)) ::
    --     forall k v. (Data k, Data v) => Map k v -> MetaDoc ann)
  where
    render :: Data b => b -> MetaDoc ann
    render t
      | constructorName == "fromList"
      , Just mapItems  <- gmapQi 0 (listElements (isPair gpretty)) t
      , Just mapItems' <- sequence mapItems
      = atomicMetaDoc
      $ ppAssocListWith mdPayload mdPayload mapItems'
      | Just mapItems  <- listElements (isPair gpretty) t
      , Just mapItems' <- sequence mapItems
      = atomicMetaDoc
      $ ppAssocListWith mdPayload mdPayload mapItems'
      | Just listItems <- listElements gpretty t
      = atomicMetaDoc
      $ ppListWith mdPayload listItems
      | isTuple
      = atomicMetaDoc
      $ ppListWithDelim PP.lparen PP.rparen
      $ map mdPayload fields
      | otherwise
      = constructorAppMetaDoc constructorDoc fields
      where
        constructorDoc :: MetaDoc ann
        constructorDoc = atomicMetaDoc $ pretty constructorName

        fields :: [MetaDoc ann]
        fields = gmapQ gpretty t

        constructorName :: String
        constructorName = showConstr $ toConstr t

        isTuple :: Bool
        isTuple = all (== ',') (filter (not . (`elem` ("()" :: String))) constructorName)

isPair :: Data a => (forall b. Data b => b -> c) -> a -> Maybe (c, c)
isPair f x
  | constructorName == "(,)" = Just (gmapQi 0 f x, gmapQi 1 f x)
  | otherwise                = Nothing
  where
    constructorName :: String
    constructorName = showConstr $ toConstr x

-- | Try to treat @a@ as a list and prettyprint its elements with @f@.
-- Returns Just on succes and Nothing if @a@ wasn't a list after all.
listElements :: forall a c. Data a => (forall b. Data b => b -> c) -> a -> Maybe [c]
listElements f = go
  where
    go :: Data d => d -> Maybe [c]
    go x
      | isNull    = Just []
      | isCons    = (:) (gmapQi 0 f x) <$> gmapQi 1 go x
      | otherwise = Nothing
      where
        constructorName :: String
        constructorName = showConstr $ toConstr x
        isCons = constructorName `elem` ["(:)", ":|"]
        isNull = constructorName == "[]"