{-# LANGUAGE TemplateHaskell            #-}

module Data.API.Tools.Combinators
    ( Tool
    , APITool
    , APINodeTool
    , runTool

      -- * Smart constructors and combinators
    , simpleTool
    , mkTool
    , contramapTool
    , readTool
    , subTools
    , apiNodeTool
    , apiDataTypeTool
    , apiSpecTool

      -- * Tool settings
    , ToolSettings
    , warnOnOmittedInstance
    , newtypeSmartConstructors
    , defaultToolSettings
    ) where

import           Data.API.Types

import           Control.Applicative
import           Data.Monoid
import           Language.Haskell.TH
import           Prelude


-- | Settings to control the behaviour of API tools.  This record may
-- be extended in the future, so you should construct a value by
-- overriding individual fields of 'defaultToolSettings'.
data ToolSettings = ToolSettings
    { warnOnOmittedInstance :: Bool
      -- ^ Generate a warning when an instance declaration is omitted
      -- because it already exists
    , newtypeSmartConstructors :: Bool
      -- ^ Rename the constructors of filtered newtypes and generate
      -- smart constructors that enforce the invariants
    }

-- | Default settings designed to be overridden.
defaultToolSettings :: ToolSettings
defaultToolSettings = ToolSettings
    { warnOnOmittedInstance = False
    , newtypeSmartConstructors = False
    }

-- | A @'Tool' a@ is something that can generate TH declarations from
-- a value of type @a@.  Tools can be combined using the 'Monoid'
-- instance.
newtype Tool a   = Tool
    { runTool :: ToolSettings -> a -> Q [Dec]
      -- ^ Execute a tool to generate some TH declarations.
    }

type APITool     = Tool API
type APINodeTool = Tool APINode

instance Monoid (Tool a) where
  mempty                    = Tool $ \ _ _ -> return []
  Tool t1 `mappend` Tool t2 = Tool $ \ ts x -> (++) <$> t1 ts x <*> t2 ts x

-- | Construct a tool that does not depend on any settings
simpleTool :: (a -> Q [Dec]) -> Tool a
simpleTool f = Tool $ const f

-- | Construct a tool that may depend on the settings
mkTool :: (ToolSettings -> a -> Q [Dec]) -> Tool a
mkTool = Tool

-- | 'Tool' is a contravariant functor
contramapTool :: (a -> b) -> Tool b -> Tool a
contramapTool f t = Tool $ \ ts a -> runTool t ts (f a)

-- | Make a tool that reads its argument to decide what to do
readTool :: (a -> Tool a) -> Tool a
readTool t = mkTool $ \ ts x -> runTool (t x) ts x

-- | Apply a tool that acts on elements of a list to the entire list
subTools :: Tool a -> Tool [a]
subTools t = Tool $ \ ts as -> concat <$> mapM (runTool t ts) as

-- | Apply a tool that acts on nodes to an entire API
apiNodeTool :: Tool APINode -> Tool API
apiNodeTool = contramapTool (\ api -> [an | ThNode an <- api ]) . subTools

-- | Apply a tool that acts on datatype nodes (i.e. those that are not
-- synonyms) to an entire API
apiDataTypeTool :: Tool APINode -> Tool API
apiDataTypeTool = contramapTool (\ api -> [an | ThNode an <- api, hasDataType $ anSpec an ]) . subTools
  where
    hasDataType (SpSynonym _) = False
    hasDataType _             = True

-- | Create a tool that acts on nodes from its action on individual
-- specs.
apiSpecTool :: Tool (APINode, SpecNewtype)
            -> Tool (APINode, SpecRecord )
            -> Tool (APINode, SpecUnion  )
            -> Tool (APINode, SpecEnum   )
            -> Tool (APINode, APIType    )
            -> Tool APINode
apiSpecTool n r u e s = Tool $ \ ts an -> case anSpec an of
              SpNewtype sn -> runTool n ts (an, sn)
              SpRecord  sr -> runTool r ts (an, sr)
              SpUnion   su -> runTool u ts (an, su)
              SpEnum    se -> runTool e ts (an, se)
              SpSynonym ss -> runTool s ts (an, ss)