{-# LANGUAGE OverloadedStrings #-}

{-

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 git://pmade.com/byline/LICENSE. No part of the
byline package, including this file, may be copied, modified,
propagated, or distributed except according to the terms contained in
the LICENSE file.

-}

--------------------------------------------------------------------------------
-- | Primitive operations such as printing messages and reading input.
module System.Console.Byline.Primitive
       ( ReportType (..)
       , say
       , sayLn
       , ask
       , askChar
       , askPassword
       , askUntil
       , report
       , reportLn
       , withCompletionFunc
       ) where

--------------------------------------------------------------------------------
-- Library imports:
import Control.Monad.IO.Class
import qualified Control.Monad.Reader as Reader
import Data.IORef
import Data.Maybe
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as T
import qualified System.Console.Haskeline as H

--------------------------------------------------------------------------------
--- Byline imports:
import System.Console.Byline.Color
import System.Console.Byline.Internal.Byline
import System.Console.Byline.Internal.Completion
import System.Console.Byline.Internal.Render
import System.Console.Byline.Modifiers
import System.Console.Byline.Stylized

--------------------------------------------------------------------------------
-- | Report types for the 'report' function.
data ReportType = Error    -- ^ Report errors with: @"error: "@
                | Warning  -- ^ Report warnings with: @"warning: "@

--------------------------------------------------------------------------------
-- | Output the stylized text to the output handle (default: stdout).
say :: (MonadIO m) => Stylized -> Byline m ()
say message = do
  env <- Reader.ask
  liftIO $ render (sayMode env) (outHandle env) message

--------------------------------------------------------------------------------
-- | Like 'say', but append a newline character.
sayLn :: (MonadIO m) => Stylized -> Byline m ()
sayLn message = say (message <> text "\n")

--------------------------------------------------------------------------------
-- | Read input after printing the given stylized text as a prompt.
ask :: (MonadIO m)
    => Stylized
       -- ^ The prompt.

    -> Maybe Text
    -- ^ Optional default answer that will be returned if the user
    -- presses return without providing any input (a zero-length
    -- string).

    -> Byline m Text
ask prompt defans = do
  let prompt' = case defans of
                  Nothing -> prompt
                  Just s  -> prompt <> text "[" <> text s <> "] "

  answer <- liftInputT . H.getInputLine =<< renderPrompt prompt'

  case answer of
    Nothing            -> eof
    Just s | null s    -> return (fromMaybe (T.pack s) defans)
           | otherwise -> return (T.pack s)

--------------------------------------------------------------------------------
-- | Read a single character of input.
askChar :: (MonadIO m)
        => Stylized
        -> Byline m Char
askChar prompt = do
  answer <- liftInputT . H.getInputChar =<< renderPrompt prompt
  case answer of
    Nothing -> eof
    Just c  -> return c

--------------------------------------------------------------------------------
-- | Read a password without echoing it to the terminal.  If a masking
-- character is given it will replace each typed character.
askPassword :: (MonadIO m)
            => Stylized
               -- ^ The prompt.

            -> Maybe Char
            -- ^ Optional masking character that will be printed each
            -- time the user presses a key.

            -> Byline m Text
askPassword prompt maskchr = do
  pass <- liftInputT . H.getPassword maskchr =<< renderPrompt prompt
  case pass of
    Nothing -> eof
    Just s  -> return (T.pack s)

--------------------------------------------------------------------------------
-- | Continue to prompt for a response until a confirmation function
-- returns a valid result.
--
-- The confirmation function receives the output from 'ask' and should
-- return a @Left Stylized@ to produce an error message (printed with
-- 'sayLn').  When an acceptable answer from 'ask' is received, the
-- confirmation function should return it with @Right@.
askUntil :: (MonadIO m)
         => Stylized                           -- ^ The prompt.
         -> Maybe Text                         -- ^ Optional default answer.
         -> (Text -> m (Either Stylized Text)) -- ^ Confirmation function.
         -> Byline m Text
askUntil prompt defans confirm = go where
  go = do
    answer <- ask prompt defans
    check  <- liftBase (confirm answer)

    case check of
      Left msg     -> sayLn msg >> go
      Right result -> return result

--------------------------------------------------------------------------------
-- | Output stylized text with a prefix determined by 'ReportType'.
report :: (MonadIO m) => ReportType -> Stylized -> Byline m ()
report Error message   = say $ (text "error: "   <> fg red)    <> message
report Warning message = say $ (text "warning: " <> fg yellow) <> message

--------------------------------------------------------------------------------
-- | Like 'report', but append a newline character.
reportLn :: (MonadIO m) => ReportType -> Stylized -> Byline m ()
reportLn rt message = report rt (message <> text "\n")

--------------------------------------------------------------------------------
-- | Run the given 'Byline' action with a different completion
-- function.
withCompletionFunc :: (MonadIO m) => CompletionFunc -> Byline m a -> Byline m a
withCompletionFunc comp byline = do
  compref <- Reader.asks compFunc
  current <- liftIO (readIORef compref)

  -- Temporally change the completion function.
  -- Exceptions will be dealt with in 'runByline'.
  liftIO (writeIORef compref (Just comp))
  output <- byline

  -- Reset the completion function and return the result.
  liftIO (writeIORef compref current)
  return output

--------------------------------------------------------------------------------
renderPrompt :: (Monad m) => Stylized -> Byline m String
renderPrompt prompt = do
    mode <- Reader.asks askMode
    return $ T.unpack (renderText mode prompt)