{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RankNTypes                 #-}
{-# LANGUAGE TypeSynonymInstances       #-}

-- | A bridge builder DSL, powered by 'Monad', 'Alternative' and lens.
--
--   Bridges can be built within the 'BridgeBuilder' monad.
--   You can check properties of the to-be-bridged 'HaskellType' with '^==' or 'doCheck',
--   you have choice ('<|>'), you can fail ('empty') and you can return a translated
--   'PSType' ('return'). The 'HaskellType' can be accessed with:
--
-- > view haskType
--
--   Find usage examples in "Language.PureScript.Bridge.Primitives" and "Language.PureScript.Bridge.PSTypes"
module Language.PureScript.Bridge.Builder (
  BridgeBuilder
, BridgePart
, FixUpBuilder
, FixUpBridge
, BridgeData
, fullBridge
, (^==)
, doCheck
, (<|>)
, psTypeParameters
, FullBridge
, buildBridge
, clearPackageFixUp
, errorFixUp
, buildBridgeWithCustomFixUp
) where

import           Control.Applicative
import           Control.Lens
import           Control.Monad                       (MonadPlus, guard, mplus,
                                                      mzero)
import           Control.Monad.Reader.Class
import           Control.Monad.Trans.Reader          (Reader, ReaderT (..),
                                                      runReader)
import           Data.Maybe                          (fromMaybe)
import qualified Data.Text                           as T
import           Language.PureScript.Bridge.TypeInfo

newtype BridgeBuilder a =
  BridgeBuilder (ReaderT BridgeData Maybe a)
    deriving (Functor, Applicative, Monad, MonadReader BridgeData)

type BridgePart = BridgeBuilder PSType

-- | Bridges to use when a 'BridgePart' returns 'Nothing' (See 'buildBridgeWithCustomFixUp').
--
--   It is similar to BridgeBuilder but does not offer choice or failure. It is used for constructing fallbacks
--   if a 'BridgePart' evaluates to 'Nothing'.
--
--   For type definitions you should use the more generic ('MonadReader' 'BridgeData' m) constraint. This way your code will work
--   in both 'FixUpBuilder' and 'BridgeBuilder':
--
-- > {-# LANGUAGE FlexibleContexts #-}
-- >
-- > import           Control.Monad.Reader.Class
-- > import           Language.PureScript.Bridge.TypeInfo

-- >
-- > psEither :: MonadReader BridgeData m => m PSType
-- > psEither = ....
--
--   instead of:
--
-- > psEither :: BridgePart
-- > psEither = ....
--
--   or
--
-- > psEither :: FixUpBridge
-- > psEither = ....
--
newtype FixUpBuilder a = FixUpBuilder (Reader BridgeData a) deriving (Functor, Applicative, Monad, MonadReader BridgeData)

type FixUpBridge = FixUpBuilder PSType

type FullBridge = HaskellType -> PSType

data BridgeData = BridgeData {
  -- | The Haskell type to translate.
    _haskType   :: HaskellType
  -- | Reference to the bridge itself, needed for translation of type constructors.
  , _fullBridge :: FullBridge
  }

-- | By implementing the 'haskType' lens in the HasHaskType class, we are able
--   to use it for both 'BridgeData' and a plain 'HaskellType', therefore
--   you can use it with 'doCheck' and '^==' for checks on the complete 'HaskellType'
--   value.
--
--   Example:
--
-- > stringBridge :: BridgePart
-- > stringBridge = do
-- >   -- Note: we are using the HaskellType instance here:
-- >   haskType ^== mkTypeInfo (Proxy :: Proxy String)
-- >   return psString
instance HasHaskType BridgeData where
  haskType inj (BridgeData iT fB) = flip BridgeData fB <$> inj iT

-- | Lens for access to the complete bridge from within our Reader monad.
--
--   This is used for example for implementing 'psTypeParameters'.
fullBridge :: Lens' BridgeData FullBridge
fullBridge inj (BridgeData iT fB) = BridgeData iT <$> inj fB

-- | Bridge to PureScript by simply clearing out the '_typePackage' field.
--   This bridge is used by default as 'FixUpBridge' by 'buildBridge':
--
-- > buildBridge = buildBridgeWithCustomFixUp clearPackageFixUp
--
--   Thus, if no bridge matches a type, it gets optimistically translated to a PureScript type
--   which is idential to the Haskell type. Only the '_typePackage' field gets cleared,
--   as it is very unlikely that the PureScript package is called the same as the Haskell package.
--
--   Alternatively, if you are not that optimistic, you can use errorFixUp
--   - which simply calls 'error' when used.
--
-- > buildBridgeWithCustomFixUp errorFixUp yourBridge
--
--   Of course you can also write your own 'FixUpBridge'. It works the same
--   as for 'BridgePart', but you can not have choice ('<|>') or failure ('empty').
clearPackageFixUp :: MonadReader BridgeData m => m PSType
clearPackageFixUp = do
  input <- view haskType
  psArgs <- psTypeParameters
  return TypeInfo {
      _typePackage = ""
    , _typeModule  = input ^. typeModule
    , _typeName    = input ^. typeName
    , _typeParameters = psArgs
    }

-- | A 'FixUpBridge' which calles 'error' when used.
--   Usage:
--
-- > buildBridgeWithCustomFixUp errorFixUp yourBridge
errorFixUp :: MonadReader BridgeData m => m PSType
errorFixUp = do
    inType <- view haskType
    let message = "No translation supplied for Haskell type: '"
          <> inType ^. typeName <> "', from module: '"
          <> inType ^. typeModule <> "', from package: '"
          <> inType ^. typePackage <> "'!"
    return $ error $ T.unpack message

-- | Build a bridge.
--
--   This is a convenience wrapper for 'buildBridgeWithCustomFixUp' and should normally be sufficient.
--
--   Definition:
--
-- > buildBridgeWithCustomFixUp clearPackageFixUp
buildBridge :: BridgePart -> FullBridge
buildBridge = buildBridgeWithCustomFixUp clearPackageFixUp


-- | Takes a constructed BridgePart and makes it a total function ('FullBridge')
--   by using the supplied 'FixUpBridge' when 'BridgePart' returns 'Nothing'.
buildBridgeWithCustomFixUp :: FixUpBridge -> BridgePart -> FullBridge
buildBridgeWithCustomFixUp (FixUpBuilder fixUp) (BridgeBuilder bridgePart) = let
    mayBridge :: HaskellType -> Maybe PSType
    mayBridge inType = runReaderT bridgePart $ BridgeData inType bridge
    fixBridge inType = runReader fixUp $ BridgeData inType bridge
    bridge inType = fixTypeParameters $ fromMaybe (fixBridge inType) (mayBridge inType)
  in
    bridge


-- | Translate types that come from any module named "Something.TypeParameters" to lower case:
--
--   Also drop the 1 at the end if present.
--   This method gets called by 'buildBridge' and buildBridgeWithCustomFixUp for you - you should not need to call it.
--
--   It enables you to even bridge type constructor definitions, see "Language.PureScript.Bridge.TypeParameters" for more details.
fixTypeParameters :: TypeInfo lang -> TypeInfo lang
fixTypeParameters t = if "TypeParameters" `T.isSuffixOf` _typeModule t
    then t {
          _typePackage = "" -- Don't suggest any packages
        , _typeModule = "" -- Don't import any modules
        , _typeName = t ^. typeName . to (stripNum . T.toLower)
        }
    else t
  where
    stripNum v = fromMaybe v (T.stripSuffix "1" v)


-- | Alternative instance for BridgeBuilder so you can construct bridges with '<|>',
--   which behaves like a logical 'or' ('||'). If the left-hand side results in Nothing
--   the right-hand side is used, otherwise the left-hand side.
--   For usage examples see "Language.PureScript.Bridge.Primitives".
instance Alternative BridgeBuilder where
  empty = BridgeBuilder . ReaderT $ const Nothing
  BridgeBuilder a <|> BridgeBuilder b = BridgeBuilder . ReaderT $ \bridgeData -> let
          ia = runReaderT a bridgeData
          ib = runReaderT b bridgeData
        in
          ia <|> ib

instance MonadPlus BridgeBuilder where
  mzero = empty
  mplus = (<|>)

-- | Do some check on properties of 'haskType'.
doCheck :: Getter HaskellType a -> (a -> Bool) -> BridgeBuilder ()
doCheck l check = guard =<< views (haskType . l) check

-- | Check parts of 'haskType' for equality:
--
-- > textBridge :: BridgePart
-- > textBridge = do
-- >   typeName ^== "Text"
-- >   typeModule ^== "Data.Text.Internal" <|> typeModule ^== "Data.Text.Internal.Lazy"
-- >   return psString
(^==) :: Eq a => Getter HaskellType a -> a -> BridgeBuilder ()
l ^== a = doCheck l (== a)

infix 4 ^==

-- | Bridge 'haskType' 'typeParameters' over to PureScript types.
--
--   To be used for bridging type constructors.
psTypeParameters :: MonadReader BridgeData m => m [PSType]
psTypeParameters = map <$> view fullBridge <*> view (haskType . typeParameters)