/*
 * Ignore.java
 *
 * Copyright (C) 2022 by Posit Software, PBC
 *
 * Unless you have received this program directly from Posit Software pursuant
 * to the terms of a commercial license agreement with Posit Software, 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 com.google.gwt.core.client.GWT;
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.StudioClientCommonConstants;
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,
                       constants_.gettingIgnoredFilesProgressMessage()).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(constants_.gettingIgnoredFilesProgressMessage());

            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(constants_.settingIgnoredFilesProgressMessage());

            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<>());

      // 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<>();
      for (String path : paths)
      {
         String thisParent =
                      FileSystemItem.createFile(path).getParentPathString();

         if (parentPath != thisParent)
         {
            GlobalDisplay gDisp = RStudioGinjector.INSTANCE.getGlobalDisplay();
            gDisp.showMessage(
                  MessageDialog.ERROR,
                  constants_.multipleDirectoriesCaption(),
                  constants_.selectedFilesNotInSameDirectoryMessage());

            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(StringUtil.substring(path, 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<>();
      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_;
   private static final StudioClientCommonConstants constants_ = GWT.create(StudioClientCommonConstants.class);

}
