{- Currently, this module contains functions for VPUI, VPUIWindow,
Workspace, VCanvas, and more.  A reorganization seems called for,
but for now I will just keep adding functions here.  -}

module Graphics.UI.Sifflet.Workspace
    (
     -- VPUI and Workspace
     vpuiNew
    , workspaceNewDefault
    , workspaceNewEditing
    , addFedWinButtons
    , defineFunction
    , workspaceId
    , openNode

     -- Quitting:
    , removeWindow
    , vpuiQuit

     -- Windows:
    , forallWindowsIO

    , baseFunctionsRows
    )

where

-- for debugging
-- import System.IO.Unsafe

import Control.Monad
import System.Directory (getCurrentDirectory)

import Data.IORef
import Data.List
import Data.Map ((!), keys)
import qualified Data.Map as Map (empty)
import Data.Maybe

import Data.Graph.Inductive as G hiding (nfilter)

import Data.Sifflet.Functoid
import Data.Sifflet.Geometry
import Data.Sifflet.TreeGraph
import Data.Sifflet.TreeLayout
import Data.Sifflet.WGraph

import Language.Sifflet.Expr
import Language.Sifflet.ExprTree

import Graphics.UI.Sifflet.Callback
import Graphics.UI.Sifflet.Canvas
import Graphics.UI.Sifflet.EditArgsPanel
import Graphics.UI.Sifflet.Frame
import Graphics.UI.Sifflet.GtkUtil

import Graphics.UI.Sifflet.LittleGtk
-- FIX!!
-- import Graphics.UI.Gtk (widgetSizeRequest, windowResize)

import Graphics.UI.Sifflet.Tool
import Graphics.UI.Sifflet.Types

import Language.Sifflet.Util

-- | Create a new VPUI.
-- This used to set up the basic "q to quit" and "on exposed" callbacks,
-- but now does not even do that.  
-- The 'init' function argument
-- may perform additional initialization;
-- if there is none, simply use 'return'.

-- The following comment is out of date,
-- but may explain some bizarre features historically:

-- Note that if you want to set up callbacks,
-- there is some trickiness: the vpui contains the workspace,
-- and the layout (which is on the workspace) needs to have callbacks
-- which know the uiref.  So, create the workspace, vpui, and uiref,
-- in that order, and then set up the callbacks.


vpuiNew :: Style -> Env -> Bool -> IO VPUI
vpuiNew style env debugging = do
  dir <- getCurrentDirectory
  return VPUI {vpuiWindows = Map.empty,
               vpuiToolkits = [],
               vpuiCurrentFile = Nothing,
               vpuiInitialDir = dir,
               vpuiCurrentDir = dir,
               vpuiStyle = style,
               vpuiInitialEnv = env,
               vpuiGlobalEnv = env,
               vpuiFileEnv = env,
               vpuiDebugging = debugging
              }



-- | Create a new "main" workspace window, with a given style.
-- The second argument should set up a menu bar and place it on the vbox, 
-- or do nothing if no menu is wanted.
workspaceNewDefault :: Style -> (VBox -> IO ()) -> IO Workspace
workspaceNewDefault style = 
    workspaceNew style (Size 3600.0 2400.0) (Just (Size 900.0 600.0)) []

workspaceNewEditing :: Style -> Env -> Function -> IO Workspace
workspaceNewEditing style initEnv func = do
  {
  ; let funcFrame = fedFuncFrame style func initEnv -- throw-away
        Size fwidth fheight = bbSize (cfBox funcFrame)
        canvSize = Size (max fwidth 300) (max fheight 300)
        mViewSize = Nothing
        specs = functionArgToolSpecs func
        addNoMenu _ = return ()        
  ; workspaceNew style canvSize mViewSize specs addNoMenu
  }

-- Add the buttons for argument tools, apply, close, etc.,
-- to a function editor window

addFedWinButtons :: CBMgr -> WinId -> VPUI -> IO ()
addFedWinButtons cbmgr winId vpui =
    case vpuiGetWindow vpui winId of
      VPUIWorkWin ws window -> 
          let bbar = wsButtonBar ws
              argSpecs = wsArgToolSpecs ws
              applyFrame :: VPUI -> IO VPUI
              applyFrame vpui' = 
                  let frames = vcFrames 
                               (vpuiWindowGetCanvas 
                                (vpuiGetWindow vpui' winId))
                  in case frames of
                       [] -> info "applyFrame: no frame found on canvas" >>
                             return vpui'
                       _:_:_ -> info ("applyFrame: more than one frame " ++
                                      "found on canvas " ++
                                      show frames) >>
                                return vpui'
                       [frame] -> defineFunction winId frame vpui'

              editParametersOK :: ArgSpecAction
              editParametersOK newSpecs = 
                  cbmgr (WithUIRef (editParametersOK' newSpecs))
              editParametersOK' newSpecs uiref =
                  do
                      stripButtonBar
                      dressButtonBar newSpecs
                      vpui' <- readIORef uiref
                      -- Maybe it should update more than just one window
                      -- here, in view of issues 36 and 31
                      --           VVV
                      let vpui'' = vpuiUpdateWindow vpui' 
                                   winId 
                                   (updateEditWindowArgSpecs newSpecs)
                      -- Should it also resize (smaller)
                      -- since the panel is being removed?
                      writeIORef uiref vpui''
                      -- redrawing must be done AFTER writing the IORef above.
                      redrawCanvas uiref winId

              editParameters :: VPUI -> IO VPUI
              editParameters vpui' =
                  do
                    {
                      let argSpecs' = vpuiWindowArgSpecs vpui' winId
                    ; argPanel <- 
                        makeEditArgsPanel cbmgr argSpecs' editParametersOK
                      -- attach it to the Gtk Layout at top left
                    ; let canv = wsCanvas ws
                          layout = vcLayout canv
                          panelRoot = editArgsPanelRoot argPanel
                    ; layoutPut layout panelRoot 1 1

                    -- Make the layout big enough to see the whole panel
                    ; expandToFit layout panelRoot

                    ; return vpui'
                    }
              addButton :: String -> CBMgrAction -> IO ()
              addButton label action = do
                {
                  button <- buttonNewWithLabel label
                ; boxPackEnd bbar button PackNatural 3
                ; widgetShow button
                ; cbmgr (AfterButtonClicked button action)
                }

              dressButtonBar :: [ArgSpec] -> IO ()
              dressButtonBar argSpecs' = do
                {
                  mapM_ (addArgToolButton cbmgr winId bbar) argSpecs'
                ; addButton "Close" (\ _uiref -> widgetDestroy window)
                ; addButton "Parameters" (modifyIORefIO editParameters)
                ; addButton "Apply" (modifyIORefIO applyFrame)
                }                

              stripButtonBar :: IO ()
              stripButtonBar = do
                {
                  -- the buttons should be the only children
                  children <- containerGetChildren bbar
                ; mapM_ widgetDestroy children
                }

          in dressButtonBar argSpecs

      FunctionPadWindow _ _ -> return () -- should not happen

vpuiWindowArgSpecs :: VPUI -> WinId -> [ArgSpec]
vpuiWindowArgSpecs vpui winId =
    case vpuiGetWindow vpui winId of
      VPUIWorkWin ws _ -> wsArgToolSpecs ws
      FunctionPadWindow _ _ -> [] -- should not happen

-- | Update an Edit Function window after the "Inputs" dialog,
-- which might have given it some new ArgSpec's.
-- In the resulting VPUIWindow, no node inlets or inputs than 
-- its new arity.

updateEditWindowArgSpecs :: [ArgSpec] -> VPUIWindow -> VPUIWindow
updateEditWindowArgSpecs newSpecs vwin =
    case vwin of 
      VPUIWorkWin ws0 window ->
          let canv0 = wsCanvas ws0
              graph0 = vcGraph canv0
              argNames = map argName newSpecs
          in case vcFrames canv0 of
               [frame0] ->
                   let foid0 = cfFunctoid frame0
                       fnodes0 = fpNodes foid0 :: [G.Node]

                       -- Filter out symbol nodes of any removed arguments
                       validNames = 
                           "if" : (argNames ++ envSymbols (cfEnv frame0))
                       validNode' :: G.Node -> Bool
                       validNode' = validNode graph0 validNames

                       fnodes1 = filter validNode' fnodes0
                       foid1 = foid0 {fpNodes = fnodes1,
                                      fpArgs = argNames} -- redundant args here
                       frame1 = frame0 {cfVarNames = argNames, -- and here
                                        cfFunctoid = foid1}
                       graph1 = nfilter validNode' graph0

                       -- Eliminate excess inlets and edges 
                       -- due to reduced arity of functional arguments
                       style = vcStyle canv0
                       graph2 = wLimitOuts style newSpecs graph1
                       -- Frame node adopts orphaned nodes so they are
                       -- not invisible
                       orphans = filter (isWSimple . fromJust . lab graph2)
                                        (graphOrphans graph2)
                       graph3 = 
                           adoptChildren graph2 (cfFrameNode frame1) orphans

                       -- plug back into larger structures
                       canv1 = canv0 {vcGraph = graph3, vcFrames = [frame1]}
                       ws1 = ws0 {wsArgToolSpecs = newSpecs, wsCanvas = canv1}
                   in VPUIWorkWin ws1 window
               _ -> vwin -- shouldn't happen
      FunctionPadWindow _ _ -> vwin -- shouldn't happen


-- | Given a WGraph and a list of valid symbols, a node is valid
-- if it is an ExprNode ... labeled with one of the valid symbols
--
-- (This function is only used above, in updateEditWindowArgSpecs)
validNode :: WGraph -> [String] -> G.Node -> Bool
validNode g validNames n =
    case G.lab g n of
      Nothing -> False
      Just (WFrame _) -> True
      Just (WSimple loNode) ->
          let ENode nodeLabel _ = gnodeValue (nodeGNode loNode)
          in case nodeLabel of
            NSymbol (Symbol name) -> name `elem` validNames
            _ -> True


-- | Require a node to have <= n inlets in the node
-- and <= n successors in the graph, where
-- n is specified in a list of ArgSpec

wLimitOut :: Style -> [ArgSpec] -> WGraph -> Node -> WGraph
wLimitOut style specs g v =
    let ordered :: Adj WEdge -> Adj WEdge
        ordered = sortBy compareAdj
        compareAdj (WEdge i, _nodei) (WEdge j, _nodej) = compare i j
    in case match v g of
         (Nothing, _) -> g
         (Just (ins, _v, wnode, outs), g') -> 
             -- wnode is the graph-node's "label"
             case wnode of
               WFrame _ -> g
               WSimple lonode@(LayoutNode {nodeGNode = gnode}) ->
                   let ENode eLabel _ = gnodeValue gnode -- :: ExprNode
                   in case eLabel of
                        NSymbol (Symbol name) ->
                            case aspecsLookup name specs of
                              Nothing -> g
                              Just n -> 
                                  let (inlets, _) = 
                                          makeIolets style 
                                                     (gnodeNodeBB gnode)
                                                     (n, 1)
                                      gnode' = gnode {gnodeInlets = inlets}
                                      wnode' = 
                                          WSimple (lonode {nodeGNode = gnode'})
                                  in (ins, v, wnode', take n (ordered outs)) & 
                                     g'
                        _ -> g

-- | Like wLimitOut, but applies to a whole graph
wLimitOuts :: Style -> [ArgSpec] -> WGraph -> WGraph
wLimitOuts style specs g = foldl (wLimitOut style specs) g (nodes g)

-- | Redraw the entire canvas of a given window
redrawCanvas :: IORef VPUI -> WinId -> IO ()
redrawCanvas uiref winId = do
  {
    vpui <- readIORef uiref
  ; let mcanvas = vpuiTryGetWindow vpui winId >>= vpuiWindowLookupCanvas 
  ; case mcanvas of
      Nothing -> return ()
      Just canvas ->
          case vcFrames canvas of
            [] -> return ()
            frame:_ -> vcInvalidateBox canvas (cfBox frame)
  }

addArgToolButton :: CBMgr -> WinId -> HBox -> ArgSpec -> IO ()
addArgToolButton cbmgr winId buttonBox (ArgSpec label n) = do
  {
    button <- buttonNewWithLabel label
  ; boxPackStart buttonBox button PackNatural 3 -- spacing between buttons
  ; widgetShow button
  ; cbmgr (AfterButtonClicked button 
           (modifyIORefIO (vpuiSetTool (ToolArg label n) winId)))
  ; return ()
  }


-- | fedFuncFrame generates a throw-away frame for the sole purplse
-- of obtaining its measurements before initializing the canvas

fedFuncFrame :: Style -> Function -> Env -> CanvFrame
fedFuncFrame style func prevEnv = 
  fst (frameNewWithLayout style (Position 0 0) 0 
                          (FunctoidFunc func) Nothing
                          CallFrame -- mode may change below
                          0 prevEnv Nothing)

-- | If mViewSize is Nothing, no scrollbars are put on the canvas,
-- and its display size request is its natural size.
-- If mViewSize = Just viewSize, then scrollbars are wrapped around
-- the canvas, and its displayed size request is viewSize.
-- addMenuBar is an action which, if desired, adds a menu bar;
-- if you don't want one, just pass (\ _ -> return ()).

workspaceNew :: Style -> Size -> Maybe Size -> [ArgSpec]
             -> (VBox -> IO ()) 
             -> IO Workspace
workspaceNew style canvSize mViewSize argSpecs addMenuBar = do
  {
  ; let Size dcWidth dcHeight = canvSize -- Double, Double
        (icWidth, icHeight) = (round dcWidth, round dcHeight)

        scrolled :: GtkLayout -> Size -> IO ScrolledWindow
        scrolled layout viewSize = do
          {
            let Size dvWidth dvHeight = viewSize -- Double, Double
                (iViewWidth, iViewHeight) = (round dvWidth, round dvHeight)
          -- Wrap layout directly in a ScrolledWindow .
          -- Adjustments: value lower upper stepIncr pageIncr pageSize
          ; xAdj <- adjustmentNew 0.0 0.0 dcWidth 10.0 dvWidth dvWidth
          ; yAdj <- adjustmentNew 0.0 0.0 dcHeight 10.0 dvHeight dvHeight
          ; scrollWin <- scrolledWindowNew (Just xAdj) (Just yAdj)
            -- show scrollbars? (never, always, or if needed)
          ; scrolledWindowSetPolicy scrollWin PolicyAutomatic PolicyAutomatic
          -- request view size for _layout_
          ; widgetSetSizeRequest layout iViewWidth iViewHeight
          ; set scrollWin [containerChild := layout]
          ; return scrollWin
          }

        bare :: GtkLayout -> IO GtkLayout
        bare layout = do
          {
            -- request canvas size for _layout_
          ; widgetSetSizeRequest layout icWidth icHeight --  new
          ; return layout
          }

  -- The canvas itself
  ; vcanvas <- vcanvasNew style dcWidth dcHeight
  ; let layout = vcLayout vcanvas
  -- Set the actual size of the canvas layout, which may be more
  -- than is displayed if scrollbars are used
  ; layoutSetSize layout icWidth icHeight

  -- An empty HBox for buttons (or it may remain empty)
  ; buttonBar <- hBoxNew False 3

  -- The Statusbar
  ; statusBar <- statusbarNew
  
  -- All together in a VBox
  ; vbox <- vBoxNew False 0 -- not equal space allotments, 0 spacing

  ; addMenuBar vbox

  ; let packGrow :: WidgetClass w => w -> IO ()
        packGrow w = boxPackStart vbox w PackGrow 0
  ; case mViewSize of
      Nothing -> bare layout >>= packGrow
      Just viewSize -> scrolled layout viewSize >>= packGrow

  ; boxPackStart vbox buttonBar PackNatural 0
  ; boxPackStart vbox statusBar PackNatural 0
  ; return (Workspace vbox vcanvas buttonBar statusBar argSpecs)
}


vpuiQuit :: VPUI -> IO VPUI
vpuiQuit vpui = do
  {
    -- This should also check for unsaved changes? ***
    vpui' <- foldM (\ vp winId -> removeWindow vp True winId)
                   vpui
                   (vpuiAllWindowKeys vpui)
  ; mainQuit
  ; return vpui'
  }

-- | List of all window ids of the vpui, 

vpuiAllWindowKeys :: VPUI -> [WinId]
vpuiAllWindowKeys = keys . vpuiWindows

-- | Perform action on all windows
-- (actually (WinId, VPUIWindow) pairs.
-- Returns updated VPUI (in case any windows are changed).

forallWindowsIO :: (VPUIWindow -> IO VPUIWindow) -> VPUI -> IO VPUI
forallWindowsIO action vpui = 
    let loop ks vpui' =
            case ks of
              [] -> return vpui'
              k : ks' -> 
                  let w = vpuiGetWindow vpui' k
                  in do 
                    {
                      w' <- action w
                    ; loop ks' (vpuiReplaceWindow vpui' k w')
                    }
    in loop (vpuiAllWindowKeys vpui) vpui


-- | This function is called either when a window *has been* destroyed,
-- with destroy = False,
-- or when you *want to* destroy a window, with destroy = True.

-- WOULD BE BETTER to have two functions, windowRemoved and removeWindow???

-- | removeWindow actually *closes* the window if destroy = True,
-- as well as removing it from the vpui's windows map.
removeWindow :: VPUI -> Bool -> WinId -> IO VPUI
removeWindow vpui destroy winId = do
  {
  -- Remove the window from vpui;
  -- if destroy is true, also destroy it.
  -- It is an error if the window id does not exist.
    let vwMap = vpuiWindows vpui
  ; when destroy $ widgetDestroy (vpuiWindowWindow (vwMap ! winId))
  ; return $ vpuiRemoveVPUIWindow winId vpui
  }

-- | Context menu command to apply the function definition
-- of an EditFrame.

-- | "Execute" the definition currently represented in the frame,
-- i.e., bind the function name in the global environment
-- to the function definition found in the frame.

defineFunction :: WinId -> CanvFrame -> VPUI -> IO VPUI
defineFunction winId frame vpui = 
    case frameType frame of
      CallFrame ->
          showErrorMessage "Software error\nNot in an edit frame!"
          >>  return vpui
      EditFrame ->
          case cfFunctoid frame of
            FunctoidFunc _function ->
                return vpui
            fparts@FunctoidParts {} ->
                let env = vpuiGlobalEnv vpui
                    vw = vpuiGetWindow vpui winId
                    canv = vpuiWindowGetCanvas vw
                    graph = vcGraph canv
                    frameNode = cfFrameNode frame
                    friendlyTypeError msg =
                      "Sifflet cannot find any set of " ++
                      "data types that will make this function work.\n" ++
                      "Details from the type checker (may be obscure):\n" ++
                      msg
                in case functoidToFunction fparts graph frameNode env of
                     Fail errmsg -> 
                         showErrorMessage (friendlyTypeError errmsg) >>
                         return vpui
                     Succ function ->
                         let BBox x y _ _ = cfBox frame
                             z = cfLevel frame
                             fname = functionName function
                             env' = envSet env fname (VFun function)
                             vpui' = vpui {vpuiGlobalEnv = env'}
                         in do 
                           {
                           ; canv' <- vcCloseFrame canv frame
                           -- can this use vpuiAddFrame? ***
                           ; canv'' <- 
                               vcAddFrame canv' (FunctoidFunc function) 
                                          Nothing 
                                          EditFrame
                                          env' x y z Nothing
                           ; let vw' = vpuiWindowSetCanvas vw canv''
                                 vpui'' = vpuiReplaceWindow vpui' winId vw'
                           ; vpuiUpdateCallFrames vpui'' fname
                           }

workspaceId :: String
workspaceId = "Sifflet Workspace"

-- | In the workspace window, update each frame calling the named function 
-- to reflect the current function definition
vpuiUpdateCallFrames :: VPUI -> String -> IO VPUI
vpuiUpdateCallFrames vpui fname = 
    case vpuiTryGetWindow vpui workspaceId of
      Nothing -> return vpui
      Just w -> do
        {
        ; let canvas = vpuiWindowGetCanvas w
              env = vpuiGlobalEnv vpui
              frames = callFrames canvas fname
              update canv frame = canvasUpdateCallFrame canv frame fname env
        ; canvas' <- foldM update canvas frames     
        ; let w' = vpuiWindowSetCanvas w canvas'
        ; return $ vpuiReplaceWindow vpui workspaceId w'
        }

-- | In the canvas, update a call frame with the current function
-- definition from the environment, returning a new canvas.
-- Root call frames are torn down and rebuilt with the new function definition.
-- Call frames that are called by other call frames are simply torn down.
canvasUpdateCallFrame :: VCanvas -> CanvFrame -> String -> Env -> IO VCanvas
canvasUpdateCallFrame canvas frame fname env = do
  {
    -- Tear down old frame
    canvas' <- vcCloseFrame canvas frame
  ; case cfParent frame of
      Nothing -> 
          -- root frame; build up new frame
          let Position x y = bbPosition (cfBox frame)
              z = cfLevel frame
              functoid = FunctoidFunc {fpFunc = envGetFunction env fname}
          in vcAddFrame canvas' functoid Nothing CallFrame env x y z Nothing
      Just _ -> 
          -- frame with a parent; finished
          return canvas'
  }

openNode :: VPUIWindow -> G.Node -> IO VPUIWindow
openNode vw node = do
  let canvas = vpuiWindowGetCanvas vw
      graph = vcGraph canvas
  if not (nodeIsSimple graph node)
    then return vw -- WFrame node -- is this possible?
    else if nodeIsOpen graph node
       then info "Already open" >> return vw
       else let frame = nodeContainerFrame canvas graph node
            in case nodeCompoundFunction graph frame node of
                 Nothing -> 
                     info "Cannot be opened" >> return vw
                 Just function ->
                     case nodeInputValues graph node of
                       EvalOk (VList values) ->
                           let env = extendEnv (functionArgNames function)
                                               values (cfEnv frame)
                               Position x y = 
                                   frameOffset (vcStyle canvas) frame
                               z = succ (cfLevel frame)
                           in vwAddFrame vw 
                                  (FunctoidFunc function) 
                                  (Just values) CallFrame
                                  env x y z (Just node)
                       EvalOk x ->
                           errcats ["openNode: non-VList result:", show x]
                       _ -> 
                           info "Cannot be opened: lacking input values" >>
                           return vw

baseFunctionsRows :: [[String]]
baseFunctionsRows = [["+", "-", "*", "div", "mod", "add1", "sub1", "/"],
                     ["==", "/=", "<", ">", "<=", ">="],
                     ["zero?", "positive?", "negative?"],
                     ["null", "head", "tail", ":"]]