-- |This module provides \"text clipping\" routines.  These routines
-- are responsible for ensuring that logical characters are clipped
-- properly when being laid out in a given physical region.  This is a
-- bit tricky because some Unicode characters use two terminal columns
-- and others (most) use one.  We have to take this into account when
-- truncating text to fit into rendering regions, so we concentrate
-- that logic here under the name of a \"clipping rectangle\" and
-- functions to apply it.
--
-- Clipping functionality is provided in two forms: one- and
-- two-dimensional clipping.  The former is useful for clipping a
-- single line of text at a given offset and up to a given width.  The
-- latter is useful for clipping a list of lines with respect to a 2-D
-- clipping rectangle.
module Graphics.Vty.Widgets.TextClip
    ( ClipRect(..)
    , clip1d
    , clip2d
    , updateRect
    )
where

import Control.Applicative
import Data.Maybe
import qualified Data.Text as T
import Graphics.Vty.Widgets.Util
    ( Phys(..)
    , chWidth
    )

-- |The type of clipping rectangles for 2-D clipping operations.  All
-- values are 'Phys' values to indicate that we are dealing explicitly
-- with physical column measurements rather than logical character
-- positions.
data ClipRect =
    ClipRect { clipLeft :: Phys
             -- ^The left margin of the clipping rectangle.
             , clipTop :: Phys
             -- ^The top row of the clipping rectangle.
             , clipWidth :: Phys
             -- ^The width, in columns, of the clipping rectangle.
             , clipHeight :: Phys
             -- ^The height, in rows, of the clipping rectangle.
             }
    deriving (Eq, Show)

-- |One-dimensional text clipping.  Takes the left clipping margin, a
-- clipping width, and a text string.  For example, @clip1d n w s@
-- clips the string @s@ so that the result includes characters in @s@
-- starting at position @n@ and including characters using no more
-- than @w@ columns in width.  Returns the clipped text plus 'Bool's
-- indicating whether wide characters were \"sliced\" on either side
-- (left and right, respectively) of the clipping region.  This
-- function guarantees that the text returned will always fit within
-- the specified clipping region.  Since wide characters may be sliced
-- during clipping, this may return a text string smaller than the
-- clipping region.
clip1d :: Phys -> Phys -> T.Text -> (T.Text, Bool, Bool)
clip1d _ 0 _ = (T.empty, False, False)
clip1d start len t = (T.pack result2, lSlice, rSlice)

    where
      pairs = [ (c, chWidth c) | c <- T.unpack t ]

      exploded = concat $ mkExp <$> pairs
      mkExp (a, i) = Just a : replicate (fromEnum i - 1) Nothing

      -- First clip up to the starting position.
      clip1 = drop (fromEnum start) exploded
      -- Then clip according to the width.
      clip2 = take (fromEnum len) clip1
      -- Rest is whatever was left after clipping to the width.
      rest = drop (fromEnum len) clip1

      rSlice = length rest > 0 && head rest == Nothing
      lSlice = length clip1 > 0 && head clip1 == Nothing

      result1 = catMaybes clip2
      result2 = if rSlice
                then init result1
                else result1

-- |Two-dimensional text clipping.  Returns clipping data for each
-- line as returned by 'clip1d', with the added behavior that it
-- returns at most 'clipHeight' lines of text and uses 'clipTop' as
-- the offset when clipping rows.
clip2d :: ClipRect -> [T.Text] -> [(T.Text, Bool, Bool)]
clip2d rect ls = clip1d left len <$> visibleLines
        where
          visibleLines = take (fromEnum height) $ drop (fromEnum top) ls
          left = clipLeft rect
          top = clipTop rect
          len = clipWidth rect
          height = clipHeight rect

-- |Given a physical point and a clipping rectangle, adjust the
-- clipping rectangle so that the point falls just inside the
-- rectangle.  If the point is already within the rectangle, return
-- the rectangle unmodified.  NB: this assumes that the physical
-- position given has passed whatever validation checks are relevant
-- for the user of the 'ClipRect'.  This function just performs a
-- rectangle transformation.
updateRect :: (Phys, Phys) -> ClipRect -> ClipRect
updateRect (row, col) oldRect = adjustLeft $ adjustTop oldRect
    where
      adjustLeft old
          | col < clipLeft oldRect = old { clipLeft = col }
          | col >= clipLeft oldRect + clipWidth oldRect =
              old { clipLeft = col - clipWidth old + 1 }
          | otherwise = old

      adjustTop old
          | row < clipTop oldRect = old { clipTop = row }
          | row >= clipTop oldRect + clipHeight oldRect =
              old { clipTop = row - clipHeight old + 1 }
          | otherwise = old