{-# LANGUAGE CPP #-}
-- |This module provides a basic infrastructure for modelling a user
-- interface widget and converting it to Vty's 'Image' type.
module Graphics.Vty.Widgets.Rendering
    ( Widget(..)
    , mkImage

    -- ** Rendering process
    -- |'Widget's are ultimately converted to Vty 'Image's, but this
    -- library uses an intermediate type, 'Render', to represent the
    -- physical layout of the images.  A 'Render' represents the
    -- various primitive rendering constructs which support vertical
    -- and horizontal concatenation and 'Image' addressing.  Once a
    -- 'Widget' has been rendered (see 'render'), the resulting
    -- 'Render' is then put through a /positioning pass/ in which the
    -- sizes and positions of any addressable image regions are stored
    -- (see 'RenderState').  The result is a single 'Image' suitable
    -- for use with Vty's 'Graphics.Vty.pic_for_image' function.
    , RenderState
#ifdef TESTING
    , Render(..)
#else
    , Render
#endif
    , renderImg
    , renderAddr
    , renderMany
    , renderWidth
    , renderHeight

    -- ** Widget addressing
    -- |Some widgets, such as editable widgets, require that their
    -- on-screen representations be known after rendering; this
    -- library supports a notion of /widget addressing/ in which a
    -- 'Widget' is marked as /addressable/ (see 'addressable').
    -- Addressable widgets' position and size information ('Address')
    -- will be recorded in the 'RenderState' during rendering in
    -- 'mkImage'.
    , Address
    , address
    , addressable
    , addrSize
    , addrPosition
    , addAddress

    -- ** Miscellaneous
    , Orientation(..)
    , withWidth
    , withHeight

#ifdef TESTING
    , mkImageSize
#endif
    )
where

import GHC.Word ( Word )
import qualified Data.Map as Map
import Control.Monad.State ( State, modify, runState )

import Graphics.Vty
    ( DisplayRegion(DisplayRegion)
    , Attr
    , Image
    , Vty(terminal)
    , display_bounds
    , (<|>)
    , (<->)
    , image_width
    , image_height
    , region_width
    , region_height
    , vert_cat
    , horiz_cat
    )

-- |A simple orientation type.
data Orientation = Horizontal | Vertical
                   deriving (Eq, Show)

-- |The type of user interface widgets.  A 'Widget' provides several
-- properties:
--
-- * /Growth properties/ which provide information about how to
--   allocate space to widgets depending on their propensity to
--   consume available space
--
-- * A /primary attribute/ which is the attribute most easily
--   identifiable with the widget's visual presentation
--
-- * An /attribute override/ which allows the widget and its children
--   to be rendered using a single attribute specified by the caller
--
-- * A /rendering routine/ which converts the widget's internal state
--   into a 'Render' value.
--
-- Of primary concern is the rendering routine, 'render'.  The
-- rendering routine takes one parameter: the size of the space in
-- which the widget should be rendered.  The space is important
-- because it provides a maximum size for the widget.  For widgets
-- that consume all available space, the size of the resulting
-- 'Render' will be equal to the supplied size.  For smaller widgets
-- (e.g., a simple string of text), the size of the 'Render' will
-- likely be much smaller than the supplied size.  In any case, any
-- 'Widget' implementation /must/ obey the rule that the resulting
-- 'Render' must not exceed the supplied 'DisplayRegion' in size.  If
-- it does, there's a good chance your interface will be garbled.
--
-- If the widget has child widgets, the supplied size should be
-- subdivided to fit the child widgets as appropriate.  How the space
-- is subdivided may depend on the growth properties of the children
-- or it may be a matter of policy.
data Widget = Widget {
    -- |Render the widget with the given dimensions.  The result
    -- /must/ not be larger than the specified dimensions, but may be
    -- smaller.
    render :: DisplayRegion -> Render

    -- |Will this widget expand to take advantage of available
    -- horizontal space?
    , growHorizontal :: Bool

    -- |Will this widget expand to take advantage of available
    -- vertical space?
    , growVertical :: Bool

    -- |The primary attribute of this widget, used when composing
    -- widgets.  For example, if you want to compose a widget /A/ with
    -- a space-filling widget /B/, you probably want /B/'s text
    -- attributes to be identical to those of /A/.
    , primaryAttribute :: Attr

    -- |Apply the specified attribute to this widget.
    , withAttribute :: Attr -> Widget
    }

-- |Information about the rendered state of a widget.
data Address = Address { addrPosition :: DisplayRegion
                       -- ^The rendered position of a widget.
                       , addrSize :: DisplayRegion
                       -- ^The rendered size of a widget.
                       }
               deriving (Eq, Show)

-- |The collection of widget names (see 'addressable') and their
-- rendering addresses as a result of 'render'.
type RenderState = Map.Map String Address

-- |An intermediate type used in the rendering process.  Widgets are
-- converted into collections of 'Image's and represented with this
-- type, using a few primitive rendering instructions to determine how
-- the rendered images are combined to form a complete terminal window
-- image.  See 'render'.
data Render = Img Image
            | Addressed String Render
            | Many Orientation [Render]

-- |Annotate a widget with a rendering identifier so that its
-- rendering address will be stored by the rendering process.  Once
-- the widget has been rendered, its address will be found in the
-- resulting 'RenderState'.  To retrieve the address of such an
-- identifier, use 'address'.
addressable :: String
            -- ^The identifier of the widget to be used in the
            -- 'RenderState'.
            -> Widget
            -- ^The widget whose rendering address ('Address') should
            -- be stored.
            -> Widget
addressable ident w = w {
                        withAttribute = addressable ident . withAttribute w
                      , render = renderAddr ident . render w
                      }

-- |Create a 'Render' containing a single 'Image'.
renderImg :: Image -> Render
renderImg = Img

-- |Create a 'Render' representing a render together with an
-- identifier.  This type of 'Render' is used with 'addressable' to
-- locate a widget's position and dimensions in the final 'Image'.
renderAddr :: String -- ^The identifier of the widget that this
                     -- 'Render' represents. Should be the same
                     -- identifier that was passed to 'addressable'.
           -> Render -- ^The 'Render' to identify.
           -> Render
renderAddr = Addressed

-- |Create a 'Render' representing a collection of renders which
-- should be combined in the specified 'Orientation'.
renderMany :: Orientation -> [Render] -> Render
renderMany = Many

-- |Compute the width, in columns, of a 'Render'.
renderWidth :: Render -> Word
renderWidth (Img img) = image_width img
renderWidth (Addressed _ w) = renderWidth w
renderWidth (Many Vertical ws) = maximum $ map renderWidth ws
renderWidth (Many Horizontal ws) = sum $ map renderWidth ws

-- |Compute the height, in rows, of a 'Render'.
renderHeight :: Render -> Word
renderHeight (Img img) = image_height img
renderHeight (Addressed _ w) = renderHeight w
renderHeight (Many Vertical ws) = sum $ map renderHeight ws
renderHeight (Many Horizontal ws) = maximum $ map renderHeight ws

-- |Given a starting position (usually @'DisplayRegion' 0 0@) and a
-- 'Render', combine the 'Render''s contents into a single 'Image' and
-- track the positions and sizes of any 'Render's with positioning
-- addresses.  Returns the resulting image and a 'RenderState'
-- containing the 'Address' values of all addressable widgets.
doPositioning :: DisplayRegion -> Render -> State RenderState Image
doPositioning _ (Img img) = return img
doPositioning _ (Many Vertical []) = error "got empty rendered list"
doPositioning _ (Many Horizontal []) = error "got empty rendered list"

doPositioning pos (Many Vertical widgets) = do
  let positionNext _ [] = return $ vert_cat []
      positionNext p (w:ws) = do
        img <- doPositioning p w
        let newPos = p `withHeight` (region_height p + image_height img)
        n <- positionNext newPos ws
        return (img <-> n)

  positionNext pos widgets

doPositioning pos (Many Horizontal widgets) = do
  let positionNext _ [] = return $ horiz_cat []
      positionNext p (w:ws) = do
        img <- doPositioning p w
        let newPos = p `withWidth` (region_width p + image_width img)
        n <- positionNext newPos ws
        return (img <|> n)

  positionNext pos widgets

doPositioning pos (Addressed s w) = do
  img <- doPositioning pos w
  addAddress s pos img
  return img

-- |Retrieve the rendering address for a given widget.  To annotate a
-- widget to induce storage of its address, use 'addressable'.
address :: String -> RenderState -> Maybe Address
address = Map.lookup

-- |Add an address for the specified identifier, position, and 'Image'
-- to the 'RenderState'.
addAddress :: String        -- ^The 'Address' identifier.
           -> DisplayRegion -- ^The position of the image.
           -> Image         -- ^The image whose size should be stored.
           -> State RenderState ()
addAddress ident pos img = do
  let rinfo = Address pos (imageSize img)
  modify (Map.insert ident rinfo)

-- |Compute the size of an 'Image' as a 'DisplayRegion'.
imageSize :: Image -> DisplayRegion
imageSize img = DisplayRegion (image_width img) (image_height img)

-- |Given a 'Widget' and a 'Vty' object, render the widget using the
-- current size of the terminal controlled by Vty. Returns the
-- rendered 'Widget' as an 'Image' along with the 'RenderState'
-- containing the 'Address'es of 'addressable' widgets.
mkImage :: Vty -> Widget -> IO (Image, RenderState)
mkImage vty w = do
  size <- display_bounds $ terminal vty
  let upperLeft = DisplayRegion 0 0
  return $ mkImageSize upperLeft size w

mkImageSize :: DisplayRegion -> DisplayRegion -> Widget
            -> (Image, RenderState)
mkImageSize position size w =
    let rendered = render w size
    in runState (doPositioning position rendered) (Map.fromList [])

-- |Modify the width component of a 'DisplayRegion'.
withWidth :: DisplayRegion -> Word -> DisplayRegion
withWidth (DisplayRegion _ h) w = DisplayRegion w h

-- |Modify the height component of a 'DisplayRegion'.
withHeight :: DisplayRegion -> Word -> DisplayRegion
withHeight (DisplayRegion w _) h = DisplayRegion w h