{-# LANGUAGE Trustworthy #-}
{-# OPTIONS_HADDOCK hide #-}

module System.Console.ANSI.Windows.Detect
  (
    ANSISupport (..)
  , ConsoleDefaultState (..)
  , aNSISupport
  , detectHandleSupportsANSI
  ) where

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif

import Control.Exception (SomeException(..), throwIO, try)
import Data.Bits ((.&.), (.|.))
import System.Console.MinTTY (isMinTTYHandle)
import System.IO (Handle, hIsWritable, stdout)
import System.IO.Unsafe (unsafePerformIO)

import System.Console.ANSI.Windows.Foreign (ConsoleException(..),
  CONSOLE_SCREEN_BUFFER_INFO (..), DWORD, HANDLE, WORD,
  bACKGROUND_INTENSE_WHITE, eNABLE_VIRTUAL_TERMINAL_PROCESSING,
  fOREGROUND_INTENSE_WHITE, getConsoleMode, getConsoleScreenBufferInfo,
  iNVALID_HANDLE_VALUE, nullHANDLE, setConsoleMode, withHandleToHANDLE)

-- | The default state of the console.
data ConsoleDefaultState = ConsoleDefaultState
  { defaultForegroundAttributes :: WORD -- ^ Foreground attributes
  , defaultBackgroundAttributes :: WORD -- ^ Background attributes
  } deriving (Eq, Show)

-- | How the console is assumed to support ANSI control codes.
data ANSISupport
  = Native                       -- ^ Assume ANSI-enabled
  | Emulated ConsoleDefaultState -- ^ Not ANSI-enabled (including the state of
                                 -- the console when that status was determined)
  deriving (Eq, Show)

-- | Terminals on Windows
data Terminal
  = NativeANSIEnabled    -- ^ Windows 10 (Command Prompt or PowerShell)
  | NativeANSIIncapable  -- ^ Versions before Windows 10 (Command Prompt or
                         -- PowerShell)
  | Mintty               -- ^ ANSI-enabled
  | UnknownTerminal

-- | This function assumes that once it is first established whether or not the
-- Windows console requires emulation, that will not change. If the console
-- requires emulation, the state of the console is considered to be its default
-- state.
{-# NOINLINE aNSISupport #-}
aNSISupport :: ANSISupport
aNSISupport = unsafePerformIO $ withHandleToHANDLE stdout $ withHANDLE
  (throwIO $ ConsoleException 6)  -- Invalid handle or no handle
  (\h -> do
    terminal <- handleToTerminal h
    case terminal of
      NativeANSIIncapable -> Emulated <$> consoleDefaultState h
      _                   -> return Native)
 where
  consoleDefaultState h = do
    info <- getConsoleScreenBufferInfo h
    let attributes = csbi_attributes info
        fgAttributes = attributes .&. fOREGROUND_INTENSE_WHITE
        bgAttributes = attributes .&. bACKGROUND_INTENSE_WHITE
    return ConsoleDefaultState
      { defaultForegroundAttributes = fgAttributes
      , defaultBackgroundAttributes = bgAttributes }

-- | This function tests that the handle is writable. If what is attached to the
-- handle is not recognised as a known terminal, it returns @return Nothing@.
detectHandleSupportsANSI :: Handle -> IO (Maybe Bool)
detectHandleSupportsANSI handle = do
  isWritable <- hIsWritable handle
  if isWritable
    then withHandleToHANDLE handle $ withHANDLE
      (return $ Just False)  -- Invalid handle or no handle
      (\h -> do
        terminal <- handleToTerminal h
        case terminal of
          NativeANSIIncapable -> return (Just False)
          UnknownTerminal     -> return Nothing  -- Not sure!
          _                   -> return (Just True))
    else return (Just False)  -- Not an output handle

-- | This function assumes that the Windows handle is writable.
handleToTerminal :: HANDLE -> IO Terminal
handleToTerminal h = do
  tryMode <- try (getConsoleMode h) :: IO (Either SomeException DWORD)
  case tryMode of
    Left _     -> do  -- No ConHost mode
      isMinTTY <- isMinTTYHandle h
      if isMinTTY
        then return Mintty  -- 'mintty' terminal emulator
        else return UnknownTerminal  -- Not sure!
    Right mode -> if mode .&. eNABLE_VIRTUAL_TERMINAL_PROCESSING /= 0
      then return NativeANSIEnabled  -- VT processing already enabled
      else do
        let mode' = mode .|. eNABLE_VIRTUAL_TERMINAL_PROCESSING
        trySetMode <- try (setConsoleMode h mode')
          :: IO (Either SomeException ())
        case trySetMode of
          Left _   -> return NativeANSIIncapable  -- Can't enable VT processing
          Right () -> return NativeANSIEnabled  -- VT processing enabled

-- | This function applies another to the Windows handle, if the handle is
-- valid. If it is invalid, the specified default action is returned.
withHANDLE :: IO a -> (HANDLE -> IO a) -> HANDLE -> IO a
withHANDLE invalid action h =
  if h == iNVALID_HANDLE_VALUE || h == nullHANDLE
    then invalid  -- Invalid handle or no handle
    else action h
