/*
 * Wizard.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.core.client.widget;


import java.util.ArrayList;

import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.Debug;
import org.rstudio.core.client.theme.res.ThemeResources;

import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.layout.client.Layout.AnimationCallback;
import com.google.gwt.layout.client.Layout.Layer;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.LayoutPanel;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;


public class Wizard<I,T> extends ModalDialog<T>
{
   public Wizard(String caption, 
                 String okCaption,
                 I initialData,
                 WizardPage<I, T> firstPage,
                 final ProgressOperationWithInput<T> operation)
   {
      super(caption, operation);
      initialData_ = initialData;
      okCaption_ = okCaption;
      firstPage_ = firstPage;
      activePage_ = firstPage;
      
      resetOkButtonCaption();
      setOkButtonVisible(false);
      
      addCloseHandler(new CloseHandler<PopupPanel>()
      {
         @Override
         public void onClose(CloseEvent<PopupPanel> arg0)
         {
            cleanupPage(firstPage_);
         }
      });

      // add next button
      nextButton_ = new ThemedButton("Next", new ClickHandler()
      {
         @Override
         public void onClick(ClickEvent arg0)
         {
            if (activePage_ instanceof WizardIntermediatePage<?,?>) 
            {
               final WizardIntermediatePage<I, T> page = 
                     (WizardIntermediatePage<I, T>) activePage_;
               
               // collect input from this page asynchronously and advance when
               // we have input
               page.collectIntermediateInput(getProgressIndicator(), 
                     new OperationWithInput<T>()
                     {
                        @Override
                        public void execute(final T input)
                        {
                           // prevent re-entrancy
                           if (validating_)
                              return;
                           
                           validating_ = true;
                           try
                           {
                              page.validateAsync(input, 
                                    new OperationWithInput<Boolean>()
                              {
                                 @Override
                                 public void execute(Boolean valid)
                                 {
                                    validating_ = false;
                                    if (valid)
                                    {
                                       intermediateResult_ = input;
                                       page.advance();
                                    }
                                 }
                              });
                           }
                           catch (Exception e)
                           {
                              validating_ = false;
                              Debug.logException(e);
                           }
                        }
                     });
            }
         }
      });
      nextButton_.setVisible(false);
      addActionButton(nextButton_);
   }
   
   @Override
   protected Widget createMainWidget()
   {
      WizardResources res = WizardResources.INSTANCE;
      WizardResources.Styles styles = res.styles();
      
      VerticalPanel mainWidget = new VerticalPanel();
      mainWidget.addStyleName(styles.mainWidget());
      
      headerPanel_ = new LayoutPanel();
      headerPanel_.addStyleName(styles.headerPanel());
      
      // layout constants
      final int kTopMargin = 5;
      final int kLeftMargin = 8;
      final int kCaptionWidth = 400;
      final int kCaptionHeight = 30;
      final int kPageUILeftMargin = 123;
      
      // first page caption
      subCaptionLabel_ = new Label(firstPage_.getPageCaption());
      subCaptionLabel_.addStyleName(styles.headerLabel());
      headerPanel_.add(subCaptionLabel_);
      headerPanel_.setWidgetLeftWidth(subCaptionLabel_,
                                      kTopMargin, Unit.PX, 
                                      kCaptionWidth, Unit.PX);
      headerPanel_.setWidgetTopHeight(subCaptionLabel_,
                                      kLeftMargin, Unit.PX,
                                      kCaptionHeight, Unit.PX);
      
      // second page back button
      ImageResource bkImg = res.wizardBackButton();
      backButton_ = new Label("Back");
      backButton_.addStyleName(styles.wizardBackButton());
      backButton_.addStyleName(ThemeResources.INSTANCE.themeStyles().handCursor());
      headerPanel_.add(backButton_);
      headerPanel_.setWidgetLeftWidth(backButton_,
                                      kTopMargin - 2, Unit.PX, 
                                      bkImg.getWidth(), Unit.PX);
      headerPanel_.setWidgetTopHeight(backButton_,
                                      kTopMargin - 2, Unit.PX,
                                      bkImg.getHeight(), Unit.PX);
      backButton_.setVisible(false);
      backButton_.addClickHandler(new ClickHandler()
      {
         public void onClick(ClickEvent event)
         {
            goBack();
         }
      });
      
      // second page caption label
      pageCaptionLabel_ = new Label();
      pageCaptionLabel_.addStyleName(styles.headerLabel());
      headerPanel_.add(pageCaptionLabel_);
      headerPanel_.setWidgetLeftWidth(pageCaptionLabel_,
                                      kPageUILeftMargin, Unit.PX, 
                                      kCaptionWidth, Unit.PX);
      headerPanel_.setWidgetTopHeight(pageCaptionLabel_,
                                      kLeftMargin, Unit.PX,
                                      kCaptionHeight, Unit.PX);
      pageCaptionLabel_.setVisible(false);
      
     
      mainWidget.add(headerPanel_);
      
      // main body panel for transitions
      bodyPanel_ = new LayoutPanel();
      ArrayList<String> wizardBodyStyles = getWizardBodyStyles();
      for (String styleName: wizardBodyStyles)
         bodyPanel_.addStyleName(styleName);
      bodyPanel_.getElement().getStyle().setProperty("overflowX", "hidden");
      mainWidget.add(bodyPanel_);
     
      // add first page (and all sub-pages recursively)
      addAndInitializePage(firstPage_, true);
      
      setNextButtonState(firstPage_);

      return mainWidget;
   }
   
   private void addAndInitializePage(WizardPage<I,T> page, boolean visible)
   {
      page.setSize("100%", "100%");
      
      bodyPanel_.add(page);
      bodyPanel_.setWidgetTopBottom(page, 0, Unit.PX, 0, Unit.PX);
      bodyPanel_.setWidgetLeftRight(page, 0, Unit.PX, 0, Unit.PX);
      bodyPanel_.setWidgetVisible(page, visible);
      
      page.initialize(initialData_);
      
      CommandWithArg<WizardPage<I,T>> showPageCmd = 
            new CommandWithArg<WizardPage<I,T>>() 
      {
         @Override
         public void execute(WizardPage<I, T> page)
         {
            showPage(page);
         };
      };

      if (page instanceof WizardNavigationPage<?,?>)
      {
         ((WizardNavigationPage<I,T>) page).setSelectionHandler(showPageCmd);
      }
      else if (page instanceof WizardIntermediatePage<?,?>) 
      {
         ((WizardIntermediatePage<I,T>) page).setNextHandler(showPageCmd);
      }

      // recursively initialize child pages
      ArrayList<WizardPage<I,T>> subPages = page.getSubPages();
      if (subPages != null)
      {
         for (int i = 0; i < subPages.size(); i++)
         {
            addAndInitializePage(subPages.get(i), false);
         }
      }
   }
   
   @Override
   protected T collectInput()
   {
      WizardPage<I,T> inputPage = activeInputPage();
      if (inputPage != null)
      {
         T input = ammendInput(inputPage.collectInput());
         return input;
      }
      else
         return null;
   }

   @Override
   protected void validateAsync(T input, 
         final OperationWithInput<Boolean> onValidated)
   {
      WizardPage<I,T> inputPage = activeInputPage();
      if (inputPage != null)
      {
         validating_ = true;
         try
         {
            inputPage.validateAsync(input, new OperationWithInput<Boolean>()
            {
               @Override
               public void execute(Boolean input)
               {
                  validating_ = false;
                  onValidated.execute(input);
               }
            });
         }
         catch (Exception e)
         {
            Debug.logException(e);
            validating_ = false;
         }
      }
      else
         onValidated.execute(false);
   }
   
   @Override
   public void showModal()
   {
      super.showModal();

      // set up state for the first page (some of this is ordinarily reached
      // via navigation)
      if (firstPage_ != null)
      {
         setOkButtonVisible(pageIsFinal(firstPage_));
         firstPage_.onActivate(getProgressIndicator());
      }
   }
   
   protected WizardPage<I,T> getFirstPage()
   {
      return firstPage_;
   }
   
   private WizardPage<I,T> activeInputPage()
   {
      if (activePage_ != null && 
          !(activePage_ instanceof WizardNavigationPage<?,?>))
      {
         return activePage_;
      }
      else
      {
         return null;
      }
   }
   
   private void animate(final Widget from, 
                        final Widget to, 
                        boolean rightToLeft,
                        final Command onCompleted) 
   {
      // protect against multiple calls
      if (isAnimating_)
         return;
      
       
      bodyPanel_.setWidgetVisible(to, true);

      int width = getOffsetWidth();

      bodyPanel_.setWidgetLeftWidth(from,
                                    0, Unit.PX,
                                    width, Unit.PX);
      bodyPanel_.setWidgetLeftWidth(to,
                                    rightToLeft ? width : -width, Unit.PX,
                                    width, Unit.PX);
      bodyPanel_.forceLayout();

      bodyPanel_.setWidgetLeftWidth(from,
                                    rightToLeft ? -width : width, Unit.PX,
                                    width, Unit.PX);
      bodyPanel_.setWidgetLeftWidth(to,
                                    0, Unit.PX,
                                    width, Unit.PX);
      
      isAnimating_ = true;
     
      bodyPanel_.animate(300, new AnimationCallback()
      {
         @Override
         public void onAnimationComplete()
         {
            bodyPanel_.setWidgetVisible(from, false);
          
            bodyPanel_.setWidgetLeftRight(to, 0, Unit.PX, 0, Unit.PX);
            bodyPanel_.forceLayout();
            
            isAnimating_ = false;
            
            onCompleted.execute(); 
         }
         @Override
         public void onLayout(Layer layer, double progress)
         {
         }
      });
   }
   
   private void showPage(final WizardPage<I,T> page)
   {
      // ask whether the page will accept the navigation
      if (!page.acceptNavigation())
         return;
      
      // give the page the currently accumulated result, if any
      page.setIntermediateResult(intermediateResult_);
            
      // determine behavior based on whether this is standard page or 
      // a navigation page
      final boolean okButtonVisible = pageIsFinal(page);
      activeParentNavigationPage_ = activePage_;
      
      animate(activePage_, page, true, new Command() {
         @Override
         public void execute()
         {
            // set active page
            activePage_ = page;
            
            // update header
            subCaptionLabel_.setVisible(false);
            backButton_.setVisible(true);
            pageCaptionLabel_.setText(page.getPageCaption());
            pageCaptionLabel_.setVisible(true);
            
            // make ok button visible
            setOkButtonVisible(okButtonVisible);
            
            // if this is an intermediate page, make Next visible
            setNextButtonState(page);
            
            // let wizard and page know that the new page is active
            onPageActivated(page, okButtonVisible);
            page.onActivate(getProgressIndicator());
            
            // set focus
            FocusHelper.setFocusDeferred(page);
         }
      });
   }
   
  
   private void goBack()
   {
      final boolean isNavigationPage = activeParentNavigationPage_ != null;

      // determine behavior based on whether we are going back to a
      // navigation page or a selector page
      final Widget toWidget = isNavigationPage ? 
         activeParentNavigationPage_ :
         firstPage_;
      
      final String pageCaptionLabel = isNavigationPage ? 
                        activeParentNavigationPage_.getPageCaption() : "";
      
      final WizardPage<I,T> newActivePage =
         isNavigationPage ? activeParentNavigationPage_ : firstPage_;
      
      final CanFocus focusWidget = (CanFocus)toWidget;
      
      activeParentNavigationPage_ = null;
      
      animate(activePage_, toWidget, false, new Command() {
         @Override
         public void execute()
         {
            // update active page
            activePage_ = newActivePage;
            
            // update header
            subCaptionLabel_.setVisible(newActivePage == firstPage_);
            pageCaptionLabel_.setVisible(
                  newActivePage != firstPage_ && isNavigationPage);
            pageCaptionLabel_.setText(pageCaptionLabel);
            
            setNextButtonState(newActivePage);
            backButton_.setVisible(
                  newActivePage != firstPage_);

            // make ok button invisible
            setOkButtonVisible(false);
            
            // call hook
            onSelectorActivated();
            
            // set focus
            focusWidget.focus();
         }
      });
   }
   
   protected void onPageActivated(WizardPage<I,T> page, boolean okButtonVisible)
   {
   }
    

   protected void onSelectorActivated()
   {
   }

   protected T ammendInput(T input)
   {
      return input;
   }
    
   protected ArrayList<String> getWizardBodyStyles()
   {
      ArrayList<String> classes = new ArrayList<String>();
      classes.add(WizardResources.INSTANCE.styles().wizardBodyPanel());
      return classes;
   }
   
   private void resetOkButtonCaption()
   {
      setOkButtonCaption(okCaption_);
   }
 
   private boolean pageIsFinal(WizardPage<I, T> page)
   {
      return page.getSubPages() == null ||
            page.getSubPages().size() == 0;
   }
   
   private void setNextButtonState(WizardPage<I, T> page)
   {
      boolean isIntermediate = page instanceof WizardIntermediatePage<?,?>;
      nextButton_.setVisible(isIntermediate);
      setDefaultOverrideButton(isIntermediate ? nextButton_ : null);
   }
   
   private void cleanupPage(WizardPage<I,T> page)
   {
      if (page == null)
         return;

      // notify child pages first (do cleanup in reverse order of construction)
      ArrayList<WizardPage<I,T>> subPages = page.getSubPages();
      if (subPages != null)
      {
         for (int i = 0; i < subPages.size(); i++)
         {
            cleanupPage(subPages.get(i));
         }
      }
      
      // clean this page
      page.onWizardClosing();
   }
   
   
   private final I initialData_; 
   private T intermediateResult_;
   
   private final String okCaption_;
   
   private LayoutPanel headerPanel_;
   private Label subCaptionLabel_;
   private Label backButton_;
   private Label pageCaptionLabel_;
   private ThemedButton nextButton_;
   
   private LayoutPanel bodyPanel_;
   private WizardPage<I,T> firstPage_ = null;
   private WizardPage<I,T> activePage_ = null;
   private WizardPage<I,T> activeParentNavigationPage_ = null;
   private boolean isAnimating_ = false;
   private boolean validating_ = false;
}
