{- ----------------------------------------------------------------------------
  Copyright (c) 2021, Daan Leijen
  This is free software; you can redistribute it and/or modify it
  under the terms of the MIT License. A copy of the license can be
  found in the "LICENSE" file at the root of this distribution.
---------------------------------------------------------------------------- -}
{-|
Description : Binding to the Isocline library, a portable alternative to GNU Readline
Copyright   : (c) 2021, Daan Leijen
License     : MIT
Maintainer  : daan@effp.org
Stability   : Experimental

![logo](https://raw.githubusercontent.com/daanx/isocline/main/doc/isocline-inline.svg) 
A Haskell wrapper around the [Isocline C library](https://github.com/daanx/isocline#readme) 
which can provide an alternative to GNU Readline.
(The Isocline library is included whole and there are no runtime dependencies).

Isocline works across Unix, Windows, and macOS, and relies on a minimal subset of ANSI escape sequences.
It has a good multi-line editing mode (use shift/ctrl-enter) which is nice for inputting small functions etc.
Other features include support for colors, history, completion, unicode, undo/redo, 
incremental history search, inline hints, brace matching, syntax highlighting, rich text using bbcode
formatting, etc.

Minimal example with history:

@
import System.Console.Isocline

main :: IO ()
main  = do putStrLn \"Welcome\"
           `setHistory` \"history.txt\" 200
           input \<- `readline` \"myprompt\"     -- full prompt becomes \"myprompt> \"
           `putFmtLn` (\"[gray]You wrote:[\/gray]\\n\" ++ input)
@

Or using custom completions with an interactive loop:

@
import System.Console.Isocline
import Data.Char( toLower )

main :: IO ()
main 
  = do `styleDef' "ic-prompt" "ansi-maroon"
       `setHistory` "history.txt" 200
       `enableAutoTab` `True`
       interaction

interaction :: IO ()
interaction 
  = do s <- `readlineEx` \"hαskell\" (Just completer) Nothing 
       putStrLn (\"You wrote:\\n\" ++ s)
       if (s == \"\" || s == \"exit\") then return () else interaction
                     
completer :: `CompletionEnv` -> String -> IO () 
completer cenv input
  = do `completeFileName` cenv input Nothing [\".\",\"\/usr\/local\"] [\".hs\"]  -- use [] for any extension
       `completeWord` cenv input Nothing wcompleter

wcompleter :: String -> [`Completion`]
wcompleter input
  = `completionsFor` (map toLower input) 
      [\"print\",\"println\",\"prints\",\"printsln\",\"prompt\"]      
@

See a larger [example](https://github.com/daanx/isocline/blob/main/test/Example.hs) 
with syntax highlighting and more extenstive custom completion 
in the [Github repository](https://github.com/daanx/isocline).

Enjoy,
-- Daan
-}
module System.Console.Isocline( 
      -- * Readline
      readline, 
      readlineEx,      
      
      -- * History
      setHistory,
      historyClear,
      historyRemoveLast,
      historyAdd,

      -- * Completion
      CompletionEnv,      
      completeFileName,
      completeWord,
      completeQuotedWord,
      completeQuotedWordEx,

      Completion(..),
      completion,
      isPrefix,
      completionsFor,
      wordCompleter,

      -- * Syntax Highlighting 
      highlightFmt,

      -- * Rich text
      Style, Fmt,      
      style,
      plain,
      pre,
      
      putFmt,
      putFmtLn,

      styleDef,
      styleOpen,
      styleClose,
      withStyle,

      -- * Configuration
      setPromptMarker,
      enableAutoTab,
      enableColor,
      enableBeep,
      enableMultiline,
      enableHistoryDuplicates,
      enableCompletionPreview,
      enableMultilineIndent,
      enableHighlight,
      enableInlineHelp,
      enableHint,
      setHintDelay,
      enableBraceMatching,
      enableBraceInsertion,
      setMatchingBraces,
      setInsertionBraces,    
            
      -- * Advanced
      setDefaultCompleter,      
      addCompletion,
      addCompletionPrim,
      addCompletions,
      completeWordPrim,
      completeQuotedWordPrim,
      completeQuotedWordPrimEx,

      readlineMaybe,
      readlineExMaybe,
      readlinePrim,
      readlinePrimMaybe,

      getPromptMarker,
      getContinuationPromptMarker,
      stopCompleting,
      hasCompletions,

      asyncStop,

      -- * Low-level highlighting
      HighlightEnv,      
      setDefaultHighlighter,      
      setDefaultFmtHighlighter,
      
      -- * Low-level Terminal
      termInit, 
      termDone,
      withTerm,
      termFlush,
      termWrite,
      termWriteLn,
      termColor,
      termBgColor,
      termColorAnsi,
      termBgColorAnsi,
      termUnderline,
      termReverse,
      termReset      

    ) where


import Data.List( intersperse, isPrefixOf )
import Control.Monad( when, foldM )
import Control.Exception( bracket )
import Foreign.C.String( CString, peekCString, peekCStringLen, withCString, castCharToCChar )
import Foreign.Ptr
import Foreign.C.Types

-- the following are used for utf8 encoding.
import qualified Data.ByteString as B ( useAsCString, packCString )
import qualified Data.Text as T  ( pack, unpack )
import Data.Text.Encoding as TE  ( decodeUtf8With, encodeUtf8)
import Data.Text.Encoding.Error  ( lenientDecode )


----------------------------------------------------------------------------
-- C Types
----------------------------------------------------------------------------

data IcCompletionEnv  

-- | Abstract list of current completions.
newtype CompletionEnv = CompletionEnv (Ptr IcCompletionEnv)

type CCompleterFun = Ptr IcCompletionEnv -> CString -> IO ()
type CompleterFun  = CompletionEnv -> String -> IO ()


data IcHighlightEnv

-- | Abstract highlight environment
newtype HighlightEnv = HighlightEnv (Ptr IcHighlightEnv)    

type CHighlightFun = Ptr IcHighlightEnv -> CString -> Ptr () -> IO ()
type HighlightFun  = HighlightEnv -> String -> IO ()



----------------------------------------------------------------------------
-- Basic readline
----------------------------------------------------------------------------

foreign import ccall ic_free        :: (Ptr a) -> IO () 
foreign import ccall ic_malloc      :: CSize -> IO (Ptr a)
foreign import ccall ic_strdup      :: CString -> IO CString
foreign import ccall ic_readline    :: CString -> IO CString
foreign import ccall ic_readline_ex :: CString -> FunPtr CCompleterFun -> (Ptr ()) -> FunPtr CHighlightFun -> (Ptr ()) -> IO CString
foreign import ccall ic_async_stop  :: IO CCBool

unmaybe :: IO (Maybe String) -> IO String
unmaybe action
  = do mb <- action
       case mb of
         Nothing -> return ""
         Just s  -> return s

-- | @readline prompt@: Read (multi-line) input from the user with rich editing abilities. 
-- Takes the prompt text as an argument. The full prompt is the combination
-- of the given prompt and the prompt marker (@\"> \"@ by default) .
-- See also 'readlineEx', 'readlineMaybe', 'enableMultiline', and 'setPromptMarker'.
readline :: String -> IO String  
readline prompt
  = unmaybe $ readlineMaybe prompt

-- | As 'readline' but returns 'Nothing' on end-of-file or other errors (ctrl-C/ctrl-D).
readlineMaybe:: String -> IO (Maybe String)
readlineMaybe prompt
  = withUTF8String prompt $ \cprompt ->
    do cres <- ic_readline cprompt
       res  <- peekUTF8StringMaybe cres
       ic_free cres
       return res

-- | @readlineEx prompt mbCompleter mbHighlighter@: as 'readline' but
-- uses the given @mbCompleter@ function to complete words on @tab@ (instead of the default completer). 
-- and the given @mbHighlighter@ function to highlight the input (instead of the default highlighter).
-- See also 'readline' and 'readlineExMaybe'.
readlineEx :: String -> Maybe (CompletionEnv -> String -> IO ()) -> Maybe (String -> Fmt) -> IO String
readlineEx prompt completer highlighter
  = unmaybe $ readlineExMaybe prompt completer highlighter

-- | As 'readlineEx' but returns 'Nothing' on end-of-file or other errors (ctrl-C/ctrl-D).
-- See also 'readlineMaybe'.
readlineExMaybe :: String -> Maybe (CompletionEnv -> String -> IO ()) -> Maybe (String -> Fmt) -> IO (Maybe String) 
readlineExMaybe prompt completer mbhighlighter
  = readlinePrimMaybe prompt completer (case mbhighlighter of
                                          Nothing -> Nothing
                                          Just hl -> Just (highlightFmt hl))

-- | @readlinePrim prompt mbCompleter mbHighlighter@: as 'readline' but
-- uses the given @mbCompleter@ function to complete words on @tab@ (instead of the default completer). 
-- and the given @mbHighlighter@ function to highlight the input (instead of the default highlighter).
-- See also 'readlineEx' and 'readlinePrimMaybe'.
readlinePrim :: String -> Maybe (CompletionEnv -> String -> IO ()) -> Maybe (HighlightEnv -> String -> IO ()) -> IO String
readlinePrim prompt completer highlighter
  = unmaybe $ readlinePrimMaybe prompt completer highlighter

-- | As 'readlinePrim' but returns 'Nothing' on end-of-file or other errors (ctrl-C/ctrl-D).
-- See also 'readlineMaybe'.
readlinePrimMaybe :: String -> Maybe (CompletionEnv -> String -> IO ()) -> Maybe (HighlightEnv -> String -> IO ()) -> IO (Maybe String) 
readlinePrimMaybe prompt completer highlighter
  = withUTF8String prompt $ \cprompt ->
    do ccompleter   <- makeCCompleter completer
       chighlighter <- makeCHighlighter highlighter
       cres <- ic_readline_ex cprompt ccompleter nullPtr chighlighter nullPtr
       res  <- peekUTF8StringMaybe cres
       ic_free cres
       when (ccompleter /= nullFunPtr)   $ freeHaskellFunPtr ccompleter
       when (chighlighter /= nullFunPtr) $ freeHaskellFunPtr chighlighter
       return res

-- | Thread safe call to asynchronously send a stop event to a 'readline' 
-- which behaves as if the user pressed @ctrl-C@,
-- which will return with 'Nothing' (or @\"\"@). 
-- Returns 'True' if the event was successfully delivered.
asyncStop :: IO Bool
asyncStop
  = uncbool $ ic_async_stop

----------------------------------------------------------------------------
-- History
----------------------------------------------------------------------------

foreign import ccall ic_set_history           :: CString -> CInt -> IO ()
foreign import ccall ic_history_remove_last   :: IO ()
foreign import ccall ic_history_clear         :: IO ()
foreign import ccall ic_history_add           :: CString -> IO ()

-- | @setHistory filename maxEntries@: 
-- Enable history that is persisted to the given file path with a given maximum number of entries.
-- Use -1 for the default entries (200).
-- See also 'enableHistoryDuplicates'.
setHistory :: FilePath -> Int -> IO ()
setHistory fname maxEntries
  = withUTF8String0 fname $ \cfname ->
    do ic_set_history cfname (toEnum maxEntries)

-- | Isocline automatically adds input of more than 1 character to the history.
-- This command removes the last entry.
historyRemoveLast :: IO ()
historyRemoveLast 
  = ic_history_remove_last

-- | Clear the history.
historyClear :: IO ()
historyClear
  = ic_history_clear

-- | @historyAdd entry@: add @entry@ to the history.
historyAdd :: String -> IO ()
historyAdd entry
  = withUTF8String0 entry $ \centry ->
    do ic_history_add centry 


----------------------------------------------------------------------------
-- Completion
----------------------------------------------------------------------------
-- use our own CBool for compatibility with an older base
type CCBool = CInt

type CCharClassFun = CString -> CLong -> IO CCBool
type CharClassFun  = Char -> Bool

foreign import ccall ic_set_default_completer :: FunPtr CCompleterFun -> IO ()
foreign import ccall "wrapper" ic_make_completer :: CCompleterFun -> IO (FunPtr CCompleterFun)
foreign import ccall "wrapper" ic_make_charclassfun :: CCharClassFun -> IO (FunPtr CCharClassFun)

foreign import ccall ic_add_completion_ex     :: Ptr IcCompletionEnv -> CString -> CString -> CString -> IO CCBool
foreign import ccall ic_add_completion_prim   :: Ptr IcCompletionEnv -> CString -> CString -> CString -> CInt -> CInt -> IO CCBool
foreign import ccall ic_complete_filename     :: Ptr IcCompletionEnv -> CString -> CChar -> CString -> CString -> IO ()
foreign import ccall ic_complete_word         :: Ptr IcCompletionEnv -> CString -> FunPtr CCompleterFun -> FunPtr CCharClassFun -> IO ()
foreign import ccall ic_complete_qword        :: Ptr IcCompletionEnv -> CString -> FunPtr CCompleterFun -> FunPtr CCharClassFun -> IO ()
foreign import ccall ic_complete_qword_ex     :: Ptr IcCompletionEnv -> CString -> FunPtr CCompleterFun -> FunPtr CCharClassFun -> CChar -> CString -> IO ()

foreign import ccall ic_has_completions       :: Ptr IcCompletionEnv -> IO CCBool
foreign import ccall ic_stop_completing       :: Ptr IcCompletionEnv -> IO CCBool

-- | A completion entry
data Completion = Completion { 
  replacement :: String,  -- ^ actual replacement
  display :: String,      -- ^ display of the completion in the completion menu
  help :: String          -- ^ help message 
} deriving (Eq, Show)

-- | Create a completion with just a replacement
completion :: String -> Completion
completion replacement
  = Completion replacement "" ""

-- | @completionFull replacement display help@: Create a completion with a separate display and help string.
completionFull :: String -> String -> String -> Completion
completionFull replacement display help
  = Completion  replacement display help 


-- | Is the given input a prefix of the completion replacement?
isPrefix :: String -> Completion -> Bool
isPrefix input compl
  = isPrefixOf input (replacement compl)

-- | @completionsFor input replacements@: Filter those @replacements@ that 
-- start with the given @input@, and return them as completions.
completionsFor :: String -> [String] -> [Completion]
completionsFor input rs
  = map completion (filter (isPrefixOf input) rs)

-- | Convenience: creates a completer function directly from a list
-- of candidate completion strings. Uses `completionsFor` to filter the 
-- input and `completeWord` to find the word boundary.
-- For example: @'readlineEx' \"myprompt\" (Just ('wordCompleter' completer)) Nothing@.
wordCompleter :: [String] -> (CompletionEnv -> String -> IO ()) 
wordCompleter completions
  = (\cenv input -> completeWord cenv input Nothing (\input -> completionsFor input completions))

-- | @setDefaultCompleter completer@: Set a new tab-completion function @completer@ 
-- that is called by Isocline automatically. 
-- The callback is called with a 'CompletionEnv' context and the current user
-- input up to the cursor.
-- By default the 'completeFileName' completer is used.
-- This overwrites any previously set completer.
setDefaultCompleter :: (CompletionEnv -> String -> IO ()) -> IO ()
setDefaultCompleter completer 
  = do ccompleter <- makeCCompleter (Just completer)
       ic_set_default_completer ccompleter

withCCompleter :: Maybe CompleterFun -> (FunPtr CCompleterFun -> IO a) -> IO a
withCCompleter completer action
  = bracket (makeCCompleter completer) (\cfun -> when (nullFunPtr /= cfun) (freeHaskellFunPtr cfun)) action

makeCCompleter :: Maybe CompleterFun -> IO (FunPtr CCompleterFun)
makeCCompleter Nothing = return nullFunPtr
makeCCompleter (Just completer)
  = ic_make_completer wrapper
  where
    wrapper :: Ptr IcCompletionEnv -> CString -> IO ()
    wrapper rpcomp cprefx
      = do prefx <- peekUTF8String0 cprefx
           completer (CompletionEnv rpcomp) prefx


-- | @addCompletion compl completion@: Inside a completer callback, add a new completion.
-- If 'addCompletion' returns 'True' keep adding completions,
-- but if it returns 'False' an effort should be made to return from the completer
-- callback without adding more completions.
addCompletion :: CompletionEnv -> Completion -> IO Bool
addCompletion (CompletionEnv rpc) (Completion replacement display help)
  = withUTF8String replacement $ \crepl ->
    withUTF8String0 display $ \cdisplay ->
    withUTF8String0 help $ \chelp ->    
    do cbool <- ic_add_completion_ex rpc crepl cdisplay chelp
       return (fromEnum cbool /= 0)

-- | @addCompletionPrim compl completion deleteBefore deleteAfter@: 
-- Primitive add completion, use with care and call only directly inside a completer callback.
-- If 'addCompletion' returns 'True' keep adding completions,
-- but if it returns 'False' an effort should be made to return from the completer
-- callback without adding more completions.
addCompletionPrim :: CompletionEnv -> Completion -> Int -> Int -> IO Bool
addCompletionPrim (CompletionEnv rpc) (Completion replacement display help) deleteBefore deleteAfter
  = withUTF8String replacement $ \crepl ->
    withUTF8String0 display $ \cdisplay ->
    withUTF8String0 help $ \chelp ->
    do cbool <- ic_add_completion_prim rpc crepl cdisplay chelp (toEnum deleteBefore) (toEnum deleteAfter)
       return (fromEnum cbool /= 0)

    
-- | @addCompletions compl completions@: add multiple completions at once.
-- If 'addCompletions' returns 'True' keep adding completions,
-- but if it returns 'False' an effort should be made to return from the completer
-- callback without adding more completions.
addCompletions :: CompletionEnv -> [Completion] -> IO Bool
addCompletions compl [] = return True
addCompletions compl (c:cs)
  = do continue <- addCompletion compl c
       if (continue) 
         then addCompletions compl cs
         else return False

-- | @completeFileName compls input dirSep roots extensions@: 
-- Complete filenames with the given @input@, a possible directory separator @dirSep@, 
-- a list of root folders @roots@  to search from
-- (by default @["."]@), and a list of extensions to match (use @[]@ to match any extension).
-- The directory separator is used when completing directory names.
-- For example, using g @\'/\'@ as a directory separator, we get:
--
-- > /ho         --> /home/
-- > /home/.ba   --> /home/.bashrc
--
completeFileName :: CompletionEnv -> String -> Maybe Char -> [FilePath] -> [String] -> IO ()
completeFileName (CompletionEnv rpc) prefx dirSep roots extensions
  = withUTF8String prefx $ \cprefx ->
    withUTF8String0 (concat (intersperse ";" roots)) $ \croots ->
    withUTF8String0 (concat (intersperse ";" extensions)) $ \cextensions ->
    do let cdirSep = case dirSep of
                       Nothing -> toEnum 0
                       Just c  -> castCharToCChar c
       ic_complete_filename rpc cprefx cdirSep croots cextensions

-- | @completeWord compl input isWordChar completer@: 
-- Complete a /word/ (or /token/) and calls the user @completer@ function with just the current word
-- (instead of the whole input)
-- Takes the 'CompletionEnv' environment @compl@, the current @input@, an possible
-- @isWordChar@ function, and a user defined 
-- @completer@ function that is called with adjusted input which 
-- is limited to the /word/ just before the cursor.
-- Pass 'Nothing' to @isWordChar@ for the default @not . separator@
-- where @separator = \c -> c `elem` \" \\t\\r\\n,.;:/\\\\(){}[]\"@.
completeWord :: CompletionEnv -> String -> Maybe (Char -> Bool) -> (String -> [Completion]) -> IO () 
completeWord cenv input isWordChar completer 
  = completeWordPrim cenv input isWordChar cenvCompleter
  where
    cenvCompleter cenv input
      = do addCompletions cenv (completer input)
           return ()

-- | @completeQuotedWord compl input isWordChar completer@: 
-- Complete a /word/ taking care of automatically quoting and escaping characters.
-- Takes the 'CompletionEnv' environment @compl@, the current @input@, and a user defined 
-- @completer@ function that is called with adjusted input which is unquoted, unescaped,
-- and limited to the /word/ just before the cursor.
-- For example, with a @hello world@ completion, we get:
--
-- > hel        -->  hello\ world
-- > hello\ w   -->  hello\ world
-- > hello w    -->                   # no completion, the word is just 'w'>
-- > "hel       -->  "hello world" 
-- > "hello w   -->  "hello world"
--
-- The call @('completeWord' compl prefx isWordChar fun)@ is a short hand for 
-- @('completeQuotedWord' compl prefx isWordChar \'\\\\\' \"\'\\\"\" fun)@.
-- Pass 'Nothing' to @isWordChar@ for the default @not . separator@
-- where @separator = \c -> c `elem` \" \\t\\r\\n,.;:/\\\\(){}[]\"@.
completeQuotedWord :: CompletionEnv -> String -> Maybe (Char -> Bool) -> (String -> [Completion]) -> IO () 
completeQuotedWord cenv input isWordChar completer 
  = completeWordPrim cenv input isWordChar cenvCompleter
  where
    cenvCompleter cenv input
      = do addCompletions cenv (completer input)
           return ()
  
-- | @completeQuotedWordEx compl input isWordChar escapeChar quoteChars completer@: 
-- Complete a /word/ taking care of automatically quoting and escaping characters.
-- Takes the 'CompletionEnv' environment @compl@, the current @input@, and a user defined 
-- @completer@ function that is called with adjusted input which is unquoted, unescaped,
-- and limited to the /word/ just before the cursor.
-- Unlike 'completeQuotedWord', this function can specify 
-- the /escape/ character and the /quote/ characters.
-- See also 'completeWord'.
completeQuotedWordEx :: CompletionEnv -> String -> Maybe (Char -> Bool) -> Maybe Char -> String -> (String -> [Completion]) -> IO () 
completeQuotedWordEx cenv input isWordChar escapeChar quoteChars completer 
  = completeQuotedWordPrimEx cenv input isWordChar escapeChar quoteChars cenvCompleter 
  where
    cenvCompleter cenv input 
      = do addCompletions cenv (completer input)
           return ()


-- | @completeWord compl input isWordChar completer@: 
-- Complete a /word/,/token/ and calls the user @completer@ function with just the current word
-- (instead of the whole input)
-- Takes the 'CompletionEnv' environment @compl@, the current @input@, an possible
-- @isWordChar@ function, and a user defined 
-- @completer@ function that is called with adjusted input which 
-- is limited to the /word/ just before the cursor.
-- Pass 'Nothing' to @isWordChar@ for the default @not . separator@
-- where @separator = \c -> c `elem` \" \\t\\r\\n,.;:/\\\\(){}[]\"@.
completeWordPrim :: CompletionEnv -> String -> Maybe (Char -> Bool) -> (CompletionEnv -> String -> IO ()) -> IO () 
completeWordPrim (CompletionEnv rpc) prefx isWordChar completer 
  = withUTF8String prefx $ \cprefx ->
    withCharClassFun isWordChar $ \cisWordChar ->
    withCCompleter (Just completer) $ \ccompleter ->
    do ic_complete_word rpc cprefx ccompleter cisWordChar


-- | @completeWordPrim compl input isWordChar completer@: 
-- Complete a /word/ taking care of automatically quoting and escaping characters.
-- Takes the 'CompletionEnv' environment @compl@, the current @input@, and a user defined 
-- @completer@ function that is called with adjusted input which is unquoted, unescaped,
-- and limited to the /word/ just before the cursor.
-- For example, with a @hello world@ completion, we get:
--
-- > hel        -->  hello\ world
-- > hello\ w   -->  hello\ world
-- > hello w    -->                   # no completion, the word is just 'w'>
-- > "hel       -->  "hello world" 
-- > "hello w   -->  "hello world"
--
-- The call @('completeWordPrim' compl prefx isWordChar fun)@ is a short hand for 
-- @('completeQuotedWordPrim' compl prefx isWordChar \'\\\\\' \"\'\\\"\" fun)@.
-- Pass 'Nothing' to @isWordChar@ for the default @not . separator@
-- where @separator = \c -> c `elem` \" \\t\\r\\n,.;:/\\\\(){}[]\"@.
completeQuotedWordPrim :: CompletionEnv -> String -> Maybe (Char -> Bool) -> (CompletionEnv -> String -> IO ()) -> IO () 
completeQuotedWordPrim (CompletionEnv rpc) prefx isWordChar completer
  = withUTF8String prefx $ \cprefx ->
    withCharClassFun isWordChar $ \cisWordChar ->
    withCCompleter (Just completer) $ \ccompleter ->
    do ic_complete_qword rpc cprefx ccompleter cisWordChar
  

-- | @completeQuotedWordPrim compl input isWordChar escapeChar quoteChars completer@: 
-- Complete a /word/ taking care of automatically quoting and escaping characters.
-- Takes the 'CompletionEnv' environment @compl@, the current @input@, and a user defined 
-- @completer@ function that is called with adjusted input which is unquoted, unescaped,
-- and limited to the /word/ just before the cursor.
-- Unlike 'completeWord', this function takes an explicit function to determine /word/ characters,
-- the /escape/ character, and a string of /quote/ characters.
-- See also 'completeWord'.
completeQuotedWordPrimEx :: CompletionEnv -> String -> Maybe (Char -> Bool) -> Maybe Char -> String -> (CompletionEnv -> String -> IO ()) ->  IO () 
completeQuotedWordPrimEx (CompletionEnv rpc) prefx isWordChar escapeChar quoteChars completer
  = withUTF8String prefx $ \cprefx ->
    withUTF8String0 quoteChars $ \cquoteChars ->
    withCharClassFun isWordChar $ \cisWordChar ->
    withCCompleter (Just completer) $ \ccompleter ->
    do let cescapeChar = case escapeChar of
                          Nothing -> toEnum 0
                          Just c  -> castCharToCChar c                      
       ic_complete_qword_ex rpc cprefx ccompleter cisWordChar cescapeChar cquoteChars
       

withCharClassFun :: Maybe (Char -> Bool) -> (FunPtr CCharClassFun -> IO a) -> IO a
withCharClassFun isInClass action
  = bracket (makeCharClassFun isInClass) (\cfun -> when (nullFunPtr /= cfun) (freeHaskellFunPtr cfun))  action 

makeCharClassFun :: Maybe (Char -> Bool) -> IO (FunPtr CCharClassFun)
makeCharClassFun Nothing = return nullFunPtr
makeCharClassFun (Just isInClass)
  = let charClassFun :: CString -> CLong -> IO CCBool
        charClassFun cstr clen 
          = let len = (fromIntegral clen :: Int)
            in if (len <= 0) then return (cbool False)
                else do s <- peekCStringLen (cstr,len)
                        return (if null s then (cbool False) else cbool (isInClass (head s)))
    in do ic_make_charclassfun charClassFun
          

-- | If this returns 'True' an effort should be made to stop completing and return from the callback.
stopCompleting :: CompletionEnv -> IO Bool
stopCompleting (CompletionEnv rpc)
  = uncbool $ ic_stop_completing rpc

-- | Have any completions be generated so far?
hasCompletions :: CompletionEnv -> IO Bool
hasCompletions (CompletionEnv rpc)
  = uncbool $ ic_has_completions rpc



----------------------------------------------------------------------------
-- Syntax highlighting
----------------------------------------------------------------------------

foreign import ccall ic_set_default_highlighter     :: FunPtr CHighlightFun -> Ptr () -> IO ()
foreign import ccall "wrapper" ic_make_highlight_fun:: CHighlightFun -> IO (FunPtr CHighlightFun)
foreign import ccall ic_highlight                   :: Ptr IcHighlightEnv -> CLong -> CLong -> CString -> IO ()
foreign import ccall ic_highlight_formatted         :: Ptr IcHighlightEnv -> CString -> CString -> IO ()


-- | Set a syntax highlighter.
-- There can only be one highlight function, setting it again disables the previous one.
setDefaultHighlighter :: (HighlightEnv -> String -> IO ()) -> IO ()
setDefaultHighlighter highlighter
  = do chighlighter <- makeCHighlighter (Just highlighter)
       ic_set_default_highlighter chighlighter nullPtr

makeCHighlighter :: Maybe (HighlightEnv -> String -> IO ()) -> IO (FunPtr CHighlightFun)
makeCHighlighter Nothing = return nullFunPtr 
makeCHighlighter (Just highlighter)
  = ic_make_highlight_fun wrapper
  where 
    wrapper :: Ptr IcHighlightEnv -> CString -> Ptr () -> IO ()
    wrapper henv cinput carg
      = do input <- peekUTF8String0 cinput
           highlighter (HighlightEnv henv) input


-- | @highlight henv pos len style@: Set the style of @len@ characters
-- starting at position @pos@ in the input 
highlight :: HighlightEnv -> Int -> Int -> String -> IO ()
highlight (HighlightEnv henv) pos len style
  = withUTF8String0 style $ \cstyle ->
    do ic_highlight henv (clong (-pos)) (clong (-len)) cstyle


-- | A style for formatted strings ('Fmt').
-- For example, a style can be @"red"@ or @"b #7B3050"@. 
-- See the full list of valid [properties](https://github.com/daanx/isocline#bbcode-format)
type Style = String

-- | A string with [bbcode](https://github.com/daanx/isocline#bbcode-format) formatting.
-- For example @"[red]this is red[\/]"@.n
type Fmt   = String

-- | Use an rich text formatted highlighter from inside a highlighter callback.
highlightFmt :: (String -> Fmt) -> (HighlightEnv -> String -> IO ())
highlightFmt highlight (HighlightEnv henv) input 
  = withUTF8String0 input $ \cinput ->
    withUTF8String0 (highlight input) $ \cfmt ->
    do ic_highlight_formatted henv cinput cfmt


-- | Style a string, e.g. @style "b red" "bold and red"@ (which is equivalent to @"[b red]bold and red[\/]"@).
-- See the repo for a full description of all [styles](https://github.com/daanx/isocline#bbcode-format).
style :: Style -> Fmt -> Fmt
style st s
  = if null st then s else ("[" ++ st ++ "]" ++ s ++ "[/]") 

-- | Escape a string so no tags are interpreted as formatting.
plain :: String -> Fmt
plain s
  = if (any (\c -> (c == '[' || c == ']')) s) then "[!pre]" ++ s ++ "[/pre]" else s

-- | Style a string that is printed as is without interpreting markup inside it (using `plain`).
pre :: Style -> String -> Fmt
pre st s
  = style st (plain s)

-- | Set a syntax highlighter that uses a pure function that returns a bbcode
-- formatted string (using 'style', 'plain' etc). See 'highlightFmt' for more information.
-- There can only be one highlight function, setting it again disables the previous one.
setDefaultFmtHighlighter :: (String -> Fmt) -> IO ()
setDefaultFmtHighlighter highlight 
  = setDefaultHighlighter (highlightFmt highlight)





----------------------------------------------------------------------------
-- Print rich text
----------------------------------------------------------------------------

foreign import ccall ic_print           :: CString -> IO ()
foreign import ccall ic_println         :: CString -> IO ()
foreign import ccall ic_style_def       :: CString -> CString -> IO ()
foreign import ccall ic_style_open      :: CString -> IO ()
foreign import ccall ic_style_close     :: IO ()

-- | Output rich formatted text containing [bbcode](https://github.com/daanx/isocline#bbcode-format).
-- For example: @putFmt \"[b]bold [red]and red[\/][\/]\"@
-- All unclosed tags are automatically closed (but see also 'styleOpen').
-- See the repo for more information about [formatted output](https://github.com/daanx/isocline#formatted-output).
putFmt :: Fmt -> IO ()
putFmt s 
  = withUTF8String0 s $ \cs -> 
    do ic_print cs

-- | Output rich formatted text containing bbcode's ending with a newline.
putFmtLn :: Fmt -> IO ()
putFmtLn s 
  = withUTF8String0 s $ \cs -> 
    do ic_println cs

-- | Define (or redefine) a style.
-- For example @styleDef "warning" "crimon underline"@,
-- and then use it as @'putFmtLn' "[warning]this is a warning[/]"@. 
-- This can be very useful for theming your application with semantic styles.
-- See also [formatted output](https://github.com/daanx/isocline#formatted-output)
styleDef :: String -> Style -> IO ()
styleDef name style
  = withUTF8String0 name $ \cname ->
    withUTF8String0 style $ \cstyle ->
    do ic_style_def cname cstyle

-- | Open a style that is active for all 'putFmt' and 'putFmtLn' until it is closed again (`styleClose`).
styleOpen :: Style -> IO ()
styleOpen style
  = withUTF8String0 style $ \cstyle ->
    do ic_style_open cstyle        

-- | Close a previously opened style.
styleClose :: IO ()
styleClose 
  = ic_style_close

-- | Use a style over an action.
withStyle :: Style -> IO a -> IO a
withStyle style action
  = bracket (styleOpen style) (\() -> styleClose) (\() -> action) 


----------------------------------------------------------------------------
-- Terminal
----------------------------------------------------------------------------

foreign import ccall ic_term_init       :: IO ()
foreign import ccall ic_term_done       :: IO ()
foreign import ccall ic_term_flush      :: IO ()
foreign import ccall ic_term_write      :: CString -> IO ()
foreign import ccall ic_term_writeln    :: CString -> IO ()
foreign import ccall ic_term_underline  :: CCBool -> IO ()
foreign import ccall ic_term_reverse    :: CCBool -> IO ()
foreign import ccall ic_term_color_ansi :: CCBool -> CInt -> IO ()
foreign import ccall ic_term_color_rgb  :: CCBool -> CInt -> IO ()
foreign import ccall ic_term_style      :: CString -> IO ()
foreign import ccall ic_term_reset      :: IO ()

-- | Initialize the terminal for the @term@ functions.
-- Does nothing on most platforms but on windows enables UTF8 output
-- and potentially enables virtual terminal processing.
-- See also 'withTerm'.
termInit :: IO ()
termInit 
  = ic_term_init

-- | Done using @term@ functions.
-- See also 'withTerm'.
termDone :: IO ()
termDone 
  = ic_term_done

-- | Use the @term@ functions (brackets 'termInit' and 'termDone').
withTerm :: IO a -> IO a
withTerm action
  = bracket termInit (\() -> termDone) (\() -> action) 

-- | Flush terminal output. Happens automatically on newline (@'\\n'@) characters as well.
termFlush :: IO ()
termFlush
  = ic_term_flush  

-- | Write output to the terminal where ANSI CSI sequences are
-- handled portably across platforms (including Windows).
termWrite :: String -> IO ()
termWrite s
  = withUTF8String0 s $ \cs -> ic_term_write cs

-- | Write output with a ending newline to the terminal where 
-- ANSI CSI sequences are handled portably across platforms (including Windows).
termWriteLn :: String -> IO ()
termWriteLn s
  = withUTF8String0 s $ \cs -> ic_term_writeln cs  

-- | Set the terminal text color as a hexadecimal number @0x@rrggbb. 
-- The color is auto adjusted for terminals with less colors.
termColor :: Int -> IO ()
termColor color
  = ic_term_color_rgb (cbool True) (toEnum color)

-- | Set the terminal text background color. The color is auto adjusted for terminals with less colors.
termBgColor :: Int -> IO ()
termBgColor color
  = ic_term_color_rgb (cbool False) (toEnum color)

-- | Set the terminal text color as an ANSI palette color (between @0@ and @255@). Use 256 for the default.
-- The color is auto adjusted for terminals with less colors.
termColorAnsi :: Int -> IO ()
termColorAnsi color
  = ic_term_color_ansi (cbool True) (toEnum color)

-- | Set the terminal text background color as an ANSI palette color (between @0@ and @255@). Use 256 for the default.
-- The color is auto adjusted for terminals with less colors.
termBgColorAnsi :: Int -> IO ()
termBgColorAnsi color
  = ic_term_color_ansi (cbool False) (toEnum color)

-- | Set the terminal attributes from a style
termStyle :: Style -> IO ()
termStyle style
  = withUTF8String0 style $ \cstyle ->
    do ic_term_style cstyle

-- | Set the terminal text underline mode.
termUnderline :: Bool -> IO ()
termUnderline enable
  = ic_term_underline (cbool enable)  

-- | Set the terminal text reverse video mode.
termReverse :: Bool -> IO ()
termReverse enable
  = ic_term_reverse (cbool enable)  

-- | Reset the terminal text mode to defaults
termReset :: IO ()
termReset 
  = ic_term_reset


----------------------------------------------------------------------------
-- Configuration
----------------------------------------------------------------------------
foreign import ccall ic_set_prompt_marker :: CString -> CString -> IO ()
foreign import ccall ic_get_prompt_marker :: IO CString
foreign import ccall ic_get_continuation_prompt_marker :: IO CString
foreign import ccall ic_enable_multiline  :: CCBool -> IO CCBool
foreign import ccall ic_enable_beep       :: CCBool -> IO CCBool
foreign import ccall ic_enable_color      :: CCBool -> IO CCBool
foreign import ccall ic_enable_auto_tab   :: CCBool -> IO CCBool
foreign import ccall ic_enable_inline_help:: CCBool -> IO CCBool
foreign import ccall ic_enable_hint       :: CCBool -> IO CCBool
foreign import ccall ic_set_hint_delay    :: CLong -> IO CLong
foreign import ccall ic_enable_highlight  :: CCBool -> IO CCBool
foreign import ccall ic_enable_history_duplicates :: CCBool -> IO CCBool
foreign import ccall ic_enable_completion_preview :: CCBool -> IO CCBool
foreign import ccall ic_enable_multiline_indent   :: CCBool -> IO CCBool
foreign import ccall ic_enable_brace_matching     :: CCBool -> IO CCBool
foreign import ccall ic_enable_brace_insertion    :: CCBool -> IO CCBool
foreign import ccall ic_set_matching_braces       :: CString -> IO ()
foreign import ccall ic_set_insertion_braces      :: CString -> IO ()

cbool :: Bool -> CCBool
cbool True  = toEnum 1
cbool False = toEnum 0

uncbool :: IO CCBool -> IO Bool
uncbool action
  = do i <- action
       return (i /= toEnum 0)

clong :: Int -> CLong
clong l = toEnum l


-- | @setPromptMarker marker multiline_marker@: Set the prompt @marker@ (by default @\"> \"@). 
-- and a possible different continuation prompt marker @multiline_marker@ for multiline 
-- input (defaults to @marker@).
setPromptMarker :: String -> String -> IO ()
setPromptMarker marker multiline_marker  
  = withUTF8String0 marker $ \cmarker ->
    withUTF8String0 multiline_marker $ \cmultiline_marker ->
    do ic_set_prompt_marker cmarker cmultiline_marker


-- | Get the current prompt marker.
getPromptMarker :: IO String
getPromptMarker 
  = do cstr  <- ic_get_prompt_marker
       if (nullPtr == cstr) 
         then return ""
         else do cstr2 <- ic_strdup cstr
                 peekUTF8String0 cstr2

-- | Get the current prompt continuation marker for multi-line input.
getContinuationPromptMarker :: IO String
getContinuationPromptMarker 
  = do cstr <- ic_get_continuation_prompt_marker
       if (nullPtr == cstr) 
         then return ""
         else do cstr2 <- ic_strdup cstr
                 peekUTF8String0 cstr2


-- | Disable or enable multi-line input (enabled by default).
-- Returns the previous value.
enableMultiline :: Bool -> IO Bool
enableMultiline enable
  = do uncbool $ ic_enable_multiline (cbool enable)

-- | Disable or enable sound (enabled by default).
-- | A beep is used when tab cannot find any completion for example.
-- Returns the previous value.
enableBeep :: Bool -> IO Bool
enableBeep enable
  = do uncbool $ ic_enable_beep (cbool enable)

-- | Disable or enable color output (enabled by default).
-- Returns the previous value.
enableColor :: Bool -> IO Bool
enableColor enable
  = do uncbool $ ic_enable_color (cbool enable)

-- | Disable or enable duplicate entries in the history (duplicate entries are not allowed by default).
-- Returns the previous value.
enableHistoryDuplicates :: Bool -> IO Bool
enableHistoryDuplicates enable
  = do uncbool $ ic_enable_history_duplicates (cbool enable)


-- | Disable or enable automatic tab completion after a completion 
-- to expand as far as possible if the completions are unique. (disabled by default).
-- Returns the previous value.
enableAutoTab :: Bool -> IO Bool
enableAutoTab enable
  = do uncbool $ ic_enable_auto_tab (cbool enable)


-- | Disable or enable short inline help message (for history search etc.) (enabled by default).
-- Pressing F1 always shows full help regardless of this setting. 
-- Returns the previous value.
enableInlineHelp :: Bool -> IO Bool
enableInlineHelp enable
  = do uncbool $ ic_enable_inline_help (cbool enable)

-- | Disable or enable preview of a completion selection (enabled by default)
-- Returns the previous value.
enableCompletionPreview :: Bool -> IO Bool
enableCompletionPreview enable
  = do uncbool $ ic_enable_completion_preview (cbool enable)


-- | Disable or enable brace matching (enabled by default)
-- Returns the previous value.
enableBraceMatching :: Bool -> IO Bool
enableBraceMatching enable
  = do uncbool $ ic_enable_brace_matching (cbool enable)

-- | Disable or enable automatic close brace insertion (enabled by default)
-- Returns the previous value.
enableBraceInsertion :: Bool -> IO Bool
enableBraceInsertion enable
  = do uncbool $ ic_enable_brace_insertion (cbool enable)

-- | Set pairs of matching braces, by default @\"(){}[]\"@.
setMatchingBraces :: String -> IO ()
setMatchingBraces bracePairs
  = withUTF8String0 bracePairs $ \cbracePairs ->
    do ic_set_matching_braces cbracePairs

-- | Set pairs of auto insertion braces, by default @\"(){}[]\\\"\\\"\'\'\"@.
setInsertionBraces :: String -> IO ()
setInsertionBraces bracePairs
  = withUTF8String0 bracePairs $ \cbracePairs ->
    do ic_set_insertion_braces cbracePairs


-- | Disable or enable automatic indentation to line up the
-- multiline prompt marker with the initial prompt marker (enabled by default).
-- Returns the previous value.
-- See also 'setPromptMarker'.
enableMultilineIndent :: Bool -> IO Bool
enableMultilineIndent enable
  = do uncbool $ ic_enable_multiline_indent (cbool enable)

-- | Disable or enable automatic inline hinting (enabled by default)
-- Returns the previous value.
enableHint :: Bool -> IO Bool
enableHint enable
  = do uncbool $ ic_enable_hint (cbool enable)

-- | Disable or enable syntax highlighting (enabled by default).
-- Returns the previous value.
enableHighlight :: Bool -> IO Bool
enableHighlight enable
  = do uncbool $ ic_enable_highlight (cbool enable)

-- | Set the delay in milliseconds before a hint is displayed (500ms by default)
-- See also 'enableHint'
setHintDelay :: Int -> IO Int
setHintDelay ms
  = do cl <- ic_set_hint_delay (toEnum ms)
       return (fromEnum cl)


----------------------------------------------------------------------------
-- UTF8 Strings
----------------------------------------------------------------------------

withUTF8String0 :: String -> (CString -> IO a) -> IO a
withUTF8String0 s action
  = if (null s) then action nullPtr else withUTF8String s action

peekUTF8String0 :: CString -> IO String
peekUTF8String0 cstr
  = if (nullPtr == cstr) then return "" else peekUTF8String cstr

peekUTF8StringMaybe :: CString -> IO (Maybe String)
peekUTF8StringMaybe cstr
  = if (nullPtr == cstr) then return Nothing 
     else do s <- peekUTF8String cstr
             return (Just s)

peekUTF8String :: CString -> IO String
peekUTF8String cstr
  = do bstr <- B.packCString cstr
       return (T.unpack (TE.decodeUtf8With lenientDecode bstr))

withUTF8String :: String -> (CString -> IO a) -> IO a
withUTF8String str action
  = do let bstr = TE.encodeUtf8 (T.pack str)
       B.useAsCString bstr action
       
