/*
 * Ignore.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.common.vcs.ignore;

import java.util.ArrayList;

import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.files.FileSystemItem;
import org.rstudio.core.client.widget.MessageDialog;
import org.rstudio.core.client.widget.ProgressIndicator;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.GlobalProgressDelayer;
import org.rstudio.studio.client.common.vcs.ProcessResult;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.workbench.model.Session;
import org.rstudio.studio.client.workbench.views.vcs.common.ConsoleProgressDialog;

import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.inject.Inject;
import com.google.inject.Provider;

public class Ignore
{
   public interface Strategy
   {
      public interface Filter
      {
         boolean includeFile(FileSystemItem file);
      }
      
      String getDialogCaption();
      
      String getIgnoresCaption();

      String getHelpLinkName();
      
      Filter getFilter();
      
      void getIgnores(String path, 
                      ServerRequestCallback<ProcessResult> requestCallback);

      void setIgnores(String path,
                      String ignores,
                      ServerRequestCallback<ProcessResult> requestCallback);
   }
   
   public interface Display
   {
      void setDialogCaption(String caption);
      void setIgnoresCaption(String caption);
      void setHelpLinkName(String helpLinkName);
      ProgressIndicator progressIndicator();
      HasClickHandlers saveButton();
      
      void setCurrentPath(String path);
      String getCurrentPath();
      HandlerRegistration addPathChangedHandler(
                                    ValueChangeHandler<String> handler);
      
      void setIgnored(String ignored);
      String getIgnored();
      
      void focusIgnored();
      
      void scrollToBottom();
      
      void showModal();
   }
   
   @Inject
   public Ignore(GlobalDisplay globalDisplay,
                 Session session,
                 Provider<Display> pDisplay)
   {
      globalDisplay_ = globalDisplay;
      session_ = session;
      pDisplay_ = pDisplay;
   }
   
   public void showDialog(ArrayList<String> paths, Strategy strategy)
   {
      IgnoreList ignoreList = createIgnoreList(paths, strategy.getFilter());
      if (ignoreList != null)
         showDialog(ignoreList, strategy);
   }
   
   public void showDialog(final IgnoreList ignoreList,
                          final Strategy strategy)
   {
      // show progress
      final ProgressIndicator globalIndicator = new GlobalProgressDelayer(
                       globalDisplay_, 
                       500,
                       "Getting ignored files for path...").getIndicator();
      
      // get existing ignores
      final String fullPath = projPathToFullPath(ignoreList.getPath());
      strategy.getIgnores(fullPath, 
                          new ServerRequestCallback<ProcessResult>() {
 
         @Override
         public void onResponseReceived(final ProcessResult result)
         {
            globalIndicator.onCompleted();
            
            if (checkForProcessError(strategy.getDialogCaption(), result))
               return;
                
            // show the ignore dialog
            String ignored = getIgnored(ignoreList, result.getOutput());
            showDialog(fullPath, ignored, strategy);
         }
         
         @Override
         public void onError(ServerError error)
         {
            globalIndicator.onError(error.getUserMessage());
         }
      });
   }
   
   private void showDialog(final String initialPath,
                           String ignores,
                           final Strategy strategy)
   {
      final Display display = pDisplay_.get();
      final ProgressIndicator indicator = display.progressIndicator();
      
      display.setDialogCaption(strategy.getDialogCaption());
      display.setIgnoresCaption(strategy.getIgnoresCaption());
      display.setHelpLinkName(strategy.getHelpLinkName());
      display.setCurrentPath(initialPath);
      display.setIgnored(ignores);
      display.scrollToBottom();
      
      display.addPathChangedHandler(new ValueChangeHandler<String>() {
         @Override
         public void onValueChange(ValueChangeEvent<String> event)
         {  
            display.setIgnored("");
            
            indicator.onProgress("Getting ignored files for path...");
            
            strategy.getIgnores(display.getCurrentPath(),
                  new ServerRequestCallback<ProcessResult>() {

               @Override
               public void onResponseReceived(final ProcessResult result)
               {
                  indicator.clearProgress();

                  if (checkForProcessError(strategy.getDialogCaption(), result))
                     return;

                  display.setIgnored(result.getOutput());
                  display.focusIgnored();
               }

               @Override
               public void onError(ServerError error)
               {
                  indicator.onError(error.getUserMessage());
               }});
            }
      });
      
      display.saveButton().addClickHandler(new ClickHandler() {

         @Override
         public void onClick(ClickEvent event)
         {
            indicator.onProgress("Setting ignored files for path...");
            
            strategy.setIgnores(
                  display.getCurrentPath(), 
                  display.getIgnored(), 
                  new ServerRequestCallback<ProcessResult>() {

               @Override
               public void onResponseReceived(ProcessResult result)
               {
                  indicator.onCompleted();
                  checkForProcessError(strategy.getDialogCaption(), result);
               }
                     
               @Override
               public void onError(ServerError error)
               {
                  indicator.onError(error.getUserMessage()); 
               }   
            }); 
            
         }   
      });
      
      display.showModal();
   }
   
   private String projPathToFullPath(String projPath)
   {
      FileSystemItem projDir = session_.getSessionInfo().getActiveProjectDir();
      return projPath.length() > 0 ? projDir.completePath(projPath) :
                                     projDir.getPath();
   }
   
   // compute the new list of ignores based on the initial/existing
   // set of paths and path(s) the user wants to add
   private IgnoreList createIgnoreList(ArrayList<String> paths,
                                       Ignore.Strategy.Filter filter)   
   {
      // special case for empty path list -- make the project root the 
      // parent path and return an empty list
      if (paths.size() == 0)
         return new IgnoreList("", new ArrayList<String>());
       
      // get the parent path of the first element
      FileSystemItem firstPath = FileSystemItem.createFile(paths.get(0));
      String parentPath = firstPath.getParentPathString();
      
      // confirm that all of the elements start with that path and take the
      // remainder of their paths for our list
      ArrayList<String> ignored = new ArrayList<String>();
      for (String path : paths)
      {
         String thisParent = 
                      FileSystemItem.createFile(path).getParentPathString();
         
         if (!parentPath.equals(thisParent))
         {
            GlobalDisplay gDisp = RStudioGinjector.INSTANCE.getGlobalDisplay();
            gDisp.showMessage(
                  MessageDialog.ERROR, 
                  "Error: Multiple Directories",
                  "The selected files are not all within the same directory " +
                  "(you can only ignore multiple files in one operation if " +
                  "they are located within the same directory).");
                
            return null;
         }
         
         // apply a filter if we have one
         if (filter != null)
         {
            FileSystemItem file = FileSystemItem.createFile(path);
            if (!filter.includeFile(file))
               continue;
         }
         
         // compute the parent relative directory
         if (parentPath.length() == 0)
            ignored.add(path);
         else
            ignored.add(path.substring(thisParent.length() + 1));
      }
            
      return new IgnoreList(parentPath, ignored);
   }
   
   private String getIgnored(IgnoreList ignoreList, String existingIgnored)
   {
      // split existing ignored into list
      ArrayList<String> ignored = new ArrayList<String>();
      Iterable<String> existing = StringUtil.getLineIterator(existingIgnored);
      for (String item : existing)
      {
         item = item.trim();
         if (item.length() > 0)
            ignored.add(item);
      }
 

      // for any element not already in the list add it
      for (String item : ignoreList.getFiles())
         if (!ignored.contains(item))
            ignored.add(item);
            
      // return as a string
      return StringUtil.join(ignored, "\n");
   }
   
   // check for an error and show the console progress dialog
   // if there was one
   private boolean checkForProcessError(String caption, ProcessResult result)
   {
      if (result.getExitCode() != 0)
      {
         new ConsoleProgressDialog(caption,
                                   result.getOutput(),
                                   result.getExitCode()).showModal();
         return true;
      }
      else
      {
         return false;
      }
   }
  
   private final GlobalDisplay globalDisplay_;
   private final Session session_;
   private final Provider<Display> pDisplay_;
   
}
