{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Pianola.Model.Swing (
        GUI (..),
        Window (..),
        WindowInfo (..),
        WindowLike (..),
        Windowed (..),
        ComponentW (..),
        Component (..),
        ComponentInfo (..),
        ComponentType (..),
        ComponentLike (..),
        Cell (..),
        Tab (..),
        mainWindow,
        childWindow,
        windowTitled,
        clickButtonByText,
        clickButtonByToolTip,
        rightClickByText,
        popupItem,
        selectInMenuBar,
        toggleInMenuBar,
        selectInComboBox,
        selectTabByText, 
        selectTabByToolTip,
        expand,
        labeledBy   
    ) where

import Prelude hiding (catch)
import Data.Tree
import Data.Function
import Data.Functor.Identity
import qualified Data.Text as T
import Control.Error
import Control.Monad
import Control.Comonad
import Control.Applicative
import Control.Monad.Trans.Class
import Control.Comonad.Trans.Class
import Control.Comonad.Trans.Env    
import Data.List
import Pianola.Util
import Pianola.Pianola
import Pianola.Geometry
import Control.Monad.Logic

-- | A client-side representation of the state of a remote Swing GUI.
-- Interaction with the GUI is through actions in the monad /m/. 
type GUI m = [Window m]

newtype Window m = Window { unWindow :: Tree (WindowInfo m) }

-- | Typeclass instantiated by windows and components aware of belonging to a
-- window.
class Windowed w where
    window :: Monad n => w m -> n (Window m)

-- | Typeclass which provides convenience functions to supplement the bare fields of a 'WindowInfo' record.
class Windowed w => WindowLike w where
    wInfo :: w m -> WindowInfo m 

    title :: (Monad m,Monad n) => (w m) -> n T.Text
    title = return . _windowTitle . wInfo

    -- | If the window has a title that satisfies the predicate, returns the
    -- window, otherwise 'mzero'.
    hasTitle :: MonadPlus n => (T.Text -> Bool) -> w m -> n (w m)
    hasTitle f w = do
        guard . f $ _windowTitle . wInfo $ w  
        return w

    -- | Convenience function to access the components in the popup layer. Most
    -- of the time, clients should use 'popupItem' instead of this function.
    popupLayer :: Monad m => Glance m l (w m) (Component m)
    popupLayer = replusify . _popupLayer . wInfo

    -- | Convenience function to log an screenshot of a window.
    logcapture :: Monad m => Pianola m LogEntry (w m) ()
    logcapture = (peek $ liftN._capture.wInfo) >>= logimg

    -- | Convenience function which returns the content pane component
    -- augmented with a reference to the containing window. 
    contentPane :: Monad m => Glance m l (w m) (ComponentW m)
    contentPane win = 
        let concrete = runIdentity $ window win
        in return . ComponentW 
                  . EnvT concrete
                  . unComponent 
                  . _contentPane   
                  . wInfo 
                  $ win
    
    -- | Brings the window to the front of the screen.
    toFront :: Monad m => Glance m l (w m) (Sealed m)
    toFront = return . _toFront . wInfo

    -- | Sends an /escape/ keypress to the window.
    escape :: Monad m => Glance m l (w m) (Sealed m)
    escape = return . _escape . wInfo

    -- | Sends an /enter/ keypress to the window.
    enter :: Monad m => Glance m l (w m) (Sealed m)
    enter = return . _enter . wInfo

    close :: Monad m => Glance m l (w m) (Sealed m)
    close = return . _close . wInfo

instance Treeish (Window m) where
    children (Window c) = children c >>= return . Window
    descendants (Window c) = descendants c >>= return . Window

instance WindowLike Window where
    wInfo = rootLabel . unWindow

instance Windowed Window where
    window = return . id

data WindowInfo m = WindowInfo 
    {  _windowTitle::T.Text
    -- | Width, height.
    ,  _windowDim::(Int,Int) 
    -- | List of components in the menu bar. See 'selectInMenuBar'.
    ,  _menu::[Component m]
    -- | List of components in the popup layer.
    ,  _popupLayer:: [Component m]
    -- | The contents pane. All non-popup components of the window are
    -- descendants of the contents pane. See 'contentPane' and 'descendants'. 
    ,  _contentPane::Component m
    -- | Action which returns a screenshot capture of the window. See 'logcapture'.
    ,  _capture::Nullipotent m Image
    -- | See 'escape'. 
    ,  _escape::Sealed m
    -- | See 'enter'.
    ,  _enter::Sealed m
    -- | See 'close'.
    ,  _close::Sealed m
    -- | See 'toFront'.
    ,  _toFront::Sealed m
    } 

-- | A component which carries a reference to the window to which it belongs.
-- See 'Windowed'.
newtype ComponentW m = ComponentW 
    { unComponentW :: EnvT (Window m) Tree (ComponentInfo m) }

instance Treeish (ComponentW m) where
    children (ComponentW c) = children c >>= return . ComponentW
    descendants (ComponentW c) = descendants c >>= return . ComponentW

instance ComponentLike ComponentW where
    cInfo = rootLabel . lower . unComponentW

instance Windowed ComponentW where
    window = return . ask . unComponentW 

newtype Component m = Component 
    { unComponent :: Tree (ComponentInfo m) }

instance Treeish (Component m) where
    children (Component c) = children c >>= return . Component 
    descendants (Component c) = descendants c >>= return . Component

instance ComponentLike Component where
    cInfo = rootLabel . unComponent

data ComponentInfo m = ComponentInfo 
    {   -- | The position of the component within the containing window. 
       _pos::(Int,Int)
        -- | Width and height.
    ,  _dim::(Int,Int)
    ,  _name::Maybe T.Text
    ,  _tooltip::Maybe T.Text
        -- | The textual value of the component.
    ,  _text::Maybe T.Text
    ,  _enabled::Bool
    ,  _componentType::ComponentType m
    ,  _click::Sealed m
    ,  _doubleClick::Sealed m
    ,  _rightClick::Sealed m
    } 

instance ComponentLike c => Geometrical (c m) where
    nwcorner = _pos . cInfo
    dimensions = _dim . cInfo     

-- | Typeclass which provides convenience functions to supplement the bare fields of a 'ComponentInfo' record.
class ComponentLike c where
    cInfo :: c m -> ComponentInfo m 

    cType :: c m -> ComponentType m 
    cType = _componentType . cInfo 

    -- | Returns the component's textual content or 'mzero' if it doesn't have
    -- any.
    text :: MonadPlus n => c m -> n T.Text
    text = justZ . _text . cInfo

    -- | If the component has some kind of textual content and the text
    -- satisfies the predicate, returns the component, otherwise 'mzero'.
    hasText:: MonadPlus n => (T.Text -> Bool) -> c m -> n (c m)
    hasText f c = do
        t <- text $ c 
        guard $ f t
        return c

    -- | Returns the component's tooltip or 'mzero' if it doesn't have any.
    tooltip :: MonadPlus n => c m -> n T.Text
    tooltip = justZ . _tooltip . cInfo

    -- | If the component has a tooltip and the tooltip satisfies the
    -- predicate, returns the component, otherwise 'mzero'.
    hasToolTip:: MonadPlus n => (T.Text -> Bool) -> c m -> n (c m)
    hasToolTip f c = do
        t <- tooltip $ c 
        guard $ f t
        return c

    -- | If the component has a name and the name satisfies the predicate,
    -- returns the component, otherwise 'mzero'.
    hasName:: MonadPlus n => (T.Text -> Bool) -> c m -> n (c m)
    hasName f c = do
        t <- justZ._name.cInfo $ c 
        guard $ f t
        return c

    -- | Toggles the component to the desired state if the component is
    -- toggleable, 'mzero' otherwise.
    toggle:: MonadPlus n => Bool -> c m -> n (Sealed m)
    toggle b (cType -> Toggleable _ f) = return $ f b
    toggle _ _ = mzero

    -- | Returns the click action of a component.
    click:: Monad n => c m -> n (Sealed m)
    click = return._click.cInfo

    doubleClick:: Monad n => c m -> n (Sealed m)
    doubleClick = return._doubleClick.cInfo

    rightClick:: Monad n => c m -> n (Sealed m)
    rightClick = return._rightClick.cInfo

    -- | If the component is a button returns its click action, otherwise
    -- 'mzero'.
    clickButton:: MonadPlus n => c m -> n (Sealed m)
    clickButton (cType -> Button a) = return a
    clickButton _ = mzero

    -- | If the component is a combo box returns its click action, otherwise
    -- 'mzero'.
    clickCombo:: MonadPlus n => c m -> n (Sealed m)
    clickCombo (cType -> ComboBox _ a) = return a
    clickCombo _ = mzero

    -- | If the component is a list and has a cell whose renderer's text
    -- satisfies the predicate, returns the cell, otherwise 'mzero'.
    listCellByText:: MonadPlus n => (T.Text -> Bool) -> c m -> n (Cell m)
    listCellByText f (cType -> List l) = do 
        cell <- replusify l
        let renderer = _renderer cell
        descendants >=> hasText f $ renderer
        return cell
    listCellByText _ _ = mzero

    -- | If the component is a table and has a cell at the specified column
    -- whose renderer's text satisfies the predicate, returns a pair of the
    -- cell and the row to which it belongs, otherwise 'mzero'.
    tableCellByText:: MonadPlus n => Int -> (T.Text -> Bool) -> c m -> n (Cell m,[Cell m])  
    tableCellByText colIndex f (cType -> Table listOfCols) = do
        column <- atZ listOfCols colIndex
        (rowfocus,row) <- replusify $ zip column $ transpose listOfCols  
        let renderer = _renderer rowfocus
        descendants >=> hasText f $ renderer
        return (rowfocus,row)    
    tableCellByText _ _ _ = mzero

    -- | If the component is a tree and has a cell at the specified depth
    -- (starting at 0 for the root) whose renderer's text satisfies the
    -- predicate, returns the subtree which has the cell as a root, otherwise
    -- 'mzero'.
    treeCellByText :: MonadPlus n => Int -> (T.Text -> Bool) -> c m -> n (Tree (Cell m))
    treeCellByText depth f (cType -> Treegui cellForest) = do
        tree <- replusify cellForest
        level <- flip atZ depth . levels . duplicate $ tree
        subtree <- replusify level
        let renderer = _renderer . rootLabel $ subtree
        descendants >=> hasText f $ renderer
        return subtree
    treeCellByText _ _ _ = mzero

    -- | Returns the tabs of a component if the component is a tabbed pane,
    -- 'mzero' otherwise.
    tab:: MonadPlus n => c m -> n (Tab m)
    tab (cType -> TabbedPane p) = replusify p
    tab _ = mzero

    -- | If the component is a text field and is editable, set the text of the
    -- text field. Otherwise 'mzero'.
    setText:: MonadPlus n => T.Text -> c m -> n (Sealed m)
    setText txt c = case (cType c) of
        TextField (Just f) -> return $ f txt
        _ -> mzero

-- | Represents data specific to each subclass of Swing components.
data ComponentType m =
     Panel
 -- | A check box, either in a window or in a popup menu. The bool value is the
 -- current selection state.
    |Toggleable Bool (Bool -> Sealed m)
 -- | A button with its selection action. Menu items in popup menus are also
 -- treated as buttons.
    |Button (Sealed m)
 -- | 'Nothing' when the textfield is not editable.
    |TextField (Maybe (T.Text -> Sealed m)) 
    |Label
 -- | A combo box which may already have a selection, and which offers a click
 -- action which shows the drop-down list. See 'selectInComboBox'. 
    |ComboBox (Maybe (Component m)) (Sealed m)
 -- | See 'listCellByText'.
    |List [Cell m]
 -- | Tables are represented as lists of columns. See 'tableCellByText'.
    |Table [[Cell m]]
 -- | A list of trees of 'Cell'. It is a list of trees instead of a single tree
 -- so that JTrees which do not show the root can be represented. See 'treeCellByText'.
    |Treegui (Forest (Cell m)) 
 -- | In Swing, popup menus reside in the popup layer of a window or, if the
 -- popup extends beyond the window, in the contents pane of a child window
 -- created to hold the popup. See 'popupItem'.
    |PopupMenu  
 -- | See 'selectTabByText'. 
    |TabbedPane [Tab m]
 -- | The text value holds the name of the class.
    |Other T.Text

-- | Complex gui components like lists, tables and trees are represented as
-- list of cells, list of lists (list of columns) of cells, and trees of cells,
-- respectively.
--
-- Bear in mind that in Swing the renderer sub-components of a complex
-- component do /not/ count as children of the component. However, editor
-- components /do/ count as children of the component. 
--
-- A common case is to double click on a table cell to activate the cell's
-- editor, and then having to look for that editor among the descendants of the
-- table.
data Cell m = Cell 
    { 
    -- | The rendering component. Clients should not try to invoke actions on
    -- rendering components, as they are inert and only used for display
    -- purposes. 
      _renderer::Component m
    , _clickCell::Sealed m
    , _doubleClickCell::Sealed m
    , _rightClickCell::Sealed m
    -- | Always 'Nothing' for cells not belonging to trees.
    , _expand:: Maybe (Bool -> Sealed m)
    }

data Tab m = Tab
    { _tabText::T.Text
    , _tabToolTip::Maybe T.Text
    , _isTabSelected:: Bool
    , _selectTab::Sealed m
    }

-- | Returns the main window of the application. Only works properly when there
-- is only one top-level window.
mainWindow :: Glance m l (GUI m) (Window m)
mainWindow = replusify

-- | Returns the children of a window.
childWindow :: Glance m l (Window m) (Window m)
childWindow = children

-- | Returns all visible windows whose title satisfies the predicate.
windowTitled :: (T.Text -> Bool) -> Glance m l (GUI m) (Window m)
windowTitled f = replusify >=> descendants >=> hasTitle f 

-- | If the component or *any of its descendants* is a button whose text
-- satisfies the predicate, returns the click action. Otherwise 'mzero'.
clickButtonByText :: (Monad m,ComponentLike c,Treeish (c m)) => (T.Text -> Bool) -> Glance m l (c m) (Sealed m) 
clickButtonByText f = descendants >=> hasText f >=> clickButton

-- | Similar to 'clickButtonByText'.
clickButtonByToolTip :: (Monad m,ComponentLike c,Treeish (c m)) => (T.Text -> Bool) -> Glance m l (c m) (Sealed m) 
clickButtonByToolTip f = descendants >=> hasToolTip f >=> clickButton

-- | Similar to 'clickButtonByText'.
rightClickByText :: (Monad m,ComponentLike c,Treeish (c m)) => (T.Text -> Bool) -> Glance m l (c m) (Sealed m) 
rightClickByText f = descendants >=> hasText f >=> rightClick

-- | Returns all the visible popup items belonging to a window (that is, not
-- only the popup components themselves, but all their clickable children).
-- Clients should use this function instead of trying to access the popup layer
-- directly.
popupItem :: Monad m => Glance m l (Window m) (Component m)
popupItem w = 
    let insidepop = children >=> contentPane >=> descendants >=> \c -> 
            case cType c of
                PopupMenu -> descendants c
                _ -> mzero
    in (popupLayer >=> descendants $ w) `mplus` 
       (insidepop >=> return . Component . lower . unComponentW $ w)

-- | Performs a sequence of selections in a window menu, based to the text of
-- the options. Pass it something like 
--
-- > map (==) ["menuitem1","menuitem2',...]
--
-- To match the exact names of the options.
selectInMenuBar :: Monad m => [T.Text -> Bool] -> Pianola m l (Window m) ()
selectInMenuBar ps = 
    let go (firstitem,middleitems,lastitem) = do
           poke $ replusify._menu.wInfo >=> descendants >=> hasText firstitem >=> clickButton
           let pairs = zip middleitems (clickButton <$ middleitems) ++
                       [(lastitem, clickButton)]
           forM_ pairs $ \(txt,action) -> 
               pmaybe pfail $ retryPoke1s 7 $ 
                   popupItem >=> hasText txt >=> action
        clip l = (,,) <$> headZ l <*> (initZ l >>= tailZ) <*> lastZ l
    in maybe pfail go (clip ps)

-- | Like 'selectInMenuBar', but for when the last item is a toggleable
-- component. The boolean paramenter is the desired selection state.
toggleInMenuBar :: Monad m => Bool -> [T.Text -> Bool] -> Pianola m l (Window m) ()
toggleInMenuBar toggleStatus ps = 
    let go (firstitem,middleitems,lastitem) = do
           poke $ replusify._menu.wInfo >=> descendants >=> hasText firstitem >=> clickButton
           let pairs = zip middleitems (clickButton <$ middleitems) ++
                       [(lastitem, toggle toggleStatus)]
           forM_ pairs $ \(txt,action) -> 
               pmaybe pfail $ retryPoke1s 7 $ 
                   popupItem >=> hasText txt >=> action
           replicateM_ (length pairs) $ poke escape
        clip l = (,,) <$> headZ l <*> (initZ l >>= tailZ)  <*> lastZ l
    in maybe pfail go (clip ps)

-- | If the component is a combo box, clicks on it and selects an option by its
-- text. Otherwise fails.
selectInComboBox :: (Monad m, ComponentLike c, Windowed c) => (T.Text -> Bool) -> Pianola m l (c m) ()
selectInComboBox f = do
        poke $ clickCombo
        poke $ window >=> popupItem >=> listCellByText f >=> return._clickCell

-- | If the component is a tabbed pane returns the select action of a tab whose
-- text matches the predicate. Returns 'mzero' if the component is not a tabbed
-- pane.
selectTabByText :: (Monad m,ComponentLike c) => (T.Text -> Bool) -> Glance m l (c m) (Sealed m)
selectTabByText f =  
    tab >=> \aTab -> do    
        guard $ f . _tabText $ aTab
        return $ _selectTab aTab   

-- | Similar to 'selecTabByText'.
selectTabByToolTip :: (Monad m,ComponentLike c) => (T.Text -> Bool) -> Glance m l (c m) (Sealed m)
selectTabByToolTip f =  
    tab >=> \aTab -> do    
        tooltip <- justZ . _tabToolTip $ aTab
        guard $ f tooltip
        return $ _selectTab aTab   

-- | Returns the expand/collapse action of the root node of a tree of cells,
-- depending on a boolean parameter.  Useful with gui trees.
expand :: Monad m => Bool -> Glance m l (Tree (Cell m)) (Sealed m)
expand b cell = (justZ . _expand . rootLabel $ cell) <*> pure b

-- | Takes a component, searches its descendants to find a label whose text
-- matches the predicate, finds the component to which the label applies, and
-- returns it.
--
-- Useful for targeting text fields in form-like dialogs.
labeledBy :: (Monad m,ComponentLike c,Treeish (c m)) => (T.Text -> Bool) -> Glance m l (c m) (c m)
labeledBy f o = do
    ref <- descendants o 
    Label {} <- return . cType $ ref
    hasText f ref  
    let 
        positioned = sameLevelRightOf ref  
        labellable c = case cType c of
            Toggleable {} -> True
            Button {} -> True
            TextField {} -> True
            ComboBox {} -> True
            List {} -> True
            Table {} -> True
            Treegui {} -> True
            _ -> False
    candidates <- lift . observeAllT $ do
        c <- descendants o 
        guard $ labellable c && positioned c
        return c
    headZ $ sortBy (compare `on` minX) candidates