/*
 * ProfilerEditingTarget.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.source.editors.profiler;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.Widget;
import com.google.inject.Inject;
import com.google.inject.Provider;

import org.rstudio.core.client.CodeNavigationTarget;
import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.Debug;
import org.rstudio.core.client.FilePosition;
import org.rstudio.core.client.StringUtil;
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.EnsureHeightEvent;
import org.rstudio.core.client.events.EnsureVisibleEvent;
import org.rstudio.core.client.events.HasSelectionCommitHandlers;
import org.rstudio.core.client.events.SelectionCommitEvent;
import org.rstudio.core.client.files.FileSystemContext;
import org.rstudio.core.client.files.FileSystemItem;
import org.rstudio.core.client.widget.Operation;
import org.rstudio.core.client.widget.OperationWithInput;
import org.rstudio.core.client.widget.ProgressIndicator;
import org.rstudio.core.client.widget.ProgressOperationWithInput;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.FileDialogs;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.ReadOnlyValue;
import org.rstudio.studio.client.common.SimpleRequestCallback;
import org.rstudio.studio.client.common.Value;
import org.rstudio.studio.client.common.filetypes.FileIcon;
import org.rstudio.studio.client.common.filetypes.FileType;
import org.rstudio.studio.client.common.filetypes.FileTypeRegistry;
import org.rstudio.studio.client.common.filetypes.ProfilerType;
import org.rstudio.studio.client.common.filetypes.TextFileType;
import org.rstudio.studio.client.palette.model.CommandPaletteEntryProvider;
import org.rstudio.studio.client.rsconnect.model.PublishHtmlSource;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.VoidResponse;
import org.rstudio.studio.client.workbench.WorkbenchContext;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.model.RemoteFileSystemContext;
import org.rstudio.studio.client.workbench.views.source.SourceColumn;
import org.rstudio.studio.client.workbench.views.source.SourceWindowManager;
import org.rstudio.studio.client.workbench.views.source.ViewsSourceConstants;
import org.rstudio.studio.client.workbench.views.source.editors.EditingTarget;
import org.rstudio.studio.client.workbench.views.source.editors.EditingTargetSource.EditingTargetNameProvider;
import org.rstudio.studio.client.workbench.views.source.editors.profiler.model.ProfileOperationResponse;
import org.rstudio.studio.client.workbench.views.source.editors.profiler.model.ProfilerContents;
import org.rstudio.studio.client.workbench.views.source.editors.profiler.model.ProfilerServerOperations;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
import org.rstudio.studio.client.workbench.views.source.events.CollabEditStartParams;
import org.rstudio.studio.client.workbench.views.source.events.DocWindowChangedEvent;
import org.rstudio.studio.client.workbench.views.source.events.PopoutDocEvent;
import org.rstudio.studio.client.workbench.views.source.events.SourceNavigationEvent;
import org.rstudio.studio.client.workbench.views.source.model.DocTabDragParams;
import org.rstudio.studio.client.workbench.views.source.model.SourceDocument;
import org.rstudio.studio.client.workbench.views.source.model.SourceNavigation;
import org.rstudio.studio.client.workbench.views.source.model.SourcePosition;
import org.rstudio.studio.client.workbench.views.source.model.SourceServerOperations;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;

public class ProfilerEditingTarget implements EditingTarget,
                                              HasSelectionCommitHandlers<CodeNavigationTarget>
{
   interface MyCommandBinder
   extends CommandBinder<Commands, ProfilerEditingTarget>
   {
   }

   private static final MyCommandBinder commandBinder =
         GWT.create(MyCommandBinder.class);

   @Inject
   public ProfilerEditingTarget(ProfilerPresenter presenter,
                                Commands commands,
                                EventBus events,
                                ProfilerServerOperations server,
                                GlobalDisplay globalDisplay,
                                Provider<SourceWindowManager> pSourceWindowManager,
                                FileDialogs fileDialogs,
                                RemoteFileSystemContext fileContext,
                                WorkbenchContext workbenchContext,
                                EventBus eventBus,
                                SourceServerOperations sourceServer,
                                FileTypeRegistry fileTypeRegistry)
   {
      presenter_ = presenter;
      commands_ = commands;
      events_ = events;
      server_ = server;
      globalDisplay_ = globalDisplay;
      pSourceWindowManager_ = pSourceWindowManager;
      fileDialogs_ = fileDialogs;
      fileContext_ = fileContext;
      workbenchContext_ = workbenchContext;
      eventBus_ = eventBus;
      sourceServer_ = sourceServer;
      fileTypeRegistry_ = fileTypeRegistry;

      if (!initializedEvents_)
      {
         initializedEvents_ = true;
         initializeEvents();
      }
   }

   public String getId()
   {
      return doc_.getId();
   }

   @Override
   public void adaptToExtendedFileType(String extendedType)
   {
   }

   @Override
   public String getExtendedFileType()
   {
      return null;
   }
   
   @Override 
   public boolean isShinyPrerenderedDoc()
   {
      return false;
   }

   public HasValue<String> getName()
   {
      return name_;
   }

   public String getTitle()
   {
      return name_.getValue();
   }

   public String getContext()
   {
      return null;
   }

   public FileIcon getIcon()
   {
      return FileIcon.PROFILER_ICON;
   }

   @Override
   public FileType getFileType()
   {
      return fileType_;
   }

   @Override
   public TextFileType getTextFileType()
   {
      return null;
   }

   public String getTabTooltip()
   {
      return constants_.rProfiler();
   }

   public HashSet<AppCommand> getSupportedCommands()
   {
      HashSet<AppCommand> commands = fileType_.getSupportedCommands(commands_);
      if (SourceWindowManager.isMainSourceWindow())
         commands.add(commands_.popoutDoc());
      else
         commands.add(commands_.returnDocToMain());
      commands.add(commands_.printSourceDoc());
      return commands;
   }

   @Override
   public void manageCommands()
   {
   }

   @Override
   public boolean canCompilePdf()
   {
      return false;
   }

   @Override
   public void verifyCppPrerequisites()
   {
   }

   @Override
   public void verifyPythonPrerequisites()
   {
   }

   @Override
   public void verifyD3Prerequisites()
   {
   }

   @Override
   public void verifyNewSqlPrerequisites()
   {
   }

   @Override
   public Position search(String regex)
   {
      return null;
   }

   @Override
   public Position search(Position startPos, String regex)
   {
      return null;
   }

   @Override
   public void forceLineHighlighting()
   {
   }

   @Override
   public void setSourceOnSave(boolean sourceOnSave)
   {
   }

   public void focus()
   {
      ArrayList<Element> focusableElements = DomUtils.getFocusableElements(asWidget().getElement());
      if (!focusableElements.isEmpty())
         focusableElements.get(0).focus();
   }

   public void onActivate()
   {
      activeProfilerEditingTarger_ = this;

      Scheduler.get().scheduleDeferred(new ScheduledCommand()
      {
         public void execute()
         {
            commands_.gotoProfileSource().setEnabled(hasValidPath_);
         }
      });

      final Operation activateOperation = new Operation()
      {

         @Override
         public void execute()
         {
            if (!htmlPathInitialized_) {
               htmlPathInitialized_ = true;

               htmlPath_ = getContents().getHtmlPath();
               htmlLocalPath_ = getContents().getHtmlLocalPath();
               isUserSaved_ = getContents().isUserSaved();

               if (htmlPath_ == null)
               {
                  presenter_.buildHtmlPath(new OperationWithInput<ProfileOperationResponse>()
                  {
                     @Override
                     public void execute(ProfileOperationResponse response)
                     {
                        htmlPath_ = response.getHtmlPath();
                        htmlLocalPath_ = response.getHtmlLocalPath();

                        persistDocumentProperty("htmlPath", htmlPath_);
                        persistDocumentProperty("htmlLocalPath", htmlLocalPath_);

                        view_.showProfilePage(htmlPath_);

                        pSourceWindowManager_.get().maximizeSourcePaneIfNecessary();
                     }

                  }, new Operation()
                  {
                     @Override
                     public void execute()
                     {
                        server_.clearProfile(getPath(), new ServerRequestCallback<JavaScriptObject>()
                        {
                           @Override
                           public void onResponseReceived(JavaScriptObject response)
                           {
                              commands_.closeSourceDoc().execute();
                           }

                           @Override
                           public void onError(ServerError error)
                           {
                              Debug.logError(error);
                              commands_.closeSourceDoc().execute();
                           }
                        });
                     }
                  }, getPath());
               }
               else
               {
                  view_.showProfilePage(htmlPath_);

                  Scheduler.get().scheduleDeferred(new ScheduledCommand()
                  {
                     public void execute()
                     {
                        pSourceWindowManager_.get().maximizeSourcePaneIfNecessary();
                     }
                  });
               }
            }
         }
      };

      if (getId() != null && !SourceWindowManager.isMainSourceWindow()) {
         sourceServer_.getSourceDocument(getId(), new ServerRequestCallback<SourceDocument>()
         {
            @Override
            public void onResponseReceived(SourceDocument document)
            {
               doc_ = document;
               activateOperation.execute();
            }

            @Override
            public void onError(ServerError error)
            {
               Debug.logError(error);
            }
         });
      }
      else {
         activateOperation.execute();
      }

      // If we're already hooked up for some reason, unhook.
      // This shouldn't happen though.
      if (commandHandlerReg_ != null)
      {
         Debug.log("Warning: onActivate called twice without intervening onDeactivate");
         commandHandlerReg_.removeHandler();
         commandHandlerReg_ = null;
      }
      commandHandlerReg_ = commandBinder.bind(commands_, this);
   }

   public void onDeactivate()
   {
      if (activeProfilerEditingTarger_ == this)
      {
         activeProfilerEditingTarger_ = null;
      }

      recordCurrentNavigationPosition();

      commandHandlerReg_.removeHandler();
      commandHandlerReg_ = null;
   }

   @Override
   public void onInitiallyLoaded()
   {
   }

   @Override
   public void recordCurrentNavigationPosition()
   {
      events_.fireEvent(new SourceNavigationEvent(
            SourceNavigation.create(
            getId(),
            getPath(),
            SourcePosition.create(0, 0))));
   }

   @Override
   public void navigateToPosition(SourcePosition position,
                                  boolean recordCurrent)
   {
   }

   @Override
   public void navigateToPosition(SourcePosition position,
                                  boolean recordCurrent,
                                  boolean highlightLine)
   {
   }
   
   @Override
   public void navigateToPosition(SourcePosition position,
                                  boolean recordCurrent,
                                  boolean highlightLine,
                                  boolean moveCursor,
                                  Command onNavigationCompleted)
   {
   }

   @Override
   public void restorePosition(SourcePosition position)
   {
   }

   @Override
   public SourcePosition currentPosition()
   {
      return null;
   }

   @Override
   public void setCursorPosition(Position position)
   {
   }

   @Override
   public void ensureCursorVisible()
   {
   }

   @Override
   public boolean isAtSourceRow(SourcePosition position)
   {
      // always true because profiler docs don't have the
      // concept of a position
      return true;
   }

   @Override
   public void highlightDebugLocation(SourcePosition startPos,
                                      SourcePosition endPos,
                                      boolean executing)
   {
   }

   @Override
   public void endDebugHighlighting()
   {
   }

   @Override
   public void beginCollabSession(CollabEditStartParams params)
   {
   }

   @Override
   public void endCollabSession()
   {
   }

   public boolean onBeforeDismiss()
   {
      return true;
   }

   public ReadOnlyValue<Boolean> dirtyState()
   {
      return neverDirtyState_;
   }

   @Override
   public boolean isSaveCommandActive()
   {
      return !isUserSaved_;
   }

   @Override
   public void forceSaveCommandActive()
   {
   }

   public void save(Command onCompleted)
   {
      onCompleted.execute();
   }

   public void saveWithPrompt(Command onCompleted, Command onCancelled)
   {
      onCompleted.execute();
   }

   public void revertChanges(Command onCompleted)
   {
      onCompleted.execute();
   }

   @Handler
   void onPrintSourceDoc()
   {
      Scheduler.get().scheduleDeferred(new ScheduledCommand()
      {
         public void execute()
         {
            view_.print();
         }
      });
   }

   private String getAndSetInitialName()
   {
      String name = getContents().getName();
      boolean createProfile = getContents().getCreateProfile();

      if (!StringUtil.isNullOrEmpty(name)) {
         return name;
      }
      else if (createProfile)
      {
         String defaultName = defaultNameProvider_.defaultNamePrefix(this);
         persistDocumentProperty("name", defaultName);
         return defaultName;
      }
      else
      {
         String nameFromFile = FileSystemItem.getNameFromPath(getPath());
         persistDocumentProperty("name", nameFromFile);
         return nameFromFile;
      }
   }

   public void initialize(SourceColumn column,
                          SourceDocument document,
                          FileSystemContext fileContext,
                          FileType type,
                          EditingTargetNameProvider defaultNameProvider)
   {
      // initialize doc, view, and presenter
      column_ = column;
      doc_ = document;

      PublishHtmlSource publishHtmlSource = new PublishHtmlSource() {

         @Override
         public void generatePublishHtml(CommandWithArg<String> onComplete)
         {
            onComplete.execute(htmlLocalPath_);
         }

         @Override
         public String getTitle()
         {
            return constants_.profileCapitalized();
         }
      };

      view_ = new ProfilerEditingTargetWidget(constants_.profilerCapitalized(), commands_, publishHtmlSource, column_);
      defaultNameProvider_ = defaultNameProvider;

      getName().setValue(getAndSetInitialName());

      presenter_.attach(doc_, view_);
   }

   public void onDismiss(int dismissType)
   {
      presenter_.detach();
   }

   public long getFileSizeLimit()
   {
      return Long.MAX_VALUE;
   }

   public long getLargeFileSize()
   {
      return Long.MAX_VALUE;
   }

   public Widget asWidget()
   {
      return view_.asWidget();
   }

   public HandlerRegistration addEnsureVisibleHandler(EnsureVisibleEvent.Handler handler)
   {
      return new HandlerRegistration()
      {
         public void removeHandler()
         {
         }
      };
   }

   public HandlerRegistration addEnsureHeightHandler(EnsureHeightEvent.Handler handler)
   {
      return new HandlerRegistration()
      {
         public void removeHandler()
         {
         }
      };
   }

   @Override
   public HandlerRegistration addCloseHandler(CloseHandler<java.lang.Void> handler)
   {
      return new HandlerRegistration()
      {
         public void removeHandler()
         {
         }
      };
   }

   public void fireEvent(GwtEvent<?> event)
   {
      assert false : "Not implemented";
   }

   public String getPath()
   {
      return getContents().getPath();
   }

   @Override
   public HandlerRegistration addSelectionCommitHandler(SelectionCommitEvent.Handler<CodeNavigationTarget> handler)
   {
      return null;
   }

   @Handler
   void onSaveSourceDoc()
   {
      saveNewFile(null);
   }

   @Handler
   void onSaveSourceDocAs()
   {
      saveNewFile(isUserSaved_ ? getPath() : null);
   }

   @Handler
   public void onGotoProfileSource()
   {
      FilePosition filePosition = FilePosition.create(selectedLine_, 0);
      fileTypeRegistry_.editFile(FileSystemItem.createFile(selectedPath_),
                                 filePosition);
   }

   @Handler
   public void onOpenProfileInBrowser()
   {
      globalDisplay_.showHtmlFile(htmlLocalPath_);
   }

   public String getDefaultNamePrefix()
   {
      return constants_.profileCapitalized();
   }

   @Override
   public String getCurrentStatus()
   {
      return constants_.codeProfileResultsDisplayed();
   }

   private void savePropertiesWithPath(String path)
   {
      String name = FileSystemItem.getNameFromPath(path);
      persistDocumentProperty("name", name);
      persistDocumentProperty("path", path);

      getName().setValue(name, true);
      name_.fireChangeEvent();
   }

   private ProfilerContents getContents()
   {
      return doc_.getProperties().cast();
   }

   private void persistDocumentProperty(String property, String value)
   {
      HashMap<String, String> props = new HashMap<>();
      props.put(property, value);

      sourceServer_.modifyDocumentProperties(
         doc_.getId(),
         props,
         new SimpleRequestCallback<VoidResponse>(constants_.errorCapitalized())
         {
            @Override
            public void onResponseReceived(VoidResponse response)
            {
            }

            @Override
            public void onError(ServerError error)
            {
               Debug.logError(error);
               globalDisplay_.showErrorMessage(constants_.failedToSaveProfileProperties(),
                     error.getMessage());
            }
      });
   }

   private void saveNewFile(final String suggestedPath)
   {
      FileSystemItem fsi;
      if (suggestedPath != null)
         fsi = FileSystemItem.createFile(suggestedPath).getParentPath();
      else
         fsi = workbenchContext_.getDefaultFileDialogDir();

      fileDialogs_.saveFile(
            constants_.saveFileName(getName().getValue()),
            fileContext_,
            fsi,
            fileType_.getDefaultExtension(),
            false,
            new ProgressOperationWithInput<FileSystemItem>()
            {
               public void execute(final FileSystemItem saveItem,
                                   final ProgressIndicator indicator)
               {
                  if (saveItem == null)
                     return;

                  workbenchContext_.setDefaultFileDialogDir(
                        saveItem.getParentPath());

                  final String toPath = saveItem.getPath();
                  server_.copyProfile(
                     htmlLocalPath_,
                     toPath,
                     new ServerRequestCallback<JavaScriptObject>() {
                        @Override
                        public void onResponseReceived(JavaScriptObject response)
                        {
                           savePropertiesWithPath(saveItem.getPath());

                           persistDocumentProperty("isUserSaved", "saved");
                           isUserSaved_ = true;

                           indicator.onCompleted();
                        }

                        @Override
                        public void onError(ServerError error)
                        {
                           Debug.logError(error);
                           indicator.onCompleted();
                           globalDisplay_.showErrorMessage(constants_.failedToSaveProfile(),
                                 error.getMessage());
                        }
                  });
               }
            });
   }

   private Command postSaveProfileCommand()
   {
      return new Command()
      {
         public void execute()
         {
         }
      };
   }

   @Override
   public CommandPaletteEntryProvider getPaletteEntryProvider()
   {
      return null;
   }

   private void onMessage(final String message,
                          final String file,
                          final String normPath,
                          final String details,
                          final int line)
   {
      if (message == "sourcefile")
      {
         server_.profileSources(file, normPath, new ServerRequestCallback<String>()
         {
            @Override
            public void onResponseReceived(String response)
            {
               selectedLine_ = line;
               hasValidPath_ = !StringUtil.isNullOrEmpty(response);
               selectedPath_ = hasValidPath_ ? response : file;

               commands_.gotoProfileSource().setEnabled(hasValidPath_);

               if (details == "open")
               {
                  if (hasValidPath_)
                  {
                     FilePosition filePosition = FilePosition.create(line, 0);
                     CodeNavigationTarget navigationTarget = new CodeNavigationTarget(response, filePosition);

                     fileTypeRegistry_.editFile(
                           FileSystemItem.createFile(navigationTarget.getFile()),
                           filePosition);
                  }
                  else if (selectedPath_.indexOf("<expr>") == -1)
                  {
                     globalDisplay_.showMessage(GlobalDisplay.MSG_ERROR,
                           constants_.errorOpeningProfilerSource(),
                           constants_.sourceFileAtPathDoesNotExist(selectedPath_));
                  }
               }
            }

            @Override
            public void onError(ServerError error)
            {
               Debug.logError(error);
            }
         });
      }
   }

   public static void onGlobalMessage(final String message,
                                final String file,
                                final String normPath,
                                final String details,
                                final int line)
   {
      if (activeProfilerEditingTarger_ != null)
      {

         activeProfilerEditingTarger_.onMessage(message, file, normPath, details, line);
      }
   }

   @Handler
   void onPopoutDoc()
   {
      events_.fireEvent(new PopoutDocEvent(getId(), currentPosition(), null));
   }

   @Handler
   void onReturnDocToMain()
   {
      events_.fireEventToMainWindow(new DocWindowChangedEvent(
            getId(), SourceWindowManager.getSourceWindowId(), "",
            DocTabDragParams.create(getId(), currentPosition(), null),
            null, 0, -1));
   }

   private native static void initializeEvents() /*-{
      var handler = $entry(function(e) {
         if (typeof e.data != 'object')
            return;
         if (e.origin.substr(0, e.origin.length) != $wnd.location.origin)
            return;
         if (e.data.source != "profvis")
            return;

         @org.rstudio.studio.client.workbench.views.source.editors.profiler.ProfilerEditingTarget::onGlobalMessage(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)(
            e.data.message,
            e.data.file,
            e.data.normpath,
            e.data.details,
            e.data.line
         );
      });
      $wnd.addEventListener("message", handler, true);
   }-*/;

   private SourceDocument doc_;
   private SourceColumn column_;
   private ProfilerEditingTargetWidget view_;
   private final ProfilerPresenter presenter_;

   private final Value<Boolean> neverDirtyState_ = new Value<>(false);

   private final EventBus events_;
   private final Commands commands_;
   private final ProfilerServerOperations server_;
   private final SourceServerOperations sourceServer_;
   private final GlobalDisplay globalDisplay_;
   private Provider<SourceWindowManager> pSourceWindowManager_;
   private final FileDialogs fileDialogs_;
   private final RemoteFileSystemContext fileContext_;
   private final WorkbenchContext workbenchContext_;
   private final EventBus eventBus_;
   private EditingTargetNameProvider defaultNameProvider_;
   private final FileTypeRegistry fileTypeRegistry_;

   private ProfilerType fileType_ = new ProfilerType();

   private HandlerRegistration commandHandlerReg_;

   private boolean htmlPathInitialized_;

   private static boolean initializedEvents_;

   private Value<String> name_ = new Value<>(null);
   private String tempName_;

   private String htmlPath_;
   private String htmlLocalPath_;
   private boolean isUserSaved_;

   private static ProfilerEditingTarget activeProfilerEditingTarger_;
   private String selectedPath_;
   private int selectedLine_;
   private Boolean hasValidPath_ = false;
   private static final ViewsSourceConstants constants_ = GWT.create(ViewsSourceConstants.class);
}
