--  | Output interface.
--
--  Access to the current terminal or a specific terminal device.
--
--  See also:
--
--  1. "Graphics.Vty.Output": This instantiates an abtract interface to the terminal interface based
--  on the TERM and COLORTERM environment variables. 
--  
--  2. "Graphics.Vty.Output.Interface": Defines the generic interface all terminals need to implement.
--
--  3. "Graphics.Vty.Output.TerminfoBased": Defines a terminal instance that uses terminfo for all
--  control strings.  No attempt is made to change the character set to UTF-8 for these terminals.
--  I don't know a way to reliably determine if that is required or how to do so.
--
--  4. "Graphics.Vty.Output.XTermColor": This module contains an interface suitable for xterm-like
--  terminals. These are the terminals where TERM == xterm. This does use terminfo for as many
--  control codes as possible. 
module Graphics.Vty.Output ( module Graphics.Vty.Output
                           , Output(..) -- \todo hide constructors
                           , AssumedState(..)
                           , DisplayContext(..) -- \todo hide constructors
                           , outputPicture
                           , displayContext
                           )
    where


import Graphics.Vty.Prelude

import Graphics.Vty.Config

import Graphics.Vty.Output.Interface
import Graphics.Vty.Output.MacOSX as MacOSX
import Graphics.Vty.Output.XTermColor as XTermColor
import Graphics.Vty.Output.TerminfoBased as TerminfoBased

import Blaze.ByteString.Builder (writeToByteString)

import Control.Exception ( SomeException, try )
import Control.Monad.Trans

import Data.List ( isPrefixOf )

import GHC.IO.Handle

import System.Environment
import System.IO

-- | Returns a `Output` for the current terminal as determined by TERM.
--
-- The specific Output implementation used is hidden from the API user. All terminal implementations
-- are assumed to perform more, or less, the same. Currently all implementations use terminfo for at
-- least some terminal specific information. This is why platforms without terminfo are not
-- supported. However, as mentioned before, any specifics about it being based on terminfo are
-- hidden from the API user.  If a terminal implementation is developed for a terminal for a
-- platform without terminfo support then Vty should work as expected on that terminal.
--
-- Selection of a terminal is done as follows:
--
--      * If TERM == xterm
--          then the terminal might be one of the Mac OS X .app terminals. Check if that might be
--          the case and use MacOSX if so.
--          otherwise use XTermColor.
--
--      * for any other TERM value TerminfoBased is used.
--
-- To differentiate between Mac OS X terminals this uses the TERM_PROGRAM environment variable.
-- However, an xterm started by Terminal or iTerm *also* has TERM_PROGRAM defined since the
-- environment variable is not reset/cleared by xterm. However a Terminal.app or iTerm.app started
-- from an xterm under X11 on mac os x will likely be done via open. Since this does not propogate
-- environment variables (I think?) this assumes that XTERM_VERSION will never be set for a true
-- Terminal.app or iTerm.app session.
--
-- The file descriptor used for output will a be a duplicate of the current stdout file descriptor.
--
-- \todo add an implementation for windows that does not depend on terminfo. Should be installable
-- with only what is provided in the haskell platform. Use ansi-terminal
outputForCurrentTerminal :: ( Applicative m, MonadIO m ) => Config -> m Output
outputForCurrentTerminal _config = do
    termType <- liftIO $ getEnv "TERM"
    outHandle <- liftIO $ hDuplicate stdout
    outputForNameAndIO termType outHandle

-- | gives an output method structure for a terminal with the given name and the given 'Handle'.
outputForNameAndIO :: (Applicative m, MonadIO m) => String -> Handle -> m Output
outputForNameAndIO termType outHandle = do
    t <- if "xterm" `isPrefixOf` termType
        then do
            maybeTerminalApp <- mGetEnv "TERM_PROGRAM"
            case maybeTerminalApp of
                Nothing
                    -> XTermColor.reserveTerminal termType outHandle
                Just v | v == "Apple_Terminal" || v == "iTerm.app" 
                    -> do
                        maybeXterm <- mGetEnv "XTERM_VERSION"
                        case maybeXterm of
                            Nothing -> MacOSX.reserveTerminal v outHandle
                            Just _  -> XTermColor.reserveTerminal termType outHandle
                -- Assume any other terminal that sets TERM_PROGRAM to not be an OS X terminal.app
                -- like terminal?
                _   -> XTermColor.reserveTerminal termType outHandle
        -- Not an xterm-like terminal. try for generic terminfo.
        else TerminfoBased.reserveTerminal termType outHandle
    return t
    where
        mGetEnv var = do
            mv <- liftIO $ try $ getEnv var
            case mv of
                Left (_e :: SomeException)  -> return $ Nothing
                Right v -> return $ Just v

-- | Sets the cursor position to the given output column and row. 
--
-- This is not necessarially the same as the character position with the same coordinates.
-- Characters can be a variable number of columns in width.
--
-- Currently, the only way to set the cursor position to a given character coordinate is to specify
-- the coordinate in the Picture instance provided to outputPicture or refresh.
setCursorPos :: MonadIO m => Output -> Int -> Int -> m ()
setCursorPos t x y = do
    bounds <- displayBounds t
    when (x >= 0 && x < regionWidth bounds && y >= 0 && y < regionHeight bounds) $ do
        dc <- displayContext t bounds
        liftIO $ outputByteBuffer t $ writeToByteString $ writeMoveCursor dc x y

-- | Hides the cursor
hideCursor :: MonadIO m => Output -> m ()
hideCursor t = do
    bounds <- displayBounds t
    dc <- displayContext t bounds
    liftIO $ outputByteBuffer t $ writeToByteString $ writeHideCursor dc
    
-- | Shows the cursor
showCursor :: MonadIO m => Output -> m ()
showCursor t = do
    bounds <- displayBounds t
    dc <- displayContext t bounds
    liftIO $ outputByteBuffer t $ writeToByteString $ writeShowCursor dc