{-# LANGUAGE ExistentialQuantification, FlexibleInstances, TypeSynonymInstances #-}
-- |This module provides a ''padding'' mechanism for adding padding to
-- a widget on one or more sides.
module Graphics.Vty.Widgets.Padding
    ( Padded
    , Padding
    , Paddable(..)
    , (+++)
    , padded
    , withPadding
    , padNone
    , padLeft
    , padRight
    , padTop
    , padBottom
    , padLeftRight
    , padTopBottom
    , padAll
    )
where

import Data.Word
import Data.Monoid
import Graphics.Vty
import Graphics.Vty.Widgets.Core
import Graphics.Vty.Widgets.Util

-- |The type of padding on widgets.
data Padding = Padding Int Int Int Int
               deriving (Show)

data Padded = forall a. (Show a) => Padded (Widget a) Padding

instance Show Padded where
    show (Padded _ p) = concat [ "Padded { "
                               , "padding = "
                               , show p
                               , ", ... }"
                               ]

instance Monoid Padding where
    mempty = Padding 0 0 0 0
    mappend (Padding a1 a2 a3 a4) (Padding b1 b2 b3 b4) =
        Padding (a1 + b1) (a2 + b2) (a3 + b3) (a4 + b4)

(+++) :: (Monoid a) => a -> a -> a
(+++) = mappend

-- |The class of types to which we can add padding.
class Paddable a where
    pad :: a -> Padding -> a

instance Paddable Padding where
    pad p1 p2 = p1 +++ p2

leftPadding :: Padding -> Word
leftPadding (Padding _ _ _ l) = toEnum l

rightPadding :: Padding -> Word
rightPadding (Padding _ r _ _) = toEnum r

bottomPadding :: Padding -> Word
bottomPadding (Padding _ _ b _) = toEnum b

topPadding :: Padding -> Word
topPadding (Padding t _ _ _) = toEnum t

-- |Padding constructor with no padding.
padNone :: Padding
padNone = Padding 0 0 0 0

-- |Padding constructor with left padding in columns.
padLeft :: Int -> Padding
padLeft v = Padding 0 0 0 v

-- |Padding constructor with right padding in columns.
padRight :: Int -> Padding
padRight v = Padding 0 v 0 0

-- |Padding constructor with top padding in rows.
padTop :: Int -> Padding
padTop v = Padding v 0 0 0

-- |Padding constructor with bottom padding in rows.
padBottom :: Int -> Padding
padBottom v = Padding 0 0 v 0

-- |Padding constructor with padding on all sides in rows and
-- columns.
padAll :: Int -> Padding
padAll v = Padding v v v v

-- |Padding constructor with padding on top and bottom in rows.
padTopBottom :: Int -> Padding
padTopBottom v = Padding v 0 v 0

-- |Padding constructor with padding on left and right in columns.
padLeftRight :: Int -> Padding
padLeftRight v = Padding 0 v 0 v

-- |Monadic combinator to construct a 'Padded' wrapper.
withPadding :: (Show a) => Padding -> Widget a -> IO (Widget Padded)
withPadding = flip padded

-- |Create a 'Padded' wrapper to add padding.
padded :: (Show a) => Widget a -> Padding -> IO (Widget Padded)
padded ch padding = do
  wRef <- newWidget $ \w ->
      w { state = Padded ch padding

        , growVertical_ = const $ growVertical ch
        , growHorizontal_ = const $ growHorizontal ch

        , render_ =
            \this sz ctx ->
                if (region_width sz < 2) || (region_height sz < 2)
                then return empty_image
                else do
                  Padded child p <- getState this
                  f <- focused <~ this

                  -- Compute constrained space based on padding
                  -- settings.
                  let constrained = sz `withWidth` (toEnum $ max 0 newWidth)
                                    `withHeight` (toEnum $ max 0 newHeight)
                      newWidth = (fromEnum $ region_width sz) - fromEnum (leftPadding p + rightPadding p)
                      newHeight = (fromEnum $ region_height sz) - fromEnum (topPadding p + bottomPadding p)
                      attr = mergeAttrs [ if f then focusAttr ctx else overrideAttr ctx
                                        , normalAttr ctx
                                        ]

                  -- Render child.
                  img <- render child constrained ctx

                  -- Create padding images.
                  let leftImg = char_fill attr ' ' (leftPadding p) (image_height img)
                      rightImg = char_fill attr ' ' (rightPadding p) (image_height img)
                      topImg = char_fill attr ' ' (image_width img + leftPadding p + rightPadding p)
                               (topPadding p)
                      bottomImg = char_fill attr ' ' (image_width img + leftPadding p + rightPadding p)
                                  (bottomPadding p)

                  return $ topImg <-> (leftImg <|> img <|> rightImg) <-> bottomImg

        , setCurrentPosition_ =
            \this pos -> do
              Padded child p <- getState this

              -- Considering left and top padding, adjust position and
              -- set on child.
              let newPos = pos
                           `plusWidth` (leftPadding p)
                           `plusHeight` (topPadding p)

              setCurrentPosition child newPos

        }

  wRef `relayKeyEvents` ch
  wRef `relayFocusEvents` ch
  return wRef