{-# OPTIONS_HADDOCK hide #-}

-- |
--
-- Copyright:
--   This file is part of the package byline. It is subject to the
--   license terms in the LICENSE file found in the top-level
--   directory of this distribution and at:
--
--     https://github.com/pjones/byline
--
--   No part of this package, including this file, may be copied,
--   modified, propagated, or distributed except according to the
--   terms contained in the LICENSE file.
--
-- License: BSD-2-Clause
module Byline.Internal.Eval
  ( MonadByline (..),
    BylineT,
    runBylineT,
    Settings (..),
    defaultBylineSettings,
    runBylineT',
    defaultRenderMode,
  )
where

import Byline.Internal.Completion
import Byline.Internal.Prim (PrimF (..))
import Byline.Internal.Stylized
import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
import Control.Monad.Cont (ContT, MonadCont)
import Control.Monad.Except (MonadError)
import qualified Control.Monad.State.Lazy as LState
import qualified Control.Monad.Trans.Free.Church as Free
import qualified System.Console.ANSI as ANSI
import qualified System.Console.Haskeline as Haskeline
import qualified System.Environment as System
import qualified System.Terminfo as Terminfo
import qualified System.Terminfo.Caps as Terminfo

-- | A class of types that can lift Byline operations into a base
-- monad.
--
-- @since 1.0.0.0
class Monad m => MonadByline (m :: * -> *) where
  liftByline :: Free.F PrimF a -> m a
  default liftByline :: (MonadTrans t, MonadByline m1, m ~ t m1) => Free.F PrimF a -> m a
  liftByline = lift . liftByline

instance MonadByline m => MonadByline (ExceptT e m)

instance MonadByline m => MonadByline (StateT s m)

instance MonadByline m => MonadByline (LState.StateT s m)

instance MonadByline m => MonadByline (ReaderT r m)

instance MonadByline m => MonadByline (IdentityT m)

instance MonadByline m => MonadByline (ContT r m)

-- | A monad transformer that implements 'MonadByline'.
--
-- @since 1.0.0.0
newtype BylineT m a = BylineT
  {unBylineT :: Free.FT PrimF m a}
  deriving newtype
    ( Functor,
      Applicative,
      Monad,
      MonadIO,
      MonadState s,
      MonadReader r,
      MonadError e,
      MonadCont,
      MonadThrow,
      MonadCatch
    )

instance MonadTrans BylineT where
  lift = BylineT . lift

instance MonadByline (BylineT m) where
  liftByline = BylineT . Free.fromF

-- | Mutable list of completion functions.
--
-- @since 1.0.0.0
type CompRef m = IORef [CompletionFunc m]

-- | Discharge the 'MonadByline' effect by running all operations and
-- returning the result in the base monad.
--
-- The result is wrapped in a 'Maybe' where a 'Nothing' value
-- indicates that an end-of-file (EOF) signal was received while
-- reading user input.
--
-- @since 1.0.0.0
runBylineT ::
  (MonadIO m, MonadMask m) =>
  BylineT m a ->
  m (Maybe a)
runBylineT = runBylineT' defaultBylineSettings

-- | Settings that control Byline at run time.
--
-- @since 1.0.0.0
data Settings = Settings
  { -- | The output handle to write to.  If 'Nothing' use standard
    -- output.
    --
    -- NOTE: This only affects Byline (i.e. functions that use
    -- @say@).  Functions like @ask@ that invoke Haskeline will always
    -- use standard output since that's the hard-coded default.
    bylineOutput :: Maybe Handle,
    -- | The input handle to read from.  If 'Nothing' use standard
    -- input.
    bylineInput :: Maybe Handle,
    -- | Override the detected render mode.
    --
    -- If 'Nothing' use the render mode that is calculated based on
    -- the type of handle Byline writes to.
    bylineMode :: Maybe RenderMode
  }

-- | The default Byline settings.
--
-- @since 1.0.0.0
defaultBylineSettings :: Settings
defaultBylineSettings = Settings Nothing Nothing Nothing

-- | Like 'runBylineT' except you can override the settings.
--
-- @since 1.0.0.0
runBylineT' ::
  forall m a.
  (MonadIO m, MonadMask m) =>
  Settings ->
  BylineT m a ->
  m (Maybe a)
runBylineT' Settings {..} m = do
  compRef <- newIORef []
  let settings =
        Haskeline.setComplete
          (compFunc compRef)
          Haskeline.defaultSettings
  let behavior =
        maybe
          Haskeline.defaultBehavior
          Haskeline.useFileHandle
          bylineInput
  let hOut = fromMaybe stdout bylineOutput
  Haskeline.runInputTBehavior behavior settings (go compRef hOut)
  where
    compFunc :: CompRef IO -> Haskeline.CompletionFunc m
    compFunc compRef input = liftIO $
      readIORef compRef >>= \case
        [] -> Haskeline.completeFilename input
        fs -> runCompletionFunctions fs input
    go ::
      CompRef IO ->
      Handle ->
      Haskeline.InputT m (Maybe a)
    go compRef hOut = do
      mode <- maybe (liftIO (defaultRenderMode hOut)) pure bylineMode
      unBylineT m
        & evalPrimF mode hOut compRef
        & unEvalT
        & runMaybeT

-- | Internal transformer for evaluating primitive operations in the
-- 'Haskeline.InputT' transformer with EOF handling.
--
-- @since 1.0.0.0
newtype EvalT m a = EvalT
  {unEvalT :: MaybeT (Haskeline.InputT m) a}
  deriving newtype (Functor, Applicative, Monad, MonadIO)

instance MonadTrans EvalT where
  lift = EvalT . lift . lift

-- | Evaluate 'PrimF' values.
--
-- @since 1.0.0.0
evalPrimF ::
  forall m a.
  (MonadIO m, MonadMask m) =>
  RenderMode ->
  Handle ->
  CompRef IO ->
  Free.FT PrimF m a ->
  EvalT m a
evalPrimF renderMode outputHandle compRef = Free.iterTM go
  where
    go ::
      PrimF (EvalT m a) ->
      EvalT m a
    go = \case
      Say s k ->
        liftIO (render renderMode outputHandle s) >> k
      AskLn s d k -> do
        let prompt =
              renderText renderMode $
                maybe s (\d' -> s <> text "[" <> text d' <> text "] ") d
        liftHaskeline (Haskeline.getInputLine (toString prompt)) >>= \case
          Nothing -> EvalT empty
          Just answer
            | null answer -> k (fromMaybe mempty d)
            | otherwise -> k (toText answer)
      AskChar s k -> do
        let prompt = toString (renderText renderMode s)
        liftHaskeline (Haskeline.getInputChar prompt) >>= \case
          Nothing -> EvalT empty
          Just c -> k c
      AskPassword s m k -> do
        let prompt = toString (renderText renderMode s)
        liftHaskeline (Haskeline.getPassword m prompt) >>= \case
          Nothing -> EvalT empty
          Just str -> k (toText str)
      PushCompFunc f k ->
        modifyIORef' compRef (f :) >> k
      PopCompFunc k ->
        modifyIORef'
          compRef
          ( \case
              [] -> []
              (_ : fs) -> fs
          )
          >> k
    liftHaskeline :: Haskeline.InputT m b -> EvalT m b
    liftHaskeline = Haskeline.withInterrupt >>> lift >>> EvalT

-- | Calculate the default rendering mode based on the terminal type.
defaultRenderMode :: Handle -> IO RenderMode
defaultRenderMode hOut = do
  isTerm <- ANSI.hSupportsANSI hOut
  if isTerm
    then runMaybeT getMaxColors >>= \case
      Nothing -> pure Simple
      Just n
        | n < 256 -> pure Simple
        | n > 256 -> pure TermRGB
        | otherwise -> pure Term256
    else pure Plain
  where
    getMaxColors :: MaybeT IO Int
    getMaxColors = do
      term <- MaybeT (System.lookupEnv "TERM")
      lift (Terminfo.acquireDatabase term) >>= \case
        Left _ -> empty
        Right db ->
          hoistMaybe $
            Terminfo.queryNumTermCap db Terminfo.MaxColors