{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NoImplicitPrelude #-}
-- |
-- Types and functions that make up the internal structure of the encoders.
--
module Waargonaut.Encode.Types
  ( -- * Types
    EncoderFns (..)

    -- * Useful aliases
  , Encoder
  , Encoder'
  , ObjEncoder
  , ObjEncoder'

    -- * Runners
  , runEncoder
  , runPureEncoder

    -- * Helpers
  , jsonEncoder
  , objEncoder
  , generaliseEncoder
  ) where

import           Control.Monad                        (Monad)
import           Control.Monad.Morph                  (MFunctor (..),
                                                       generalize)

import           Control.Applicative                  (Applicative, liftA2,
                                                       pure)
import           Control.Category                     (id, (.))
import           Control.Lens                         (( # ))

import           Data.Either                          (either)
import           Data.Function                        (const, ($))
import           Data.Functor                         (Functor)
import           Data.Functor.Contravariant           (Contravariant (..))

import           Data.Functor.Contravariant.Divisible (Decidable (..),
                                                       Divisible (..))
import           Data.Monoid                          (mempty)
import           Data.Semigroup                       ((<>))
import           Data.Void                            (absurd)

import           Data.Functor                         (fmap)
import           Data.Functor.Identity                (Identity (..))

import           Waargonaut.Types                     (JObject, Json, WS, _JObj)


-- |
-- Define an "encoder" as a function from some @a@ to some 'Json' with the
-- allowance for some context @f@.
--
-- The helper functions 'jsonEncoder' and 'objEncoder' are probably what you
-- want to use.
--
data EncoderFns i f a = EncoderFns
  { finaliseEncoding :: i -> Json -- ^ The @i@ need not be the final 'Json' structure. This function will complete the output from 'initialEncoding' to the final 'Json' output.

  , initialEncoding  :: a -> f i -- ^ Run the initial encoding step of the given input. This lets you encode the @a@ to an intermediate structure before utilising the 'finaliseEncoding' function to complete the process.
  }

instance MFunctor (EncoderFns i) where
  hoist nat (EncoderFns f i) = EncoderFns f (nat . i)

-- | Generalise any 'Encoder' a' to 'Encoder f a'
generaliseEncoder :: Monad f => EncoderFns i Identity a -> EncoderFns i f a
generaliseEncoder (EncoderFns f i) = EncoderFns f (generalize . i)

instance Contravariant (EncoderFns o f) where
  contramap f e = EncoderFns (finaliseEncoding e) (initialEncoding e . f)
  {-# INLINE contramap #-}

instance Applicative f => Divisible (EncoderFns (JObject WS Json) f) where
  conquer = objEncoder (const (pure mempty))
  {-# INLINE conquer #-}

  divide atobc (EncoderFns _ oB) (EncoderFns _ oC) = objEncoder $ \a ->
    let
      (b,c) = atobc a
    in
      liftA2 (<>) (oB b) (oC c)
  {-# INLINE divide #-}

instance Applicative f => Decidable (EncoderFns (JObject WS Json) f) where
  lose f = objEncoder $ \a -> absurd (f a)
  {-# INLINE lose #-}

  choose split (EncoderFns _ oB) (EncoderFns _ oC) = objEncoder $ \a ->
    either oB oC (split a)
  {-# INLINE choose #-}

-- | As a convenience, this type defines the @i@ to be a specific 'Json' structure:
type Encoder f a = EncoderFns Json f a

-- | As a convenience, this type defines the @i@ to be a specific 'JObject WS Json' structure:
type ObjEncoder f a = EncoderFns (JObject WS Json) f a

-- | As a convenience, this type is a pure Encoder over 'Identity' in place of the @f@.
type Encoder' a = EncoderFns Json Identity a
-- | As a convenience, this type is a pure ObjEncoder over 'Identity' in place of the @f@.
type ObjEncoder' a = EncoderFns (JObject WS Json) Identity a

-- | Run any encoder to the 'Json' representation, allowing for some
-- 'Applicative' context @f@.
runEncoder :: Functor f => EncoderFns i f a -> a -> f Json
runEncoder e = fmap (finaliseEncoding e) . initialEncoding e
{-# INLINE runEncoder #-}

-- | Run any encoder to the 'Json' representation, with the context specialised
-- to 'Identity' for convenience.
runPureEncoder :: EncoderFns i Identity a -> a -> Json
runPureEncoder e = runIdentity . fmap (finaliseEncoding e) . initialEncoding e
{-# INLINE runPureEncoder #-}

-- | Helper function for creating an 'Encoder', provides the default
-- 'finaliseEncoding' function for 'Json' encoders.
jsonEncoder :: (a -> f Json) -> EncoderFns Json f a
jsonEncoder = EncoderFns id
{-# INLINE jsonEncoder #-}

-- | Helper function for creating a JSON 'object' 'Encoder'. Provides the
-- default 'finaliseEncoding' function for completing the 'JObject' to the
-- necessary 'Json' type.
objEncoder :: (a -> f (JObject WS Json)) -> EncoderFns (JObject WS Json) f a
objEncoder = EncoderFns (\o -> _JObj # (o, mempty))
{-# INLINE objEncoder #-}