module Composite.Aeson.TH
  ( makeRecJsonWrapper
  , makeRecJsonWrapperExplicit
  ) where

import Composite.Aeson.Base (JsonFormat, dimapJsonFormat, parseJsonWithFormat', toJsonWithFormat)
import Composite.Aeson.Record (defaultJsonFormatRec, recJsonFormat)
import Composite.Record (Record)
import Control.Lens (_head, over)
import Data.Aeson (FromJSON(parseJSON), ToJSON(toJSON))
import Data.Char (toLower)
import Data.Monoid ((<>))
import Language.Haskell.TH
  ( Q, clause, cxt
  , normalB
  , recC, varBangType, bang, bangType, noSourceUnpackedness, noSourceStrictness
  , Dec, funD, instanceD, newtypeD, sigD, valD
  , Exp, conE, varE
  , Name, mkName, newName
  , varP
  , conT
  )

-- |TH splice which makes it more convenient to define ToJSON / FromJSON instances for record types.
--
-- For example:
--
-- @
--   type MyRecord = '[FFoo, FBar]
--   makeRecJsonWrapper "MyRecordJson" ''MyRecord
-- @
--
-- is equivalent to:
--
-- @
--   newtype MyRecordJson = MyRecordJson { unMyRecordJson :: Record MyRecord }
--   myRecordJsonFormat :: 'Composite.Aeson.Record.JsonFormatRec' Void MyRecordJson
--   myRecordJsonFormat =
--     'dimapJsonFormat' unMyRecordJson MyRecordJson $
--       'recJsonFormat' defaultJsonFormatRec
--   instance FromJSON MyRecordJson where
--     parseJSON = 'parseJsonWithFormat'' myRecordJsonFormat
--   instance ToJSON MyRecordJson where
--     toJSON = 'toJsonWithFormat' myRecordJsonFormat
-- @
--
-- This function uses 'defaultJsonFormatRec' to derive the formatting for the record. If you want to customize that formatting, use
-- 'makeRecJsonWrapperExplicit' instead.
makeRecJsonWrapper :: String -> Name -> Q [Dec]
makeRecJsonWrapper wrapperName tyName =
  makeRecJsonWrapperExplicit wrapperName tyName [| defaultJsonFormatRec |]

-- |TH splice which makes it more convenient to define ToJSON / FromJSON instances for record types.
--
-- For example:
--
-- @
--   type MyRecord = '[FFoo, FBar]
--   makeRecJsonWrapperExplicit "MyRecordJson" ''MyRecord [| set (rlens fFoo_) specialFormat defaultJsonFormatRec |]
-- @
--
-- is equivalent to:
--
-- @
--   newtype MyRecordJson = MyRecordJson { unMyRecordJson :: Record MyRecord }
--   myRecordJsonFormat :: 'Composite.Aeson.Record.JsonFormatRec' Void MyRecordJson
--   myRecordJsonFormat =
--     'dimapJsonFormat' unMyRecordJson MyRecordJson $
--       'recJsonFormat' (set (rlens fFoo_) specialFormat defaultJsonFormatRec)
--   instance FromJSON MyRecordJson where
--     parseJSON = 'parseJsonWithFormat'' myRecordJsonFormat
--   instance ToJSON MyRecordJson where
--     toJSON = 'toJsonWithFormat' myRecordJsonFormat
-- @
makeRecJsonWrapperExplicit :: String -> Name -> Q Exp -> Q [Dec]
makeRecJsonWrapperExplicit wrapperNameStr fieldsTyName recFormatExp = do
  let wrapperName = mkName wrapperNameStr
      extractorName = mkName $ "un" <> wrapperNameStr
      recordTy = [t| Record $(conT fieldsTyName) |]
  formatName <- newName $ over _head toLower wrapperNameStr <> "Format"
  sequence
    [ newtypeD
        (cxt [])
        wrapperName
        [] -- TyVarBndrs
        Nothing -- kind
        (recC wrapperName [varBangType extractorName (bangType (bang noSourceUnpackedness noSourceStrictness) recordTy)])
        (cxt []) -- deriving context
    , sigD
        formatName
        [t| forall e. JsonFormat e $(conT wrapperName) |]
    , valD
        (varP formatName)
        (normalB [| dimapJsonFormat $(varE extractorName) $(conE wrapperName) (recJsonFormat $recFormatExp) |])
        []
    , instanceD
        (cxt [])
        [t| FromJSON $(conT wrapperName) |]
        [ funD
            'parseJSON
            [ clause [] (normalB [| parseJsonWithFormat' $(varE formatName) |]) [] ]
        ]
    , instanceD
        (cxt [])
        [t| ToJSON $(conT wrapperName) |]
        [ funD
            'toJSON
            [ clause [] (normalB [| toJsonWithFormat $(varE formatName) |]) [] ]
        ]
    ]