{-# OPTIONS -fglasgow-exts #-}
--------------------------------------------------------------------------------
{-|	Module      :  Attributes
	Copyright   :  (c) Daan Leijen 2003
	License     :  wxWindows

	Maintainer  :  wxhaskell-devel@lists.sourceforge.net
	Stability   :  provisional
	Portability :  portable


Widgets @w@ can have attributes of type @a@ represented by the type 'Attr' @w a@.
An example of an attribute is 'Graphics.UI.WX.Classes.text' with type:

> text :: Attr (Window a) String

This means that any object derived from 'Window' has a 'Graphics.UI.WX.Classes.text' attribute of type 'String'.
An attribute can be read with the 'get' function:

> get w title           :: IO String

When an attribute is associated with a value, we call it a /property/ of type 'Prop' @w@.
Properties are constructed by assigning a value to an attribute with the (':=') constructor:

> text := "hello world"      :: Prop (Window a)

A list of properties can be set with the 'set' function:

> set w [text := "Hi"]   :: IO ()

The (':~') constructor is used to transform an attribute value with an update function.
For example, the 'interval' on a timer can be doubled with the following expression.

> set timer [interval :~ (*2)]

The functions 'get', 'set', (':='), and (':~') are polymorphic and work for all widgets, but
the @text@ attribute just works for windows. Many attributes work for different kind
of objects and are organised into type classes. Actually, the real type of the
'Graphics.UI.WX.Classes.text' attribute is:

> Textual w => Attr w String

and 'Window' derived objects are part of this class:

> instance Textual (Window a)

But also menus and status fields:

> instance Textual (Menu a)
> instance Textual (StatusField)

Sometimes, it is convenient to also get a reference to the object itself when setting
a property. The operators ('::=') and ('::~') provide this reference.
-}
--------------------------------------------------------------------------------
module Graphics.UI.WX.Attributes
    (
    -- * Attributes
      Attr, Prop((:=),(:~),(::=),(::~)), ReadAttr, WriteAttr, CreateAttr
    , get, set, swap
    , mapAttr, mapAttrW


    -- * Internal

    -- ** Attributes
    , newAttr, readAttr, writeAttr, nullAttr, constAttr, makeAttr
    
    -- ** Reflection
    , attrName, propName, containsProperty
    
    -- ** Reflective attributes
    , reflectiveAttr, createAttr, withProperty, findProperty
    , withStyleProperty, withStylePropertyNot

    -- *** Filter
    , PropValue(..)
    , filterProperty 

    -- ** Cast
    , castAttr, castProp, castProps
    ) where

import Graphics.UI.WX.Types
import Data.Dynamic

infixr 0 :=,:~,::=,::~

-- | A property of a widget @w@ is an attribute that
-- is already associated with a value. .
data Prop w     = forall a. Attr w a := a         -- ^ Assign a value to an attribute.
                | forall a. Attr w a :~ (a -> a)  -- ^ Apply an update function to an attribute.
                | forall a. Attr w a ::= (w -> a) -- ^ Assign a value to an attribute with the widget as argument.
                | forall a. Attr w a ::~ (w -> a -> a) -- ^ Apply an update function to an attribute with the widget as an argument.


-- | An attribute that should be specified at creation time. Just for documentation purposes.
type CreateAttr w a = Attr w a

-- | A read-only attribute. Just for documentation purposes.
type ReadAttr w a = Attr w a

-- | A write-only attribute. Just for documentation purposes.
type WriteAttr w a = Attr w a

-- | Widgets @w@ can have attributes of type @a@.
data Attr w a   = Attr String (Maybe (a -> Dynamic, Dynamic -> Maybe a))  -- name, dynamic conversion
                              (w -> IO a) (w -> a -> IO ())               -- getter setter 
                              (w -> (a -> a) -> IO a)                     -- updater      


-- | Cast attributes.
castAttr :: (v -> w) -> Attr w a -> Attr v a
castAttr coerce (Attr name mbdyn getter setter upd)
  = Attr name mbdyn (\v -> getter (coerce v)) (\v x -> (setter (coerce v) x))
                    (\v f -> upd (coerce v) f) 

-- | Cast properties
castProp :: (v -> w) -> Prop w -> Prop v
castProp coerce prop
  = case prop of
      (attr := x)   -> (castAttr coerce attr) := x
      (attr :~ f)   -> (castAttr coerce attr) :~ f
      (attr ::= f)  -> (castAttr coerce attr) ::= (\v -> f (coerce v))
      (attr ::~ f)  -> (castAttr coerce attr) ::~ (\v x -> f (coerce v) x)

-- | Cast a list of properties.
castProps :: (v -> w) -> [Prop w] -> [Prop v]
castProps coerce props
  = map (castProp coerce) props

-- | Create a /reflective/ attribute with a specified name: value can possibly
-- retrieved using 'getPropValue'. Note: the use of this function is discouraged
-- as it leads to non-compositional code.
reflectiveAttr :: Typeable a => String -> (w -> IO a) -> (w -> a -> IO ()) -> Attr w a
reflectiveAttr name getter setter
  = Attr name (Just (toDyn, fromDynamic)) getter setter updater 
  where
    updater w f   = do x <- getter w; setter w (f x); return x

-- | Create a /reflective/ attribute with a specified name: value can possibly
-- retrieved using 'getPropValue'. Note: the use of this function is discouraged
-- as it leads to non-compositional code.
createAttr :: Typeable a => String -> (w -> IO a) -> (w -> a -> IO ()) -> CreateAttr w a
createAttr name getter setter
  = reflectiveAttr name getter setter

-- | Create a new attribute with a specified name, getter, setter, and updater function.
makeAttr :: String -> (w -> IO a) -> (w -> a -> IO ()) -> (w -> (a -> a) -> IO a) -> Attr w a
makeAttr name getter setter updater
  = Attr name Nothing getter setter updater 


-- | Create a new attribute with a specified name, getter and setter function.
newAttr :: String -> (w -> IO a) -> (w -> a -> IO ()) -> Attr w a
newAttr name getter setter
  = makeAttr name getter setter updater 
  where
    updater w f   = do x <- getter w; setter w (f x); return x


-- | Define a read-only attribute.
readAttr :: String -> (w -> IO a) -> ReadAttr w a
readAttr name getter
  = newAttr name getter (\w x -> ioError (userError ("attribute '" ++ name ++ "' is read-only.")))

-- | Define a write-only attribute.
writeAttr :: String -> (w -> a -> IO ()) -> WriteAttr w a
writeAttr name setter
  = newAttr name (\w -> ioError (userError ("attribute '" ++ name ++ "' is write-only."))) setter

-- | A dummy attribute.
nullAttr :: String -> WriteAttr w a
nullAttr name
  = writeAttr name (\w x -> return ())

-- | A constant attribute.
constAttr :: Typeable a => String -> a -> Attr w a
constAttr name x
  = newAttr name (\w -> return x) (\w x -> return ())


-- | (@mapAttr get set attr@) maps an attribute of @Attr w a@ to
-- @Attr w b@ where (@get :: a -> b@) is used when the attribute is
-- requested and (@set :: a -> b -> a@) is applied to current
-- value when the attribute is set.
mapAttr :: (a -> b) -> (a -> b -> a) -> Attr w a -> Attr w b
mapAttr get set (Attr name reflect getter setter updater)
    = Attr name Nothing
                (\w   -> do a <- getter w; return (get a))
                (\w b -> do a <- getter w; setter w (set a b))
                (\w f -> do a <- updater w (\a -> set a (f (get a))); return (get a)) 

-- | (@mapAttrW conv attr@) maps an attribute of @Attr w a@ to
-- @Attr v a@ where (@conv :: v -> w@) is used to convert a widget
-- @v@ into a widget of type @w@.
mapAttrW :: (v -> w) -> Attr w a -> Attr v a
mapAttrW f attr
  = castAttr f attr

-- | Get the value of an attribute
--
-- > t <- get w text
--
get :: w -> Attr w a -> IO a
get w (Attr name reflect getter setter updater)
  = getter w

-- | Set a list of properties.
--
-- > set w [text := "Hi"]
--
set :: w -> [Prop w] -> IO ()
set w props
  = mapM_ setprop props
  where
    setprop ((Attr name reflect getter setter updater) := x)
      = setter w x
    setprop ((Attr name reflect getter setter updater) :~ f)
      = do updater w f; return ()
    setprop ((Attr name reflect getter setter updater) ::= f)
      = setter w (f w)
    setprop ((Attr name reflect getter setter updater) ::~ f)
      = do updater w (f w); return ()

-- | Set the value of an attribute and return the old value.
swap :: w -> Attr w a -> a -> IO a
swap w (Attr name reflect getter setter updater) x
  = updater w (const x)

-- | Retrieve the name of an attribute
attrName :: Attr w a -> String
attrName (Attr name _ _ _ _)
  = name

-- | Retrieve the name of a property.
propName :: Prop w -> String
propName (attr := x)    = attrName attr
propName (attr :~ f)    = attrName attr
propName (attr ::= f)   = attrName attr
propName (attr ::~ f)   = attrName attr


-- | Is a certain property in a list of properties?
containsProperty :: Attr w a -> [Prop w] -> Bool
containsProperty attr props
  = containsPropName (attrName attr) props

-- | Is a certain property in a list of properties?
containsPropName :: String -> [Prop w] -> Bool
containsPropName name props
  = any (\p -> propName p == name) props


-- | Property value: used when retrieving a property from a list.
data PropValue a  = PropValue  a
                  | PropModify (a -> a)
                  | PropNone

instance Show a => Show (PropValue a) where
  show (PropValue x)  = "PropValue " ++ show x
  show (PropModify f) = "PropModify"
  show (PropNone)     = "PropNone"

-- | Retrieve the value of a property and the list with the property removed.
filterProperty :: Typeable a => Attr w a -> [Prop w] -> (PropValue a, [Prop w])
filterProperty (Attr name _ _ _ _) props
  = walk [] PropNone props
  where
    -- Daan: oh, how a simple thing like properties can result into this... ;-)
    walk :: Typeable a => [Prop w] -> PropValue a -> [Prop w] -> (PropValue a, [Prop w])
    walk acc res props
      = case props of
          -- Property setter found.
          (((Attr attr (Just (todyn,fromdyn)) _ _ _) := x):rest)  | name == attr
              -> case fromDynamic (todyn x) of
                   Just x  -> walk acc (PropValue x) rest
                   Nothing -> walk acc res props
                   
          -- Property modifier found.
          (((Attr attr (Just (todyn,fromdyn)) _ _ _) :~ f):rest)  | name == attr
              -> let dynf x = case fromdyn (toDyn x) of
                                Just xx -> case fromDynamic (todyn (f xx)) of
                                             Just y  -> y
                                             Nothing -> x  -- identity
                                Nothing -> x -- identity
                 in case res of
                      PropValue x  -> walk acc (PropValue (dynf x)) rest
                      PropModify g -> walk acc (PropModify (dynf . g)) rest
                      PropNone     -> walk acc (PropModify dynf) rest

          -- Property found, but with the wrong arguments
          (((Attr attr _ _ _ _) := _):rest)   | name == attr  -> stop
          (((Attr attr _ _ _ _) :~ _):rest)   | name == attr  -> stop
          (((Attr attr _ _ _ _) ::= _):rest)  | name == attr  -> stop
          (((Attr attr _ _ _ _) ::~ _):rest)  | name == attr  -> stop

          -- Defaults
          (prop:rest)
              -> walk (prop:acc) res rest
          []  -> stop
       where
        stop  = (res, reverse acc ++ props)
  
               
-- | Try to find a property value and call the contination function with that value
-- and the property list witht the searched property removed. If the property is not
-- found, use the default value and the unchanged property list.
withProperty :: Typeable a => Attr w a -> a -> (a -> [Prop w] -> b) -> [Prop w] -> b
withProperty attr def cont props
  = case filterProperty attr props of
      (PropValue x, ps)  -> cont x ps
      (PropModify f, ps) -> cont (f def) ps
      (PropNone, ps)     -> cont def ps

-- | Try to find a property value. Return |Nothing| if not found at all.
findProperty :: Typeable a => Attr w a -> a -> [Prop w] -> Maybe (a,[Prop w])
findProperty attr def props
  = case filterProperty attr props of
      (PropValue x, ps)  -> Just (x,ps)
      (PropModify f, ps) -> Just (f def,ps)
      (PropNone, ps)     -> Nothing



-- | Transform the properties based on a style property.
withStyleProperty :: Attr w Bool -> Style -> ([Prop w] -> Style -> a) -> [Prop w] -> Style -> a
withStyleProperty prop flag 
  = withStylePropertyEx prop (bitsSet flag) (\isSet style -> if isSet then (style .+. flag) else (style .-. flag)) 

-- | Transform the properties based on a style property. The flag is interpreted negatively, i.e. |True|
-- removes the bit instead of setting it.
withStylePropertyNot :: Attr w Bool -> Style -> ([Prop w] -> Style -> a) -> [Prop w] -> Style -> a
withStylePropertyNot prop flag 
  = withStylePropertyEx prop (not . bitsSet flag) (\isSet style -> if isSet then (style .-. flag) else (style .+. flag)) 


-- | Transform the properties based on a style property.
withStylePropertyEx :: Attr w Bool -> (Style -> Bool) -> (Bool -> Style -> Style) -> ([Prop w] -> Style -> a) -> [Prop w] -> Style -> a
withStylePropertyEx prop def transform cont props style
  = case filterProperty prop props of
      (PropValue x, ps)  -> cont ps (transform x style) 
      (PropModify f, ps) -> cont ps (transform (f (def style)) style)
      (PropNone, ps)     -> cont ps style