content: -- Copyright 2009-2010 Corey O'Connor
  {-# LANGUAGE NamedFieldPuns #-}
  {-# LANGUAGE DisambiguateRecordFields #-}
  -- | A Vty program makes 'Picture's from 'Image's. This module provides
  -- the core constructors for creating, combining, and modifying
  -- 'Image's.
  module Graphics.Vty.Image
    (
    -- * Images
      Image
    , imageWidth
    , imageHeight
    -- * Image constructors
    , emptyImage
    , char
    , string
    , iso10646String
    , utf8String
    , text
    , text'
    , backgroundFill
    , utf8Bytestring
    , utf8Bytestring'
    , charFill
    -- * Combinators
    , horizJoin
    , (<|>)
    , vertJoin
    , (<->)
    , horizCat
    , vertCat
    -- * Image modifications
    , crop
    , cropRight
    , cropLeft
    , cropBottom
    , cropTop
    , pad
    , resize
    , resizeWidth
    , resizeHeight
    , translate
    , translateX
    , translateY
    -- * Character width functions
    , safeWcwidth
    , safeWcswidth
    , safeWctwidth
    , safeWctlwidth
    , wcwidth
    , wcswidth
    , wctwidth
    , wctlwidth
    -- * Display Regions
    , DisplayRegion
    , regionWidth
    , regionHeight
    )
  where
  
  import Graphics.Vty.Attributes
  import Graphics.Vty.Image.Internal
  import Graphics.Text.Width
  
  import qualified Data.ByteString as B
  import qualified Data.ByteString.Lazy as BL
  import qualified Data.Text as T
  import qualified Data.Text.Encoding as T
  import qualified Data.Text.Lazy as TL
  import qualified Data.Text.Lazy.Encoding as TL
  import Data.Word
  
  -- | A region of the display (first width, then height)
  type DisplayRegion = (Int,Int)
  
  regionWidth :: DisplayRegion -> Int
  regionWidth = fst
  
  regionHeight :: DisplayRegion -> Int
  regionHeight = snd
  
  infixr 5 <|>
  infixr 4 <->
  
  -- | An area of the picture's background (See 'Background').
  backgroundFill :: Int
                 -- ^ Fill width in columns
                 -> Int
                 -- ^ Fill height in rows
                 -> Image
  backgroundFill w h
      | w == 0    = EmptyImage
      | h == 0    = EmptyImage
      | otherwise = BGFill w h
  
  -- | Combines two images horizontally. This is an alias for 'horizJoin'.
  --
  -- infixr 5
  (<|>) :: Image -> Image -> Image
  (<|>) = horizJoin
  
  -- | Combines two images vertically. This is an alias for 'vertJoin'.
  --
  -- infixr 4
  (<->) :: Image -> Image -> Image
  (<->) = vertJoin
  
  -- | Compose any number of images together horizontally, with the first
  -- in the list being leftmost.
  horizCat :: [Image] -> Image
  horizCat = foldr horizJoin EmptyImage
  
  -- | Compose any number of images vertically, with the first in the list
  -- being topmost.
  vertCat :: [Image] -> Image
  vertCat = foldr vertJoin EmptyImage
  
  -- | Make an 'Image' from a lazy text value. The text value should be
  -- sanitized of escape sequences (ASCII 27) and carriage returns;
  -- otherwise layout and attribute problems may result.
  text :: Attr -> TL.Text -> Image
  text a txt = let displayWidth = safeWctlwidth txt
               in HorizText a txt displayWidth (fromIntegral $! TL.length txt)
  
  -- | Make an 'Image' from a text value. The text value should be
  -- sanitized of escape sequences (ASCII 27) and carriage returns;
  -- otherwise layout and attribute problems may result.
  text' :: Attr -> T.Text -> Image
  text' a txt = let displayWidth = safeWctwidth txt
                in HorizText a (TL.fromStrict txt) displayWidth (T.length txt)
  
  -- | Make an image from a single character. This is a standard Haskell
  -- 31-bit character assumed to be in the ISO-10646 encoding.
  char :: Attr -> Char -> Image
  char a c =
      let displayWidth = safeWcwidth c
      in HorizText a (TL.singleton c) displayWidth 1
  
  -- | Make an image from a string of characters laid out on a single
  -- row with the same display attribute. The string is assumed to be a
  -- sequence of ISO-10646 characters. The input string should be
  -- sanitized of escape sequences (ASCII 27) and carriage returns;
  -- otherwise layout and attribute problems may result.
  --
  -- Note: depending on how the Haskell compiler represents string
  -- literals, a string literal in a UTF-8 encoded source file, for
  -- example, may be represented as a ISO-10646 string. That is, I think,
  -- the case with GHC 6.10. This means, for the most part, you don't need
  -- to worry about the encoding format when outputting string literals.
  -- Just provide the string literal directly to iso10646String or string.
  iso10646String :: Attr -> String -> Image
  iso10646String a str =
      let displayWidth = safeWcswidth str
      in HorizText a (TL.pack str) displayWidth (length str)
  
  -- | Make an 'Image' from a 'String'.
  --
  -- This is an alias for iso10646String since the usual case is that a
  -- literal string like "foo" is represented internally as a list of ISO
  -- 10646 31 bit characters.
  --
  -- Note: Keep in mind that GHC will compile source encoded as UTF-8
  -- but the literal strings, while UTF-8 encoded in the source, will be
  -- transcoded to a ISO 10646 31 bit characters runtime representation.
  string :: Attr -> String -> Image
  string = iso10646String
  
  -- | Make an 'Image' from a string of characters layed out on a single
  -- row. The input is assumed to be the bytes for UTF-8 encoded text.
  utf8String :: Attr -> [Word8] -> Image
  utf8String a bytes = utf8Bytestring a (BL.pack bytes)
  
  -- | Make an 'Image' from a UTF-8 encoded lazy bytestring.
  utf8Bytestring :: Attr -> BL.ByteString -> Image
  utf8Bytestring a bs = text a (TL.decodeUtf8 bs)
  
  -- | Make an 'Image' from a UTF-8 encoded strict bytestring.
  utf8Bytestring' :: Attr -> B.ByteString -> Image
  utf8Bytestring' a bs = text' a (T.decodeUtf8 bs)
  
  -- | Make an image filling a region with the specified character.
  --
  -- If either the width or height are less than or equal to 0, then
  -- the result is the empty image.
  charFill :: Integral d
           => Attr
           -- ^ The attribute to use.
           -> Char
           -- ^ The character to use in filling the region.
           -> d
           -- ^ The region width.
           -> d
           -- ^ The region height.
           -> Image
  charFill a c w h
    | w <= 0 || h <= 0 = EmptyImage
    | otherwise        = vertCat
                       $ replicate (fromIntegral h)
                       $ HorizText a txt displayWidth charWidth
    where
      txt          = TL.replicate charWidth (TL.singleton c)
      displayWidth = safeWcwidth c * charWidth
  
      charWidth   :: Num a => a
      charWidth    = fromIntegral w
  
  -- | The empty image. Useful for fold combinators. These occupy no space
  -- and do not affect display attributes.
  emptyImage :: Image
  emptyImage = EmptyImage
  
  -- | Pad the given image. This adds background character fills to the
  -- left, top, right, bottom.
  pad :: Int
      -- ^ How much padding to add to the left side of the image.
      -> Int
      -- ^ How much padding to add to the top of the image.
      -> Int
      -- ^ How much padding to add to the right side of the image.
      -> Int
      -- ^ How much padding to add to the bottom of the image.
      -> Image
      -- ^ The image to pad.
      -> Image
  pad 0 0 0 0 i = i
  pad inL inT inR inB inImage
      | inL < 0 || inT < 0 || inR < 0 || inB < 0 = error "cannot pad by negative amount"
      | otherwise = go inL inT inR inB inImage
          where
              go 0 0 0 0 i = i
              go 0 0 0 b i = VertJoin i (BGFill w b) w h
                  where w = imageWidth  i
                        h = imageHeight i + b
              go 0 0 r b i = go 0 0 0 b $ HorizJoin i (BGFill r h) w h
                  where w = imageWidth  i + r
                        h = imageHeight i
              go 0 t r b i = go 0 0 r b $ VertJoin (BGFill w t) i w h
                  where w = imageWidth  i
                        h = imageHeight i + t
              go l t r b i = go 0 t r b $ HorizJoin (BGFill l h) i w h
                  where w = imageWidth  i + l
                        h = imageHeight i
  
  -- | Translates an image by padding or cropping the left and top.
  --
  -- If translation offsets are negative then the image is cropped.
  translate :: Int
            -- ^ The horizontal translation offset (can be negative)
            -> Int
            -- ^ The vertical translation offset (can be negative)
            -> Image
            -- ^ The image to translate.
            -> Image
  translate x y i = translateX x (translateY y i)
  
  -- | Translates an image by padding or cropping its left side.
  translateX :: Int -> Image -> Image
  translateX x i
      | x < 0 && (abs x > imageWidth i) = emptyImage
      | x < 0     = cropLeft (imageWidth i + x) i
      | x == 0    = i
      | otherwise = let h = imageHeight i in HorizJoin (BGFill x h) i (imageWidth i + x) h
  
  -- | Translates an image by padding or cropping its top.
  translateY :: Int -> Image -> Image
  translateY y i
      | y < 0 && (abs y > imageHeight i) = emptyImage
      | y < 0     = cropTop (imageHeight i + y) i
      | y == 0    = i
      | otherwise = let w = imageWidth i in VertJoin (BGFill w y) i w (imageHeight i + y)
  
  -- | Ensure an image is no larger than the provided size. If the image
  -- is larger then crop the right or bottom.
  --
  -- This is equivalent to a vertical crop from the bottom followed by
  -- horizontal crop from the right.
  crop :: Int
       -- ^ Cropping width
       -> Int
       -- ^ Cropping height
       -> Image
       -- ^ The image to crop
       -> Image
  crop 0 _ _ = EmptyImage
  crop _ 0 _ = EmptyImage
  crop w h i = cropBottom h (cropRight w i)
  
  -- | Crop an image's height. If the image's height is less than or equal
  -- to the specified height then this operation has no effect. Otherwise
  -- the image is cropped from the bottom.
  cropBottom :: Int -> Image -> Image
  cropBottom 0 _ = EmptyImage
  cropBottom h inI
      | h < 0     = error "cannot crop height to less than zero"
      | otherwise = go inI
          where
              go EmptyImage = EmptyImage
              go i@(Crop {outputHeight})
                  = i {outputHeight = min h outputHeight}
              go i
                  | h >= imageHeight i = i
                  | otherwise = Crop i 0 0 (imageWidth i) h
  
  -- | Crop an image's width. If the image's width is less than or equal
  -- to the specified width then this operation has no effect. Otherwise
  -- the image is cropped from the right.
  cropRight :: Int -> Image -> Image
  cropRight 0 _ = EmptyImage
  cropRight w inI
      | w < 0     = error "cannot crop width to less than zero"
      | otherwise = go inI
          where
              go EmptyImage = EmptyImage
              go i@(Crop {outputWidth})
                  = i {outputWidth = min w outputWidth}
              go i
                  | w >= imageWidth i = i
                  | otherwise = Crop i 0 0 w (imageHeight i)
  
  -- | Crop an image's width. If the image's width is less than or equal
  -- to the specified width then this operation has no effect. Otherwise
  -- the image is cropped from the left.
  cropLeft :: Int -> Image -> Image
  cropLeft 0 _ = EmptyImage
  cropLeft w inI
      | w < 0     = error "cannot crop the width to less than zero"
      | otherwise = go inI
          where
              go EmptyImage = EmptyImage
              go i@(Crop {leftSkip, outputWidth}) =
                  let delta = max 0 (outputWidth - w)
                  in i { leftSkip = leftSkip + delta
                       , outputWidth = outputWidth - delta }
              go i
                  | imageWidth i <= w = i
                  | otherwise = Crop i (imageWidth i - w) 0 w (imageHeight i)
  
  -- | Crop an image's height. If the image's height is less than or equal
  -- to the specified height then this operation has no effect. Otherwise
  -- the image is cropped from the top.
  cropTop :: Int -> Image -> Image
  cropTop 0 _ = EmptyImage
  cropTop h inI
      | h < 0  = error "cannot crop the height to less than zero"
      | otherwise = go inI
          where
              go EmptyImage = EmptyImage
              go i@(Crop {topSkip, outputHeight}) =
                  let delta = max 0 (outputHeight - h)
                  in i { topSkip = topSkip + delta
                       , outputHeight = outputHeight - delta }
              go i
                  | imageHeight i <= h = i
                  | otherwise = Crop i 0 (imageHeight i - h) (imageWidth i) h
  
  -- | Generic resize. Pads and crops are added to ensure that the
  -- resulting image matches the specified dimensions. This is biased to
  -- pad/crop the right and bottom.
  resize :: Int -> Int -> Image -> Image
  resize w h i = resizeHeight h (resizeWidth w i)
  
  -- | Resize the width. Pads and crops as required to assure the given
  -- display width. This is biased to pad/crop on the right.
  resizeWidth :: Int -> Image -> Image
  resizeWidth w i = case w `compare` imageWidth i of
      LT -> cropRight w i
      EQ -> i
      GT -> i <|> BGFill (w - imageWidth i) (imageHeight i)
  
  -- | Resize the height. Pads and crops as required to assure the given
  -- display height. This is biased to pad/crop on the bottom.
  resizeHeight :: Int -> Image -> Image
  resizeHeight h i = case h `compare` imageHeight i of
      LT -> cropBottom h i
      EQ -> i
      GT -> i <-> BGFill (imageWidth i) (h - imageHeight i)
  