-- Copyright 2009-2010 Corey O'Connor
{-# LANGUAGE BangPatterns #-}
{-# 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
  , wcwidth
  , wcswidth
  -- * Display Regions
  , DisplayText
  , 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. This function should not be
-- given a text value containing escapes.
text :: Attr -> TL.Text -> Image
text a txt = let displayWidth = safeWcswidth (TL.unpack txt)
             in HorizText a txt displayWidth (fromIntegral $! TL.length txt)

-- | Make an 'Image' from a text value. This function should not be
-- given a text value containing escapes.
text' :: Attr -> T.Text -> Image
text' a txt = let displayWidth = safeWcswidth (T.unpack 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 layed out on a single
-- row with the same display attribute. The string is assumed to be a
-- sequence of ISO-10646 characters. This function should not be given a
-- string containing escapes.
--
-- 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. This function should not be given a string
-- containing escapes.
--
-- 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 lazy 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     = let s = abs x in CropLeft i s (imageWidth i - s) (imageHeight 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     = let s = abs y in CropTop i s (imageWidth i) (imageHeight i - s)
    | 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@(CropBottom {croppedImage, outputWidth, outputHeight})
                | outputHeight <= h = i
                | otherwise          = CropBottom croppedImage outputWidth h
            go i
                | h >= imageHeight i = i
                | otherwise           = CropBottom i (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@(CropRight {croppedImage, outputWidth, outputHeight})
                | outputWidth <= w = i
                | otherwise         = CropRight croppedImage w outputHeight
            go i
                | w >= imageWidth i = i
                | otherwise          = CropRight i 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@(CropLeft {croppedImage, leftSkip, outputWidth, outputHeight})
                | outputWidth <= w = i
                | otherwise         =
                    let leftSkip' = leftSkip + outputWidth - w
                    in CropLeft croppedImage leftSkip' w outputHeight
            go i
                | imageWidth i <= w = i
                | otherwise          = CropLeft i (imageWidth i - w) 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@(CropTop {croppedImage, topSkip, outputWidth, outputHeight})
                | outputHeight <= h = i
                | otherwise         =
                    let topSkip' = topSkip + outputHeight - h
                    in CropTop croppedImage topSkip' outputWidth h
            go i
                | imageHeight i <= h = i
                | otherwise          = CropTop i (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)