/*
 * ConsoleProgressDialog.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.workbench.views.vcs.common;

import com.google.gwt.aria.client.Roles;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style;
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.user.client.Window;
import com.google.gwt.user.client.ui.*;
import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.widget.*;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.AriaLiveService;
import org.rstudio.studio.client.application.events.AriaLiveStatusEvent.Severity;
import org.rstudio.studio.client.application.events.AriaLiveStatusEvent.Timing;
import org.rstudio.studio.client.common.SimpleRequestCallback;
import org.rstudio.studio.client.common.console.ConsoleOutputEvent;
import org.rstudio.studio.client.common.console.ConsolePromptEvent;
import org.rstudio.studio.client.common.console.ConsoleProcess;
import org.rstudio.studio.client.common.console.ConsoleProcessInfo;
import org.rstudio.studio.client.common.console.ProcessExitEvent;
import org.rstudio.studio.client.common.crypto.CryptoServerOperations;
import org.rstudio.studio.client.common.shell.ShellDisplay;
import org.rstudio.studio.client.common.shell.ShellInput;
import org.rstudio.studio.client.common.shell.ShellInteractionManager;
import org.rstudio.studio.client.common.shell.ShellOutputWriter;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.VoidResponse;
import org.rstudio.studio.client.server.VoidServerRequestCallback;
import org.rstudio.studio.client.workbench.views.vcs.ViewVcsConstants;

public class ConsoleProgressDialog extends ProgressDialog
                                   implements ConsoleOutputEvent.Handler, 
                                              ConsolePromptEvent.Handler,
                                              ProcessExitEvent.Handler,
                                              ClickHandler
{
   public ConsoleProgressDialog(ConsoleProcess consoleProcess,
                                CryptoServerOperations server)
   {
      this(consoleProcess.getProcessInfo().getCaption(), 
           consoleProcess, 
           "", 
           null, 
           server);
   }

   public ConsoleProgressDialog(String title, 
                                String output, 
                                int exitCode)
   {
      this(title, null, output, exitCode, null);
   }

   public ConsoleProgressDialog(String title,
                                ConsoleProcess consoleProcess,
                                String initialOutput,
                                Integer exitCode,
                                CryptoServerOperations server)
   {
      super(title, Roles.getDialogRole());
      
      if (consoleProcess == null && exitCode == null)
      {
         throw new IllegalArgumentException(
               "Invalid combination of arguments to ConsoleProgressDialog");
      }

      // save console process so that we can compute interaction mode below
      consoleProcess_ = consoleProcess;
      
      if (getInteractionMode() != ConsoleProcessInfo.INTERACTION_NEVER)
      {
         ShellInteractionManager shellInteractionManager = 
               new ShellInteractionManager(display_, inputHandler_);
         
         if (getInteractionMode() != ConsoleProcessInfo.INTERACTION_ALWAYS)
            shellInteractionManager.setHistoryEnabled(false);
         
         outputWriter_ = shellInteractionManager;
      }
      else
      {
         display_.setReadOnly(true);
         outputWriter_ = display_;
      }
      
      if (!StringUtil.isNullOrEmpty(initialOutput))
      {
         outputWriter_.consoleWriteOutput(initialOutput, false);
      }

      if (exitCode != null)
         setExitCode(exitCode);
     
      attachToProcess(consoleProcess);
   }
   
   @Override
   protected Widget createDisplayWidget(Object param)
   {
      display_ = new ConsoleProgressWidget();
      display_.setMaxOutputLines(getMaxOutputLines());
      display_.setSuppressPendingInput(true);
      return display_.getShellWidget();
   }
   
   public void attachToProcess(final ConsoleProcess consoleProcess)
   {
      consoleProcess_ = consoleProcess;
      
      // interaction-always mode is a shell -- customize ui accordingly
      if (getInteractionMode() == ConsoleProcessInfo.INTERACTION_ALWAYS)
      {
         hideProgress();
         stopButton().setText(constants_.closeCapitalized());
         
         int height = Window.getClientHeight() - 150;
         int width = Math.min(800, Window.getClientWidth() - 150);
         Style style = display_.getShellWidget().getElement().getStyle();
         style.setHeight(height, Unit.PX);
         style.setWidth(width, Unit.PX);
      }
      else
      {
         showProgress();
         stopButton().setText(constants_.stopCapitalized());
      }
      
      stopButton().addClickHandler(this);
      
      if (consoleProcess != null)
      {
         addHandlerRegistration(consoleProcess.addConsolePromptHandler(this));
         addHandlerRegistration(consoleProcess.addConsoleOutputHandler(this));
         addHandlerRegistration(consoleProcess.addProcessExitHandler(this));

         consoleProcess.start(new SimpleRequestCallback<VoidResponse>()
         {
            @Override
            public void onError(ServerError error)
            {
               // Show error and stop
               super.onError(error);
               
               // if this is showOnOutput_ then we will never get
               // a ProcessExitEvent or an onUnload so we should unsubscribe 
               // from events here
               unregisterHandlers();
               
               closeDialog();
            }
         });
      }

      addCloseHandler(new CloseHandler<PopupPanel>()
      {
         @Override
         public void onClose(CloseEvent<PopupPanel> popupPanelCloseEvent)
         {
            if (consoleProcess != null)
               consoleProcess.reap(new VoidServerRequestCallback());
         }
      });
   }

   public ConsoleProcess getConsoleProcess()
   {
      return consoleProcess_;
   }
   
   public void showOnOutput()
   {
      showOnOutput_ = true;
   }
   
   @Override 
   protected boolean handleEnterKey()
   {
      if (!running_)
      {
         stopButton().click();
         return true;
      }
      else
      {
         return false;
      }
   }

   @Override
   protected void announceCompletion(String message)
   {
      RStudioGinjector.INSTANCE.getAriaLiveService().announce(
            AriaLiveService.PROGRESS_COMPLETION, message, Timing.IMMEDIATE, Severity.STATUS);
   }

   public void writeOutput(String output)
   {
      maybeShowOnOutput(output);
      outputWriter_.consoleWriteOutput(output, false);
   }

   public void writePrompt(String prompt)
   {
      maybeShowOnOutput(prompt);
      outputWriter_.consoleWritePrompt(prompt, false);
   }

   @Override
   public void onConsoleOutput(ConsoleOutputEvent event)
   {
      writeOutput(event.getOutput());
   }
   
   @Override
   public void onConsolePrompt(ConsolePromptEvent event)
   {
      writePrompt(event.getPrompt());
   }

   @Override
   public void onProcessExit(ProcessExitEvent event)
   {    
      setExitCode(event.getExitCode());
      
      if (isShowing())
      {
         display_.setReadOnly(true);
      
         // when a shell exits we close the dialog
         if (getInteractionMode() == ConsoleProcessInfo.INTERACTION_ALWAYS)
            closeDialog();
         
         // when we were showOnOutput and the process succeeded then
         // we also auto-close
         else if (showOnOutput_ && (event.getExitCode() == 0))
            closeDialog();
      }
      
      // the dialog was showOnOutput_ but was never shown so just tear
      // down registrations and reap the process
      else if (showOnOutput_)
      {
         unregisterHandlers();
         
         if (consoleProcess_ != null)
            consoleProcess_.reap(new VoidServerRequestCallback());
      }
      
   }
   
   private void setExitCode(int exitCode)
   {
      running_ = false;
      stopButton().setText(constants_.closeCapitalized());
      stopButton().setDefault(true);
      hideProgress();
   }

   @Override
   public void onClick(ClickEvent event)
   {
      if (running_)
      {
         consoleProcess_.interrupt(new SimpleRequestCallback<VoidResponse>()
         {
            @Override
            public void onResponseReceived(VoidResponse response)
            {
               // Don't immediately close; wait for the process itself
               // to exit before closing since the interrupt might not
               // immediately stop the underlying process.
            }

            @Override
            public void onError(ServerError error)
            {
               super.onError(error);
            }
         });
      }
      else
      {
         closeDialog();
      }
   }
   
   private CommandWithArg<ShellInput> inputHandler_ = 
                                          new CommandWithArg<ShellInput>() 
   {
      @Override
      public void execute(ShellInput input)
      {         
         consoleProcess_.writeStandardInput(
            input, 
            new VoidServerRequestCallback() {
               @Override
               public void onError(ServerError error)
               {
                  outputWriter_.consoleWriteError(error.getUserMessage(), false);
               }
            });
      }

   };
   
  
   private int getInteractionMode()
   {
      if (consoleProcess_ != null)
         return consoleProcess_.getProcessInfo().getInteractionMode();
      else
         return ConsoleProcessInfo.INTERACTION_NEVER;
   }
   
   private int getMaxOutputLines()
   {
      if (consoleProcess_ != null)
         return consoleProcess_.getProcessInfo().getMaxOutputLines();
      else
         return 1000;
   }
   
   private void maybeShowOnOutput(String output)
   {    
      // NOTE: we have to trim the output because when the password
      // manager provides a password non-interactively the back-end
      // process sometimes echos a newline back to us
      if (!isShowing() && showOnOutput_ && (output.trim().length() > 0))
         showModal();
   }

   private boolean running_ = true;
   
   private boolean showOnOutput_ = false;
   
   private ConsoleProcess consoleProcess_;
 
   private final ShellOutputWriter outputWriter_;
  
   private ShellDisplay display_;
   private static final ViewVcsConstants constants_ = GWT.create(ViewVcsConstants.class);
}
