/*
 * PaneManager.java
 *
 * Copyright (C) 2009-12 by RStudio, Inc.
 *
 * Unless you have received this program directly from RStudio pursuant
 * to the terms of a commercial license agreement with RStudio, then
 * this program is licensed to you under the terms of version 3 of the
 * GNU Affero General Public License. This program is distributed WITHOUT
 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
 *
 */
package org.rstudio.studio.client.workbench.ui;

import com.google.gwt.animation.client.Animation;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.SplitterResizedEvent;
import com.google.gwt.user.client.ui.Widget;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;

import org.rstudio.core.client.Debug;
import org.rstudio.core.client.JsArrayUtil;
import org.rstudio.core.client.Triad;
import org.rstudio.core.client.command.AppCommand;
import org.rstudio.core.client.command.CommandBinder;
import org.rstudio.core.client.command.Handler;
import org.rstudio.core.client.dom.DomUtils;
import org.rstudio.core.client.events.ManageLayoutCommandsEvent;
import org.rstudio.core.client.events.WindowEnsureVisibleEvent;
import org.rstudio.core.client.events.WindowStateChangeEvent;
import org.rstudio.core.client.js.JsObject;
import org.rstudio.core.client.layout.DualWindowLayoutPanel;
import org.rstudio.core.client.layout.LogicalWindow;
import org.rstudio.core.client.layout.WindowState;
import org.rstudio.core.client.theme.MinimizedModuleTabLayoutPanel;
import org.rstudio.core.client.theme.MinimizedWindowFrame;
import org.rstudio.core.client.theme.PrimaryWindowFrame;
import org.rstudio.core.client.theme.WindowFrame;
import org.rstudio.core.client.theme.res.ThemeResources;
import org.rstudio.core.client.widget.ToolbarButton;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.events.ZoomPaneEvent;
import org.rstudio.studio.client.workbench.model.ClientState;
import org.rstudio.studio.client.workbench.model.Session;
import org.rstudio.studio.client.workbench.model.WorkbenchServerOperations;
import org.rstudio.studio.client.workbench.model.helper.IntStateValue;
import org.rstudio.studio.client.workbench.model.helper.JSObjectStateValue;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.prefs.views.PaneLayoutPreferencesPane;
import org.rstudio.studio.client.workbench.views.console.ConsoleInterruptButton;
import org.rstudio.studio.client.workbench.views.console.ConsolePane;
import org.rstudio.studio.client.workbench.views.output.find.FindOutputTab;
import org.rstudio.studio.client.workbench.views.output.markers.MarkersOutputTab;
import org.rstudio.studio.client.workbench.views.source.SourceShim;
import org.rstudio.studio.client.workbench.views.source.SourceWindowManager;
import org.rstudio.studio.client.workbench.views.source.model.SourceDocument;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/*
 * TODO: Push client state when selected tab or layout changes
 */

public class PaneManager
{
   public interface Binder extends CommandBinder<Commands, PaneManager> {}
   
   public enum Tab {
      History, Files, Plots, Packages, Help, VCS, Build,
      Presentation, Environment, Viewer, Source, Console
   }
   
   class SelectedTabStateValue extends IntStateValue
   {
      SelectedTabStateValue(String name,
                            WorkbenchTabPanel tabPanel)
      {
         super("workbench-pane", name, ClientState.PROJECT_PERSISTENT,
               session_.getSessionInfo().getClientState(), true);
         tabPanel_ = tabPanel;
         finishInit(session_.getSessionInfo().getClientState());
      }

      @Override
      protected void onInit(Integer value)
      {
         if (value != null)
            tabPanel_.selectTab(value);
      }

      @Override
      protected Integer getValue() { return tabPanel_.getSelectedIndex(); }

      private final WorkbenchTabPanel tabPanel_;
   }
   
   private class ZoomedTabStateValue extends JSObjectStateValue
   {
      public ZoomedTabStateValue()
      {
         super("workbench-pane", "TabZoom", ClientState.PROJECT_PERSISTENT,
               session_.getSessionInfo().getClientState(), true);
         finishInit(session_.getSessionInfo().getClientState());
      }
      
      @Override
      protected void onInit(final JsObject value)
      {
         if (value == null)
            return;
         
         if (!value.hasKey(MAXIMIZED_TAB_KEY) || !value.hasKey(WIDGET_SIZE_KEY))
            return;
         
         // Time-out action just to ensure all client state is ready
         new Timer()
         {
            @Override
            public void run()
            {
               String tabString = value.getString(MAXIMIZED_TAB_KEY);
               double widgetSize = value.getDouble(WIDGET_SIZE_KEY);
               
               maximizedTab_ = Tab.valueOf(tabString);
               maximizedWindow_ = getWindowForTab(maximizedTab_);
               widgetSizePriorToZoom_ = widgetSize;
               fullyMaximizeWindow(maximizedWindow_, maximizedTab_);
               manageLayoutCommands();
            }
         }.schedule(200);
      }
      
      @Override
      protected boolean hasChanged()
      {
         if (lastValue_ == null)
            return true;
         
         JsObject oldValue = lastValue_;
         JsObject newValue = getValue();
         
         boolean oldHasKey = oldValue.hasKey(MAXIMIZED_TAB_KEY);
         boolean newHasKey = newValue.hasKey(MAXIMIZED_TAB_KEY);
         
         if (oldHasKey && newHasKey)
            return !oldValue.getString(MAXIMIZED_TAB_KEY).equals(newValue.getString(MAXIMIZED_TAB_KEY));
         
         return oldHasKey != newHasKey;
      }
      
      @Override
      protected JsObject getValue()
      {
         final JsObject object = JsObject.createJsObject();
         if (maximizedTab_ != null)
            object.setString(MAXIMIZED_TAB_KEY, maximizedTab_.toString());
         
         if (widgetSizePriorToZoom_ >= 0)
            object.setDouble(WIDGET_SIZE_KEY, widgetSizePriorToZoom_);
         
         lastValue_ = object;
         return object;
      }
      
      private static final String MAXIMIZED_TAB_KEY = "MaximizedTab";
      private static final String WIDGET_SIZE_KEY = "WidgetSize";
      
      private JsObject lastValue_ = null;
      
   }
   
   private LogicalWindow getWindowForTab(Tab tab)
   {
      switch (tab)
      {
      case Console: return getConsoleLogicalWindow();
      case Source:  return getSourceLogicalWindow();
      default:      return getOwnerTabPanel(tab).getParentWindow();
      }
   }

   @Inject
   public PaneManager(Provider<MainSplitPanel> pSplitPanel,
                      WorkbenchServerOperations server,
                      EventBus eventBus,
                      Session session,
                      Binder binder,
                      Commands commands,
                      UIPrefs uiPrefs,
                      @Named("Console") final Widget consolePane,
                      ConsoleInterruptButton consoleInterrupt,
                      SourceShim source,
                      @Named("History") final WorkbenchTab historyTab,
                      @Named("Files") final WorkbenchTab filesTab,
                      @Named("Plots") final WorkbenchTab plotsTab,
                      @Named("Packages") final WorkbenchTab packagesTab,
                      @Named("Help") final WorkbenchTab helpTab,
                      @Named("VCS") final WorkbenchTab vcsTab,
                      @Named("Build") final WorkbenchTab buildTab,
                      @Named("Presentation") final WorkbenchTab presentationTab,
                      @Named("Environment") final WorkbenchTab environmentTab,
                      @Named("Viewer") final WorkbenchTab viewerTab,
                      @Named("Compile PDF") final WorkbenchTab compilePdfTab,
                      @Named("Source Cpp") final WorkbenchTab sourceCppTab,
                      @Named("R Markdown") final WorkbenchTab renderRmdTab,
                      @Named("Deploy") final WorkbenchTab deployContentTab,
                      final MarkersOutputTab markersTab,
                      final FindOutputTab findOutputTab,
                      OptionsLoader.Shim optionsLoader)
   {
      eventBus_ = eventBus;
      session_ = session;
      commands_ = commands;
      uiPrefs_ = uiPrefs;
      consolePane_ = (ConsolePane)consolePane;
      consoleInterrupt_ = consoleInterrupt;
      source_ = source;
      historyTab_ = historyTab;
      filesTab_ = filesTab;
      plotsTab_ = plotsTab;
      packagesTab_ = packagesTab;
      helpTab_ = helpTab;
      vcsTab_ = vcsTab;
      buildTab_ = buildTab;
      presentationTab_ = presentationTab;
      environmentTab_ = environmentTab;
      viewerTab_ = viewerTab;
      compilePdfTab_ = compilePdfTab;
      findOutputTab_ = findOutputTab;
      sourceCppTab_ = sourceCppTab;
      renderRmdTab_ = renderRmdTab;
      deployContentTab_ = deployContentTab;
      markersTab_ = markersTab;
      optionsLoader_ = optionsLoader;
      
      binder.bind(commands, this);
      
      PaneConfig config = validateConfig(uiPrefs.paneConfig().getValue());
      initPanes(config);

      panes_ = createPanes(config);
      left_ = createSplitWindow(panes_.get(0), panes_.get(1), "left", 0.4);
      right_ = createSplitWindow(panes_.get(2), panes_.get(3), "right", 0.6);

      panel_ = pSplitPanel.get();
      panel_.initialize(left_, right_);
      
      // count the number of source docs assigned to this window
      JsArray<SourceDocument> docs = 
            session_.getSessionInfo().getSourceDocuments();
      String windowId = SourceWindowManager.getSourceWindowId();
      int numDocs = 0;
      for (int i = 0; i < docs.length(); i++)
      {
         String docWindowId = docs.get(i).getSourceWindowId();
         if (docWindowId == windowId)
         {
            numDocs++;
         }
      }
      
      if (numDocs == 0 && sourceLogicalWindow_.getState() != WindowState.HIDE)
      {
         sourceLogicalWindow_.onWindowStateChange(
               new WindowStateChangeEvent(WindowState.HIDE));
      }
      else if (numDocs > 0
               && sourceLogicalWindow_.getState() == WindowState.HIDE)
      {
         sourceLogicalWindow_.onWindowStateChange(
               new WindowStateChangeEvent(WindowState.NORMAL));
      }

      uiPrefs.paneConfig().addValueChangeHandler(new ValueChangeHandler<PaneConfig>()
      {
         public void onValueChange(ValueChangeEvent<PaneConfig> evt)
         {
            ArrayList<LogicalWindow> newPanes = createPanes(validateConfig(evt.getValue()));
            panes_ = newPanes;
            left_.replaceWindows(newPanes.get(0), newPanes.get(1));
            right_.replaceWindows(newPanes.get(2), newPanes.get(3));

            tabSet1TabPanel_.clear();
            tabSet2TabPanel_.clear();
            populateTabPanel(tabNamesToTabs(evt.getValue().getTabSet1()),
                             tabSet1TabPanel_, tabSet1MinPanel_);
            populateTabPanel(tabNamesToTabs(evt.getValue().getTabSet2()),
                             tabSet2TabPanel_, tabSet2MinPanel_);
            
            manageLayoutCommands();
         }
      });
      
      eventBus_.addHandler(ZoomPaneEvent.TYPE, new ZoomPaneEvent.Handler()
      {
         @Override
         public void onZoomPane(ZoomPaneEvent event)
         {
            String pane = event.getPane();
            LogicalWindow window = panesByName_.get(pane);
            assert window != null :
               "No pane with name '" + pane + "'";
            
            toggleWindowZoom(window, tabForName(event.getTab()));
         }
      });
      
      eventBus_.addHandler(WindowEnsureVisibleEvent.TYPE, new WindowEnsureVisibleEvent.Handler()
      {
         @Override
         public void onWindowEnsureVisible(WindowEnsureVisibleEvent event)
         {
            final LogicalWindow window = getLogicalWindow(event.getWindowFrame());
            if (window == null)
               return;
            
            // If we're currently zooming a pane, and we're now ensuring
            // a separate window is visible (e.g. a pane raising itself),
            // then transfer zoom to that window.
            if (maximizedWindow_ != null && !maximizedWindow_.equals(window))
            {
               fullyMaximizeWindow(window, lastSelectedTab_);
               return;
            }
            
            int width = window.getActiveWidget().getOffsetWidth();
            
            // If the widget is already visible horizontally, then bail
            // (other logic handles vertical visibility)
            if (width > 0)
               return;
            
            final Command afterAnimation = new Command()
            {
               @Override
               public void execute()
               {
                  window.getNormal().onResize();
               }
            };
            
            int newWidth = computeAppropriateWidth();
            resizeHorizontally(0, newWidth, afterAnimation);
         }
      });
      
      eventBus_.addHandler(
            ManageLayoutCommandsEvent.TYPE,
            new ManageLayoutCommandsEvent.Handler()
            {
               @Override
               public void onManageLayoutCommands(ManageLayoutCommandsEvent event)
               {
                  manageLayoutCommands();
               }
            });
      
      manageLayoutCommands();
      new ZoomedTabStateValue();
   }
   
   int computeAppropriateWidth()
   {
      double windowWidth = Window.getClientWidth();
      double candidateWidth = 2.0 * windowWidth / 5.0;
      return (int) candidateWidth;
   }
   
   LogicalWindow getLogicalWindow(WindowFrame frame)
   {
      for (LogicalWindow window : panes_)
         if (window.getNormal() == frame)
            return window;
      
      return null;
   }
   
   LogicalWindow getParentLogicalWindow(Element el)
   {
      LogicalWindow targetWindow = null;
      
      while (el != null && targetWindow == null)
      {
         el = el.getParentElement();
         for (LogicalWindow window : panes_)
         {
            if (el.equals(window.getActiveWidget().getElement()))
            {
               targetWindow = window;
               break;
            }
         }
      }
      return targetWindow;
   }
   
   public LogicalWindow getActiveLogicalWindow()
   {
      Element activeEl = DomUtils.getActiveElement();
      return getParentLogicalWindow(activeEl);
   }
   
   @Handler
   public void onLayoutZoomCurrentPane()
   {
      LogicalWindow activeWindow = getActiveLogicalWindow();
      if (activeWindow == null)
         return;
      toggleWindowZoom(activeWindow, null);
   }
   
   @Handler
   public void onLayoutEndZoom()
   {
      restoreLayout();
   }
   
   @Handler
   public void onLayoutConsoleOnLeft()
   {
      if (!commands_.layoutConsoleOnLeft().isChecked())
      {
         PaneConfig paneConfig = getCurrentConfig();
         int consoleTargetIndex = paneConfig.getConsoleLeftOnTop() ? 0 : 1;
         swapConsolePane(paneConfig, consoleTargetIndex);
      }
   }
   
   @Handler
   public void onLayoutConsoleOnRight()
   {
      if (!commands_.layoutConsoleOnRight().isChecked())
      {
         PaneConfig paneConfig = getCurrentConfig();
         int consoleTargetIndex = paneConfig.getConsoleRightOnTop() ? 2 : 3;
         swapConsolePane(paneConfig, consoleTargetIndex);
      }
   }
   
   
   private void swapConsolePane(PaneConfig paneConfig, int consoleTargetIndex)
   {
      int consoleCurrentIndex = paneConfig.getConsoleIndex();
      if (consoleCurrentIndex != consoleTargetIndex)
      {
         JsArrayString panes = JsArrayUtil.copy(paneConfig.getPanes());
         panes.set(consoleCurrentIndex, panes.get(consoleTargetIndex));
         panes.set(consoleTargetIndex, "Console");
         uiPrefs_.paneConfig().setGlobalValue(PaneConfig.create(
            panes, 
            paneConfig.getTabSet1(), 
            paneConfig.getTabSet2(),
            paneConfig.getConsoleLeftOnTop(),
            paneConfig.getConsoleRightOnTop()));
         uiPrefs_.writeUIPrefs();
      }
   }
   
   @Handler
   public void onPaneLayout()
   {
      optionsLoader_.showOptions(PaneLayoutPreferencesPane.class);
   }
   
   private <T> boolean equals(T lhs, T rhs)
   {
      if (lhs == null)
         return rhs == null;
      
      return lhs.equals(rhs);
   }
   
   public void toggleWindowZoom(LogicalWindow window, Tab tab)
   {
      if (isAnimating_)
         return;
      
      boolean hasZoom = maximizedWindow_ != null;
      
      if (hasZoom)
      {
         if (equals(window, maximizedWindow_))
         {
            // If we're zooming a different tab in the same window,
            // just activate that tab.
            if (!equals(tab, maximizedTab_))
            {
               maximizedTab_ = tab;
               manageLayoutCommands();
               activateTab(tab);
            }
            
            // Otherwise, we're trying to maximize the same tab
            // and the same window. Interpret this as a toggle off.
            else
            {
               restoreLayout();
            }
         }
         else
         {
            // We're transferring zoom from one window to another --
            // maximize the new window.
            fullyMaximizeWindow(window, tab);
         }
      }
      else
      {
         // No zoom currently on -- just zoom the selected window + tab.
         fullyMaximizeWindow(window, tab);
      }
   }
   
   private void fullyMaximizeWindow(final LogicalWindow window, final Tab tab)
   {
      if (window.equals(getSourceLogicalWindow()))
         maximizedTab_ = Tab.Source;
      else if (window.equals(getConsoleLogicalWindow()))
         maximizedTab_ = Tab.Console;
      else
         maximizedTab_ = tab;
      maximizedWindow_ = window;
      
      manageLayoutCommands();
      panel_.setSplitterEnabled(false);
         
      if (widgetSizePriorToZoom_ < 0)
         widgetSizePriorToZoom_ = panel_.getWidgetSize(right_);
      
      // Put all of the panes in NORMAL mode, just to ensure an appropriate
      // transfer to EXCLUSIVE mode works. (It seems that 'exclusive' -> 'exclusive'
      // transfers don't always propagate as expected)
      for (LogicalWindow pane : panes_)
         pane.onWindowStateChange(new WindowStateChangeEvent(WindowState.NORMAL));
      
      boolean isLeftWidget =
            DomUtils.contains(left_.getElement(), window.getActiveWidget().getElement());
      
      window.onWindowStateChange(new WindowStateChangeEvent(WindowState.EXCLUSIVE));
      
      final double initialSize = panel_.getWidgetSize(right_);
      
      double targetSize = isLeftWidget ? 0 : panel_.getOffsetWidth();
      
      if (targetSize < 0)
         targetSize = 0;
      
      // Ensure focus is sent to Help iframe on activation.
      Command onActivation = null;
      if (maximizedTab_.equals(Tab.Help))
      {
         onActivation = new Command()
         {
            @Override
            public void execute()
            {
               commands_.activateHelp().execute();
            }
         };
      }
      
      resizeHorizontally(initialSize, targetSize, onActivation);
      
   }
   
   private void resizeHorizontally(final double start,
                                   final double end)
   {
      resizeHorizontally(start, end, null);
   }
   
   private void resizeHorizontally(final double start,
                                   final double end,
                                   final Command afterComplete)
   {
      horizontalResizeAnimation(start, end, afterComplete).run(300);
   }
   
   private Animation horizontalResizeAnimation(final double start,
                                               final double end,
                                               final Command afterComplete)
   {
      return new Animation()
      {
         @Override
         protected void onUpdate(double progress)
         {
            double size =
                  (1 - progress) * start +
                  progress * end;
            
            panel_.setWidgetSize(right_, size);
         }

         @Override
         protected void onStart()
         {
            isAnimating_ = true;
            super.onStart();
         }

         @Override
         protected void onComplete()
         {
            isAnimating_ = false;
            panel_.onSplitterResized(new SplitterResizedEvent());
            super.onComplete();
            if (afterComplete != null)
               afterComplete.execute();
         }
      };
   }
   
   private void restoreLayout()
   {
      // If we're currently zoomed, then use that to provide the previous
      // 'non-zoom' state.
      if (maximizedWindow_ != null)
         restoreSavedLayout();
      else
         restoreFourPaneLayout();
   }
   
   private void invalidateSavedLayoutState(boolean enableSplitter)
   {
      maximizedWindow_ = null;
      maximizedTab_ = null;
      widgetSizePriorToZoom_ = -1;
      panel_.setSplitterEnabled(enableSplitter);
      manageLayoutCommands();
   }
   
   private void restoreFourPaneLayout()
   {
      // Ensure that all windows are in the 'normal' state. This allows
      // hidden windows to display themselves, and so on. This also forces
      // widgets to size themselves vertically.
      for (LogicalWindow window : panes_)
         window.onWindowStateChange(new WindowStateChangeEvent(WindowState.NORMAL, true));
      
      double rightWidth = panel_.getWidgetSize(right_);
      double panelWidth = panel_.getOffsetWidth();
      
      double minThreshold = (2.0 / 5.0) * panelWidth;
      double maxThreshold = (3.0 / 5.0) * panelWidth;
      
      if (rightWidth <= minThreshold)
         resizeHorizontally(rightWidth, minThreshold);
      else if (rightWidth >= maxThreshold)
         resizeHorizontally(rightWidth, maxThreshold);
      
      invalidateSavedLayoutState(true);
   }
   
   private void restoreSavedLayout()
   {
      // Ensure that all windows are in the 'normal' state. This allows
      // hidden windows to display themselves, and so on.
      for (LogicalWindow window : panes_)
         window.onWindowStateChange(new WindowStateChangeEvent(WindowState.NORMAL, true));
      
      maximizedWindow_.onWindowStateChange(new WindowStateChangeEvent(WindowState.NORMAL, true));
      resizeHorizontally(panel_.getWidgetSize(right_), widgetSizePriorToZoom_);
      invalidateSavedLayoutState(true);
   }
   
   @Handler
   public void onMaximizeConsole()
   {
      LogicalWindow consoleWindow = panesByName_.get("Console");
      if (consoleWindow.getState() != WindowState.MAXIMIZE)
      {
         consoleWindow.onWindowStateChange(
                        new WindowStateChangeEvent(WindowState.MAXIMIZE));
      }
   }

   private ArrayList<LogicalWindow> createPanes(PaneConfig config)
   {
      ArrayList<LogicalWindow> results = new ArrayList<LogicalWindow>();

      JsArrayString panes = config.getPanes();
      for (int i = 0; i < 4; i++)
      {
         results.add(panesByName_.get(panes.get(i)));
      }
      return results;
   }

   private void initPanes(PaneConfig config)
   {
      panesByName_ = new HashMap<String, LogicalWindow>();
      panesByName_.put("Console", createConsole());
      panesByName_.put("Source", createSource());

      Triad<LogicalWindow, WorkbenchTabPanel, MinimizedModuleTabLayoutPanel> ts1 = createTabSet(
            "TabSet1",
            tabNamesToTabs(config.getTabSet1()));
      panesByName_.put("TabSet1", ts1.first);
      tabSet1TabPanel_ = ts1.second;
      tabSet1MinPanel_ = ts1.third;

      Triad<LogicalWindow, WorkbenchTabPanel, MinimizedModuleTabLayoutPanel> ts2 = createTabSet(
            "TabSet2",
            tabNamesToTabs(config.getTabSet2()));
      panesByName_.put("TabSet2", ts2.first);
      tabSet2TabPanel_ = ts2.second;
      tabSet2MinPanel_ = ts2.third;
   }
   
   private ArrayList<Tab> tabNamesToTabs(JsArrayString tabNames)
   {
      ArrayList<Tab> tabList = new ArrayList<Tab>();
      for (int j = 0; j < tabNames.length(); j++)
         tabList.add(Enum.valueOf(Tab.class, tabNames.get(j)));
      return tabList;
   }

   private PaneConfig validateConfig(PaneConfig config)
   {
      if (config == null)
         config = PaneConfig.createDefault();
      if (!config.validateAndAutoCorrect())
      {
         Debug.log("Pane config is not valid");
         config = PaneConfig.createDefault();
      }
      return config;
   }

   public MainSplitPanel getPanel()
   {
      return panel_;
   }
   
   public WorkbenchTab getTab(Tab tab)
   {
      switch (tab)
      {
         case History:
            return historyTab_;
         case Files:
            return filesTab_;
         case Plots:
            return plotsTab_;
         case Packages:
            return packagesTab_;
         case Help:
            return helpTab_;
         case VCS:
            return vcsTab_;
         case Build:
            return buildTab_;
         case Presentation:
            return presentationTab_;
         case Environment:
            return environmentTab_;
         case Viewer:
            return viewerTab_;
         case Source:
         case Console:
            // not 'real' tabs so should be an error to ask for their tabs
      }
      throw new IllegalArgumentException("Unknown tab");
   }

   public WorkbenchTab[] getAllTabs()
   {
      return new WorkbenchTab[] { historyTab_, filesTab_,
                                  plotsTab_, packagesTab_, helpTab_,
                                  vcsTab_, buildTab_, presentationTab_,
                                  environmentTab_, viewerTab_};
   }

   public void activateTab(Tab tab)
   {
      lastSelectedTab_ = tab;
      WorkbenchTabPanel panel = getOwnerTabPanel(tab);
      
      // Ensure that the pane is visible (otherwise tab selection will fail)
      LogicalWindow parent = panel.getParentWindow();
      if (parent.getState() == WindowState.MINIMIZE ||
          parent.getState() == WindowState.HIDE)
      {
         parent.onWindowStateChange(new WindowStateChangeEvent(WindowState.NORMAL));
      }
      
      int index = tabToIndex_.get(tab);
      panel.selectTab(index);
   }
   
   public void activateTab(String tabName)
   {
      Tab tab = tabForName(tabName);
      if (tab != null)
         activateTab(tab);
   }
   
   public void zoomTab(Tab tab)
   {
      activateTab(tab);
      WorkbenchTabPanel tabPanel = getOwnerTabPanel(tab);
      LogicalWindow parentWindow = tabPanel.getParentWindow();
      if (parentWindow == null)
         return;
      
      toggleWindowZoom(parentWindow, tab);
   }

   public ConsolePane getConsole()
   {
      return consolePane_;
   }

   public WorkbenchTabPanel getOwnerTabPanel(Tab tab)
   {
      return tabToPanel_.get(tab);
   }

   public LogicalWindow getSourceLogicalWindow()
   {
      return sourceLogicalWindow_;
   }
   
   public LogicalWindow getConsoleLogicalWindow()
   {
      return panesByName_.get("Console");
   }

   private DualWindowLayoutPanel createSplitWindow(LogicalWindow top,
                                                   LogicalWindow bottom,
                                                   String name,
                                                   double bottomDefaultPct)
   {
      return new DualWindowLayoutPanel(
            eventBus_,
            top,
            bottom,
            session_,
            name,
            WindowState.NORMAL,
            (int) (Window.getClientHeight()*bottomDefaultPct));
   }

   private LogicalWindow createConsole()
   {
      PrimaryWindowFrame frame = new PrimaryWindowFrame("Console", null);

      ToolbarButton goToWorkingDirButton =
            commands_.goToWorkingDir().createToolbarButton();
      goToWorkingDirButton.addStyleName(
            ThemeResources.INSTANCE.themeStyles().windowFrameToolbarButton());
      
      LogicalWindow logicalWindow =
            new LogicalWindow(frame, new MinimizedWindowFrame("Console"));

      @SuppressWarnings("unused")
      ConsoleTabPanel consoleTabPanel = new ConsoleTabPanel(frame,
                                                            logicalWindow,
                                                            consolePane_,
                                                            compilePdfTab_,
                                                            findOutputTab_,
                                                            sourceCppTab_,
                                                            renderRmdTab_,
                                                            deployContentTab_,
                                                            markersTab_,
                                                            eventBus_,
                                                            consoleInterrupt_,
                                                            goToWorkingDirButton);
      
      return logicalWindow;
   }

   private LogicalWindow createSource()
   {
      WindowFrame sourceFrame = new WindowFrame();
      sourceFrame.setFillWidget(source_.asWidget());
      source_.forceLoad();
      return sourceLogicalWindow_ = new LogicalWindow(
            sourceFrame,
            new MinimizedWindowFrame("Source"));
   }

   private
         Triad<LogicalWindow, WorkbenchTabPanel, MinimizedModuleTabLayoutPanel>
         createTabSet(String persisterName, ArrayList<Tab> tabs)
   {
      final WindowFrame frame = new WindowFrame();
      final MinimizedModuleTabLayoutPanel minimized = new MinimizedModuleTabLayoutPanel();
      final LogicalWindow logicalWindow = new LogicalWindow(frame, minimized);

      final WorkbenchTabPanel tabPanel = new WorkbenchTabPanel(frame, logicalWindow);

      populateTabPanel(tabs, tabPanel, minimized);

      frame.setFillWidget(tabPanel);

      minimized.addSelectionHandler(new SelectionHandler<Integer>()
      {
         public void onSelection(SelectionEvent<Integer> integerSelectionEvent)
         {
            int tab = integerSelectionEvent.getSelectedItem();
            tabPanel.selectTab(tab);
         }
      });

      tabPanel.addSelectionHandler(new SelectionHandler<Integer>()
      {
         public void onSelection(SelectionEvent<Integer> integerSelectionEvent)
         {
            int index = integerSelectionEvent.getSelectedItem();
            WorkbenchTab selected = tabPanel.getTab(index);
            lastSelectedTab_ = workbenchTabToTab(selected);
            session_.persistClientState();
         }
      });

      new SelectedTabStateValue(persisterName, tabPanel);

      return new Triad<LogicalWindow, WorkbenchTabPanel, MinimizedModuleTabLayoutPanel>(
            logicalWindow,
            tabPanel,
            minimized);
   }
   
   private Tab workbenchTabToTab(WorkbenchTab tab)
   {
      return wbTabToTab_.get(tab);
   }

   private void populateTabPanel(ArrayList<Tab> tabs,
                                 WorkbenchTabPanel tabPanel,
                                 MinimizedModuleTabLayoutPanel minimized)
   {
      ArrayList<WorkbenchTab> tabList = new ArrayList<WorkbenchTab>();
      for (int i = 0; i < tabs.size(); i++)
      {
         Tab tab = tabs.get(i);
         WorkbenchTab wbTab = getTab(tab);
         
         wbTabToTab_.put(wbTab, tab);
         tabToPanel_.put(tab, tabPanel);
         tabToIndex_.put(tab, i);
         
         tabList.add(wbTab);
      }
      tabPanel.setTabs(tabList);

      ArrayList<String> labels = new ArrayList<String>();
      for (Tab tab : tabs)
      {
         if (!getTab(tab).isSuppressed())
            labels.add(getTabLabel(tab));
      }
      minimized.setTabs(labels.toArray(new String[labels.size()]));
   }

   private String getTabLabel(Tab tab)
   {
      switch (tab)
      {
         case VCS:
            return getTab(tab).getTitle();
         case Presentation:
            return getTab(tab).getTitle();
         default:
            return tab.toString();
      }
   }
   
   private Tab tabForName(String name)
   {
      if (name.equalsIgnoreCase("history"))
         return Tab.History;
      if (name.equalsIgnoreCase("files"))
         return Tab.Files;
      if (name.equalsIgnoreCase("plots"))
         return Tab.Plots;
      if (name.equalsIgnoreCase("packages"))
         return Tab.Packages;
      if (name.equalsIgnoreCase("help"))
         return Tab.Help;
      if (name.equalsIgnoreCase("vcs"))
         return Tab.VCS;
      if (name.equalsIgnoreCase("build"))
         return Tab.Build;
      if (name.equalsIgnoreCase("presentation"))
         return Tab.Presentation;
      if (name.equalsIgnoreCase("environment"))
         return Tab.Environment;
      if (name.equalsIgnoreCase("viewer"))
         return Tab.Viewer;
      if (name.equalsIgnoreCase("source"))
         return Tab.Source;
      if (name.equalsIgnoreCase("console"))
         return Tab.Console;
      
      return null;
   }
   
   private AppCommand getLayoutCommandForTab(Tab tab)
   {
      if (tab == null)
         return commands_.layoutEndZoom();
      
      switch (tab)
      {
      case Build:        return commands_.layoutZoomBuild();
      case Console:      return commands_.layoutZoomConsole();
      case Environment:  return commands_.layoutZoomEnvironment();
      case Files:        return commands_.layoutZoomFiles();
      case Help:         return commands_.layoutZoomHelp();
      case History:      return commands_.layoutZoomHistory();
      case Packages:     return commands_.layoutZoomPackages();
      case Plots:        return commands_.layoutZoomPlots();
      case Source:       return commands_.layoutZoomSource();
      case VCS:          return commands_.layoutZoomVcs();
      case Viewer:       return commands_.layoutZoomViewer();
      default:
         throw new IllegalArgumentException("Unexpected tab '" + tab.toString() + "'");
      }
   }
   
   private void manageLayoutCommands()
   {
      List<AppCommand> layoutCommands = getLayoutCommands();
      AppCommand activeCommand = getLayoutCommandForTab(maximizedTab_);
      
      for (AppCommand command : layoutCommands)
         command.setChecked(activeCommand.equals(command));
      
      // manage console left/right commands
      boolean maximized = maximizedTab_ != null;
      commands_.layoutConsoleOnLeft().setVisible(!maximized);
      commands_.layoutConsoleOnRight().setVisible(!maximized); 
      if (!maximized)
      {
         PaneConfig config = getCurrentConfig();
         commands_.layoutConsoleOnLeft().setChecked(config.getConsoleLeft());
         commands_.layoutConsoleOnRight().setChecked(config.getConsoleRight());
      }
      else
      {
         commands_.layoutConsoleOnLeft().setVisible(false);
         commands_.layoutConsoleOnRight().setVisible(false);
      } 
   }
   
   private List<AppCommand> getLayoutCommands()
   {
      List<AppCommand> commands = new ArrayList<AppCommand>();
      
      commands.add(commands_.layoutEndZoom());
      commands.add(commands_.layoutZoomBuild());
      commands.add(commands_.layoutZoomConsole());
      commands.add(commands_.layoutZoomEnvironment());
      commands.add(commands_.layoutZoomFiles());
      commands.add(commands_.layoutZoomHelp());
      commands.add(commands_.layoutZoomHistory());
      commands.add(commands_.layoutZoomPackages());
      commands.add(commands_.layoutZoomPlots());
      commands.add(commands_.layoutZoomSource());
      commands.add(commands_.layoutZoomVcs());
      commands.add(commands_.layoutZoomViewer());
      
      return commands;
   }
   
   private PaneConfig getCurrentConfig()
   {
      PaneConfig config = uiPrefs_.paneConfig().getValue();

      // use default config if pref isn't set yet
      if (config == null)
         return PaneConfig.createDefault();

      return config;
   }

   private final EventBus eventBus_;
   private final Session session_;
   private final Commands commands_;
   private final UIPrefs uiPrefs_;
   private final FindOutputTab findOutputTab_;
   private final WorkbenchTab compilePdfTab_;
   private final WorkbenchTab sourceCppTab_;
   private final ConsolePane consolePane_;
   private final ConsoleInterruptButton consoleInterrupt_;
   private final SourceShim source_;
   private final WorkbenchTab historyTab_;
   private final WorkbenchTab filesTab_;
   private final WorkbenchTab plotsTab_;
   private final WorkbenchTab packagesTab_;
   private final WorkbenchTab helpTab_;
   private final WorkbenchTab vcsTab_;
   private final WorkbenchTab buildTab_;
   private final WorkbenchTab presentationTab_;
   private final WorkbenchTab environmentTab_;
   private final WorkbenchTab viewerTab_;
   private final WorkbenchTab renderRmdTab_;
   private final WorkbenchTab deployContentTab_;
   private final MarkersOutputTab markersTab_;
   private final OptionsLoader.Shim optionsLoader_;
   private MainSplitPanel panel_;
   private LogicalWindow sourceLogicalWindow_;
   private final HashMap<Tab, WorkbenchTabPanel> tabToPanel_ =
         new HashMap<Tab, WorkbenchTabPanel>();
   private final HashMap<Tab, Integer> tabToIndex_ =
         new HashMap<Tab, Integer>();
   private final HashMap<WorkbenchTab, Tab> wbTabToTab_ =
         new HashMap<WorkbenchTab, Tab>();
   private HashMap<String, LogicalWindow> panesByName_;
   private DualWindowLayoutPanel left_;
   private DualWindowLayoutPanel right_;
   private ArrayList<LogicalWindow> panes_;
   private WorkbenchTabPanel tabSet1TabPanel_;
   private MinimizedModuleTabLayoutPanel tabSet1MinPanel_;
   private WorkbenchTabPanel tabSet2TabPanel_;
   private MinimizedModuleTabLayoutPanel tabSet2MinPanel_;
   
   // Zoom-related members ----
   private Tab lastSelectedTab_ = null;
   private LogicalWindow maximizedWindow_ = null;
   private Tab maximizedTab_ = null;
   private double widgetSizePriorToZoom_ = -1;
   private boolean isAnimating_ = false;
}
