{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
--
-- Module      :  VCSGui.Common.GtkHelper
-- Copyright   :  2011 Stephan Fortelny, Harald Jagenteufel
-- License     :  GPL
--
-- Maintainer  :  stephanfortelny at gmail.com, h.jagenteufel at gmail.com
-- Stability   :
-- Portability :
--
-- | This module contains functions to help building a GTK GUI using GTKBuilder.
--
-----------------------------------------------------------------------------

module VCSGui.Common.GtkHelper (
-- * Typesynonyms to wrap Gtk objects (*Item)
--
-- | These are built after the following scheme:
--
-- @((Name of the item as in the gladefile), (actual gtk object), (getter, setter))@
--
-- Note however that you can (and should) use the functions 'getName', 'getItem', 'get' and 'set'.

    openGladeFile
    , getWindowFromGlade
    , getActionFromGlade
    , getLabelFromGlade
    , getTextEntryFromGlade
    , getTextViewFromGlade
    , getComboBoxFromGlade
    , getCheckButtonFromGlade
    , getButtonFromGlade
    , getTreeViewFromGlade
    , getTreeViewFromGladeCustomStore
    , addColumnToTreeView
    , addColumnToTreeView'
    , addTextColumnToTreeView
    , addTextColumnToTreeView'


    , getName
    , getItem
    , get
    , set

    , closeWin
    , registerClose
    , registerCloseAction
    , registerQuit
    , registerQuitAction

    , WindowItem
    , ActionItem
    , LabelItem
    , TextEntryItem
    , TextViewItem
    , ComboBoxItem
    , CheckButtonItem
    , TreeViewItem
    , ButtonItem
) where

import qualified Graphics.UI.Gtk as Gtk

import System.Directory
import Control.Monad.Trans(liftIO)
import System.IO (hPutStrLn, stderr)
import VCSGui.Common.Helpers (emptyTextToNothing)
import Data.Text (Text)
import qualified Data.Text as T (unpack)

-- Typesynonyms
type WindowItem = (Text, Gtk.Window, ())
type ActionItem = (Text, Gtk.Action, ())
type LabelItem = (Text, Gtk.Label, (IO (Maybe Text), Text -> IO ()))
type TextEntryItem = (Text, Gtk.Entry, (IO (Maybe Text), Text -> IO ()))
type ComboBoxItem = (Text, Gtk.ComboBox, (IO (Maybe Text), [Text] -> IO ()))
type TextViewItem = (Text, Gtk.TextView, (IO (Maybe Text), Text -> IO ()))
type TreeViewItem a = (Text, (Gtk.ListStore a, Gtk.TreeView), (IO (Maybe [a]), [a] -> IO ()))
type CheckButtonItem = (Text, Gtk.CheckButton, (IO Bool, Bool -> IO()))
type ButtonItem = (Text, Gtk.Button, (IO Text, Text -> IO()))

-- Type accessors

-- | return the name of this item (as in the gladefile)
getName :: (Text, a, b) -> Text
getName (n, _, _) = n

-- | return the Gtk object wrapped by given item
getItem :: (Text, a, b) -> a
getItem (_, item, _) = item

-- | call teh get method of an *Item
get :: (Text, a, (b, c)) -> b
get (_, _, (getter, _)) = getter

-- | call the set method of an *Item
set :: (Text, a, (b, c)) -> c
set (_,_, (_, setter)) = setter


----------------------
-- *FromGlade
----------------------

-- | Open a gladefile with a new 'Gtk.Builder'.
openGladeFile :: FilePath -- ^ Gladefile to open.
    -> IO Gtk.Builder
openGladeFile fn = do
    builder <- Gtk.builderNew
    Gtk.builderAddFromFile builder fn
    return builder

-- | Get a 'WindowItem' from a gladefile.
getWindowFromGlade :: Gtk.Builder
    -> Text -- ^ name of the window to get as specified in the gladefile.
    -> IO WindowItem
getWindowFromGlade builder name = do
    (a, b) <- wrapWidget builder Gtk.castToWindow name
    return (a, b, ())

-- | Get an 'ActionItem' from a gladefile.
getActionFromGlade :: Gtk.Builder
    -> Text -- ^ name of the action to get as specified in the gladefile.
    -> IO ActionItem
getActionFromGlade builder name = do
    (a, b) <- wrapWidget builder Gtk.castToAction name
    return (a, b, ())

-- | Get an 'LabelItem' from a gladefile.
getLabelFromGlade :: Gtk.Builder
    -> Text -- ^ name of the label to get as specified in the gladefile.
    -> IO LabelItem
getLabelFromGlade builder name = do
    (_, entry) <- wrapWidget builder Gtk.castToLabel name
    let getter = error "don't call get on a gtk label!" :: IO (Maybe Text)
        setter val = Gtk.labelSetText entry val :: IO ()
    return (name, entry, (getter, setter))

-- | Get a 'ButtonItem' from a gladefile.
getButtonFromGlade :: Gtk.Builder
    -> Text -- ^ name of the button to get as specified in the gladefile.
    -> IO ButtonItem
getButtonFromGlade builder name = do
    (_,btn) <- wrapWidget builder Gtk.castToButton name
    let getter = Gtk.buttonGetLabel btn :: IO Text
        setter val = Gtk.buttonSetLabel btn val
    return (name, btn, (getter,setter))

-- | Get a 'TextEntryItem' from a gladefile.
getTextEntryFromGlade :: Gtk.Builder
    -> Text -- ^ name of the text entry to get as specified in the gladefile.
    -> IO TextEntryItem
getTextEntryFromGlade builder name = do
    (_, entry) <- wrapWidget builder Gtk.castToEntry name
    let getter = fmap emptyTextToNothing $ Gtk.entryGetText entry :: IO (Maybe Text)
        setter val = Gtk.entrySetText entry val :: IO ()
    return (name, entry, (getter, setter))

-- | Get a 'ComboBoxItem' from a gladefile.
getComboBoxFromGlade :: Gtk.Builder
                    -> Text -- ^ name of the combo box to get as specified in the gladefile.
                    -> IO ComboBoxItem
getComboBoxFromGlade builder name = do
    (_, combo) <- wrapWidget builder Gtk.castToComboBox name
    Gtk.comboBoxSetModelText combo
    let getter = Gtk.comboBoxGetActiveText combo  :: IO (Maybe Text) -- get selected text
        setter entries = do -- fill with new entries
            store <- Gtk.comboBoxGetModelText combo
            Gtk.listStoreClear store
            mapM_ (Gtk.listStoreAppend store) entries
            return ()
    return (name, combo, (getter, setter))

-- | Get a 'TextViewItem' from a gladefile.
getTextViewFromGlade :: Gtk.Builder
    -> Text -- ^ name of the text view to get as specified in the gladefile.
    -> IO TextViewItem
getTextViewFromGlade builder name =  do
        (_, entry)  <- wrapWidget builder Gtk.castToTextView name
        buffer <- Gtk.textViewGetBuffer entry
        let getter = getLongText buffer :: IO (Maybe Text)
            setter = (\text -> Gtk.textBufferSetText buffer text) :: Text -> IO ()
        return (name, entry, (getter, setter))
    where
    getLongText buffer = do
        start <- Gtk.textBufferGetStartIter buffer
        end <- Gtk.textBufferGetEndIter buffer
        isEmpty <- (Gtk.textIterEqual start end)
        if isEmpty then return Nothing else do
            s <- Gtk.textBufferGetText buffer start end True -- True to inclue hidden char
            return $ Just s

-- | Get a 'CheckButtonItem' from a gladefile.
getCheckButtonFromGlade :: Gtk.Builder
    -> Text -- ^ name of the check button to get as specified in the gladefile.
    -> IO CheckButtonItem
getCheckButtonFromGlade builder name = do
        (_,bt) <- wrapWidget builder Gtk.castToCheckButton name
        let getter = Gtk.toggleButtonGetActive bt
            setter = (\bool -> Gtk.toggleButtonSetActive bt bool) :: Bool -> IO()
        return (name,bt, (getter,setter))

---------------------------------
-- TreeView
---------------------------------

-- | Get a 'TreeViewItem' from a gladefile.
getTreeViewFromGlade :: Gtk.Builder
    -> Text -- ^ name of the tree view to get as specified in the gladefile.
    -> [a] -- ^ Content of the new tree view.
    -> IO (TreeViewItem a)
getTreeViewFromGlade builder name rows = do
    (_, tView) <- wrapWidget builder Gtk.castToTreeView name
    entry@(store, treeView) <- createStoreForTreeView tView rows
    let getter = getFromListStore entry
        setter = setToListStore entry
    return (name, (store, treeView), (getter, setter))

-- | Get a 'TreeViewItem' from a gladefile.
getTreeViewFromGladeCustomStore :: Gtk.Builder
                        -> Text -- ^ name of the tree view to get as specified in the gladefile.
                        -> (Gtk.TreeView -> IO (Gtk.ListStore a)) -- ^ fn defining how to setup the liststore
                        -> IO (TreeViewItem a)
getTreeViewFromGladeCustomStore builder name setupListStore = do
    (_, tView) <- wrapWidget builder Gtk.castToTreeView name
    store <- setupListStore tView
    Gtk.treeViewSetModel tView store
    let getter = getFromListStore (store, tView)
        setter = setToListStore (store, tView)
    return (name, (store, tView), (getter, setter))

-- | Create a new 'Gtk.ListStore' for a 'Gtk.TreeView'.
createStoreForTreeView :: Gtk.TreeView -- ^ The created list store will be set the model for this TreeView.
    -> [a] -- ^ Content of the new store.
    -> IO (Gtk.ListStore a, Gtk.TreeView)
createStoreForTreeView listView rows = do
    listStore <- Gtk.listStoreNew rows
    Gtk.treeViewSetModel listView listStore
    return (listStore, listView)

-- | Get the content of a ListStore.
getFromListStore :: (Gtk.ListStore a, Gtk.TreeView)
    -> IO (Maybe [a]) -- ^ Nothing if the ListStore is empty.
getFromListStore (store, _) = do
    list <- Gtk.listStoreToList store
    if null list
        then return Nothing
        else return $ Just list

-- | Set the content of a ListStore.
setToListStore :: (Gtk.ListStore a, Gtk.TreeView)
    -> [a] -- ^ New content of the ListStore.
    -> IO ()
setToListStore (store, view) newList = do
    Gtk.listStoreClear store
    mapM_ (Gtk.listStoreAppend store) newList
    return ()

--
-- Various helpers
--

-- | Close a window.
closeWin :: WindowItem -> IO ()
closeWin win = (Gtk.widgetHide (getItem win))

-- | Close a window if 'Gtk.deleteEvent' occurs on this 'WindowItem'.
registerClose :: WindowItem -> IO ()
registerClose win = Gtk.on (getItem win) Gtk.deleteEvent (liftIO (closeWin win) >> return False) >> return ()

-- | Close a window if the specified action occurs on this 'WindowItem'.
registerCloseAction :: ActionItem -> WindowItem -> IO ()
registerCloseAction act win = Gtk.on (getItem act) Gtk.actionActivated (liftIO (closeWin win)) >> return ()

-- | Call 'Gtk.mainQuit' if 'Gtk.deleteEvent' occurs on this 'WindowItem'.
registerQuit :: WindowItem -> IO ()
registerQuit win = Gtk.on (getItem win) Gtk.deleteEvent (liftIO $ Gtk.mainQuit >> return False) >> return ()

-- | Call 'Gtk.mainQuit' if the specified action occurs on this 'WindowItem'.
registerQuitAction :: ActionItem -> IO ()
registerQuitAction act = Gtk.on (getItem act) Gtk.actionActivated (liftIO (Gtk.mainQuit)) >> return ()

-- TODO fun argument is not used. what was the purpos of this function?
-- | same as 'registerQuitAction' since second argument is ignored (?)
registerQuitWithCustomFun :: WindowItem
                -> IO () -- ^ custom fun
                -> IO ()
registerQuitWithCustomFun win fun = Gtk.on (getItem win) Gtk.deleteEvent (liftIO $ Gtk.mainQuit >> return False) >> return ()

-- | Add a column to given ListStore and TreeView using a mapping.
-- The mapping consists of a CellRenderer, the title and a function, that maps each row to attributes of the column
addColumnToTreeView :: Gtk.CellRendererClass r =>
    TreeViewItem a
    -> r -- ^ CellRenderer
    -> Text -- ^ title
    -> (a -> [Gtk.AttrOp r]) -- ^ mapping
    -> IO ()
addColumnToTreeView (_, item, _) = do
    addColumnToTreeView' item
--    newCol <- Gtk.treeViewColumnNew
--    Gtk.set newCol [Gtk.treeViewColumnTitle Gtk.:= title]
--    Gtk.treeViewAppendColumn listView newCol
--    Gtk.treeViewColumnPackStart newCol renderer True
--    Gtk.cellLayoutSetAttributes newCol renderer listStore value2attributes

-- | Same as 'addColumnToTreeView'. This function can be called without a complete 'TreeViewItem'.
addColumnToTreeView' :: Gtk.CellRendererClass r =>
    (Gtk.ListStore a, Gtk.TreeView)
    -> r
    -> Text
    -> (a -> [Gtk.AttrOp r])
    -> IO ()
addColumnToTreeView' (listStore, listView) renderer title value2attributes = do
    newCol <- Gtk.treeViewColumnNew
    Gtk.set newCol [Gtk.treeViewColumnTitle Gtk.:= title]
    Gtk.treeViewAppendColumn listView newCol
    Gtk.treeViewColumnPackStart newCol renderer True
    Gtk.cellLayoutSetAttributes newCol renderer listStore value2attributes

-- | Shortcut for adding text columns to a TreeView. See 'addColumnToTreeView'.
addTextColumnToTreeView :: TreeViewItem a
    -> Text -- ^ title
    -> (a -> [Gtk.AttrOp Gtk.CellRendererText]) -- ^ mapping
    -> IO ()
addTextColumnToTreeView tree title map = do
    r <- Gtk.cellRendererTextNew
    addColumnToTreeView tree r title map

-- | Shortcut for adding text columns to a TreeView. See 'addColumnToTreeView\''.
addTextColumnToTreeView' :: (Gtk.ListStore a, Gtk.TreeView)
    -> Text
    -> (a -> [Gtk.AttrOp Gtk.CellRendererText])
    -> IO ()
addTextColumnToTreeView' item title map = do
    r <- Gtk.cellRendererTextNew
    addColumnToTreeView' item r title map

---------------------------
-- internal helpers
---------------------------

wrapWidget :: Gtk.GObjectClass objClass =>
     Gtk.Builder
     -> (Gtk.GObject -> objClass)
     -> Text -> IO (Text, objClass)
wrapWidget builder cast name = do
    hPutStrLn stderr $ " cast " ++ T.unpack name
    gobj <- Gtk.builderGetObject builder cast name
    return (name, gobj)