{-# LANGUAGE FlexibleInstances #-}

-- | This module contains functions that convert a 'T.Chunk' into
-- 'ByteString's.  Ordinarily everything you need from this module is
-- exported from "Rainbow".
module Rainbow.Translate where

import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Text as X
import qualified Data.Text.Encoding as X
import qualified Data.Text.Lazy as XL
import Data.ByteString (ByteString)
import Data.Word
import Data.List (intersperse)
import qualified Rainbow.Types as T
import System.Process
import Text.Read
import System.Exit
import Control.Monad
import Control.Exception
import qualified System.IO as IO


-- | Items that can be rendered.  'render' returns a difference list.
class Renderable a where
  render :: a -> [ByteString] -> [ByteString]

-- | Converts a strict Text to a UTF-8 ByteString.
instance Renderable X.Text where
  render x = (X.encodeUtf8 x :)

-- | Converts a lazy Text to UTF-8 ByteStrings.
instance Renderable XL.Text where
  render = foldr (.) id . map render . XL.toChunks

-- | Strict ByteString is left as-is.
instance Renderable BS.ByteString where
  render x = (x :)

-- | Lazy ByteString is converted to strict chunks.
instance Renderable BSL.ByteString where
  render x = (BSL.toChunks x ++)

-- | Strings are converted first to a strict Text and then to a strict
-- ByteString.
instance Renderable String where
  render x = ((X.encodeUtf8 . X.pack $ x):)

single :: Char -> [ByteString] -> [ByteString]
single c = ((BS8.singleton c):)

escape :: [ByteString] -> [ByteString]
escape = single '\x1B'

csi :: [ByteString] -> [ByteString]
csi = escape . single '['

sgr :: ([ByteString] -> [ByteString]) -> [ByteString] -> [ByteString]
sgr sq = csi . sq . single 'm'

params :: Show a => [a] -> [ByteString] -> [ByteString]
params cs = ((intersperse semi . map (BS8.pack . show) $ cs) ++)
  where
    semi = BS8.singleton ';'

sgrSingle :: Word -> [ByteString] -> [ByteString]
sgrSingle w = sgr $ params [w]

sgrDouble :: Word -> Word -> [ByteString] -> [ByteString]
sgrDouble x y = sgr $ params [x, y]

normalDefault :: [ByteString] -> [ByteString]
normalDefault = sgrSingle 0

bold :: [ByteString] -> [ByteString]
bold = sgrSingle 1

faint :: [ByteString] -> [ByteString]
faint = sgrSingle 2

italic :: [ByteString] -> [ByteString]
italic = sgrSingle 3

underline :: [ByteString] -> [ByteString]
underline = sgrSingle 4

blink :: [ByteString] -> [ByteString]
blink = sgrSingle 5

-- Yes, blink is 5, inverse is 7; 6 is skipped.  In ISO 6429 6 blinks
-- at a different rate.

inverse :: [ByteString] -> [ByteString]
inverse = sgrSingle 7

invisible :: [ByteString] -> [ByteString]
invisible = sgrSingle 8

strikeout :: [ByteString] -> [ByteString]
strikeout = sgrSingle 9

foreBlack :: [ByteString] -> [ByteString]
foreBlack = sgrSingle 30

foreRed :: [ByteString] -> [ByteString]
foreRed = sgrSingle 31

foreGreen :: [ByteString] -> [ByteString]
foreGreen = sgrSingle 32

foreYellow :: [ByteString] -> [ByteString]
foreYellow = sgrSingle 33

foreBlue :: [ByteString] -> [ByteString]
foreBlue = sgrSingle 34

foreMagenta :: [ByteString] -> [ByteString]
foreMagenta = sgrSingle 35

foreCyan :: [ByteString] -> [ByteString]
foreCyan = sgrSingle 36

foreWhite :: [ByteString] -> [ByteString]
foreWhite = sgrSingle 37

-- code 3 8 is skipped

foreDefault :: [ByteString] -> [ByteString]
foreDefault = sgrSingle 39

backBlack :: [ByteString] -> [ByteString]
backBlack = sgrSingle 40

backRed :: [ByteString] -> [ByteString]
backRed = sgrSingle 41

backGreen :: [ByteString] -> [ByteString]
backGreen = sgrSingle 42

backYellow :: [ByteString] -> [ByteString]
backYellow = sgrSingle 43

backBlue :: [ByteString] -> [ByteString]
backBlue = sgrSingle 44

backMagenta :: [ByteString] -> [ByteString]
backMagenta = sgrSingle 45

backCyan :: [ByteString] -> [ByteString]
backCyan = sgrSingle 46

backWhite :: [ByteString] -> [ByteString]
backWhite = sgrSingle 47

-- code 4 8 is skipped

backDefault :: [ByteString] -> [ByteString]
backDefault = sgrSingle 49

fore256 :: Word8 -> [ByteString] -> [ByteString]
fore256 c = sgr $ params [38,5,c]

back256 :: Word8 -> [ByteString] -> [ByteString]
back256 c = sgr $ params [48,5,c]

foreColor8 :: T.Enum8 -> [ByteString] -> [ByteString]
foreColor8 e8 = case e8 of
  T.E0 -> foreBlack
  T.E1 -> foreRed
  T.E2 -> foreGreen
  T.E3 -> foreYellow
  T.E4 -> foreBlue
  T.E5 -> foreMagenta
  T.E6 -> foreCyan
  T.E7 -> foreWhite

backColor8 :: T.Enum8 -> [ByteString] -> [ByteString]
backColor8 e8 = case e8 of
  T.E0 -> backBlack
  T.E1 -> backRed
  T.E2 -> backGreen
  T.E3 -> backYellow
  T.E4 -> backBlue
  T.E5 -> backMagenta
  T.E6 -> backCyan
  T.E7 -> backWhite

renderFormat :: T.Format -> [ByteString] -> [ByteString]
renderFormat (T.Format bld fnt ita und bli ivr isb stk)
  = effect bold bld
  . effect faint fnt
  . effect italic ita
  . effect underline und
  . effect blink bli
  . effect inverse ivr
  . effect invisible isb
  . effect strikeout stk
  where
    effect on x = if x then on else id

renderStyle8 :: T.Style T.Enum8 -> [ByteString] -> [ByteString]
renderStyle8 (T.Style fore back format)
  = effect foreColor8 fore
  . effect backColor8 back
  . renderFormat format
  where
      effect on (T.Color may) = maybe id on may

renderStyle256 :: T.Style Word8 -> [ByteString] -> [ByteString]
renderStyle256 (T.Style fore back format)
  = effect fore256 fore
  . effect back256 back
  . renderFormat format
  where
    effect on (T.Color may) = maybe id on may

toByteStringsColors0
  :: Renderable a
  => T.Chunk a
  -> [ByteString]
  -> [ByteString]
toByteStringsColors0 (T.Chunk _ yn) = render yn

toByteStringsColors8
  :: Renderable a
  => T.Chunk a
  -> [ByteString]
  -> [ByteString]
toByteStringsColors8 (T.Chunk (T.Scheme s8 _) yn)
  = normalDefault
  . renderStyle8 s8
  . render yn
  . normalDefault

toByteStringsColors256
  :: Renderable a
  => T.Chunk a
  -> [ByteString]
  -> [ByteString]
toByteStringsColors256 (T.Chunk (T.Scheme _ s256) yn)
  = normalDefault
  . renderStyle256 s256
  . render yn
  . normalDefault



-- | Spawns a subprocess to read the output of @tput colors@.  If this
-- says there are at least 256 colors are available, returns
-- 'toByteStringsColors256'.  Otherwise, if there are at least 8
-- colors available, returns 'toByteStringsColors8'.  Otherwise,
-- returns 'toByteStringsColors0'.
--
-- If any IO exceptions arise during this process, they are discarded
-- and 'toByteStringsColors0' is returned.
byteStringMakerFromEnvironment
  :: Renderable a
  => IO (T.Chunk a -> [ByteString] -> [ByteString])
byteStringMakerFromEnvironment
  = catcher (fmap f $ readProcessWithExitCode "tput" ["colors"] "")
  where
    f (code, stdOut, _) = maybe toByteStringsColors0 id $ do
      case code of
        ExitFailure _ -> mzero
        _ -> return ()
      numColors <- readMaybe stdOut
      return $ numColorsToFunc numColors
    numColorsToFunc i
      | i >= (256 :: Int) = toByteStringsColors256
      | i >= 8 = toByteStringsColors8
      | otherwise = toByteStringsColors0

    catcher act = fmap g (try act)
      where
        g (Left e) = toByteStringsColors0
          where _types = e :: IOException
        g (Right good) = good

-- | Like 'byteStringMakerFromEnvironment' but also consults a
-- provided 'Handle'.  If the 'Handle' is not a terminal,
-- 'toByteStringsColors0' is returned.  Otherwise, the value of
-- 'byteStringMakerFromEnvironment' is returned.
byteStringMakerFromHandle
  :: Renderable a
  => IO.Handle
  -> IO (T.Chunk a -> [ByteString] -> [ByteString])
byteStringMakerFromHandle h = IO.hIsTerminalDevice h >>= f
  where
    f isTerm | isTerm = byteStringMakerFromEnvironment
             | otherwise = return toByteStringsColors0
          

-- | Convert a list of 'T.Chunk' to a list of 'ByteString'.  The
-- length of the returned list may be longer than the length of the
-- input list.
--
-- So, for example, to print a bunch of chunks to standard output
-- using 256 colors:
--
-- > module PrintMyChunks where
-- >
-- > import qualified Data.ByteString as BS
-- > import Rainbow
-- >
-- > myChunks :: [Chunk String]
-- > myChunks = [ chunk "Roses" & fore red, chunk "\n",
-- >              chunk "Violets" & fore blue, chunk "\n" ]
-- >
-- > myPrintedChunks :: IO ()
-- > myPrintedChunks = mapM_ BS.putStr
-- >                 . chunksToByteStrings toByteStringsColors256
-- >                 $ myChunks
--
-- To use the highest number of colors that this terminal supports:
--
-- > myPrintedChunks' :: IO ()
-- > myPrintedChunks' = do
-- >   printer <- byteStringMakerFromEnvironment
-- >   mapM_ BS.putStr
-- >     . chunksToByteStrings printer
-- >     $ myChunks

chunksToByteStrings
  :: (T.Chunk a -> [ByteString] -> [ByteString])
  -- ^ Function that converts 'T.Chunk' to 'ByteString'.  This
  -- function, when applied to a 'T.Chunk', returns a difference list.
  -> [T.Chunk a]
  -> [ByteString]
chunksToByteStrings mk = ($ []) . foldr (.) id . map mk

-- Quick and dirty I/O functions

-- | Writes a 'T.Chunk' to standard output.  Spawns a child process to
-- read the output of @tput colors@ to determine how many colors to
-- use, for every single chunk.  Therefore, this is not going to win
-- any speed awards.  You are better off using 'chunksToByteStrings'
-- and the functions in "Data.ByteString" to print your 'T.Chunk's if
-- you are printing a lot of them.
putChunk :: Renderable a => T.Chunk a -> IO ()
putChunk ck = do
  mkr <- byteStringMakerFromEnvironment
  mapM_ BS.putStr . chunksToByteStrings mkr $ [ck]

-- | Writes a 'T.Chunk' to standard output, and appends a newline.
-- Spawns a child process to read the output of @tput colors@ to
-- determine how many colors to use, for every single chunk.
-- Therefore, this is not going to win any speed awards.  You are
-- better off using 'chunksToByteStrings' and the functions in
-- "Data.ByteString" to print your 'T.Chunk's if you are printing a lot
-- of them.
putChunkLn :: Renderable a => T.Chunk a -> IO ()
putChunkLn ck = putChunk ck >> putStrLn ""