/*
 * EditorCommandManager.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.core.client.command;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.rstudio.core.client.BrowseCap;
import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.dom.Clipboard;
import org.rstudio.core.client.events.EditorKeybindingsChangedEvent;
import org.rstudio.core.client.files.ConfigFileBacked;
import org.rstudio.core.client.files.FileSystemItem;
import org.rstudio.core.client.js.JsObject;
import org.rstudio.core.client.js.JsUtil;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.application.events.ResetEditorCommandsEvent;
import org.rstudio.studio.client.application.events.SetEditorCommandBindingsEvent;
import org.rstudio.studio.client.common.filetypes.events.CopySourcePathEvent;
import org.rstudio.studio.client.workbench.views.files.model.FilesServerOperations;
import org.rstudio.studio.client.workbench.views.source.editors.text.AceEditor;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceCommand;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceCommandManager;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.EditorLoadedEvent;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.user.client.Command;
import com.google.inject.Inject;
import com.google.inject.Singleton;

@Singleton
public class EditorCommandManager
{
   public static class EditorKeyBindings extends JsObject
   {
      public static final EditorKeyBindings create()
      {
         return JavaScriptObject.createObject().cast();
      }

      public final EditorKeyBinding get(String key)
      {
         return getObject(key).cast();
      }

      public final void setBindings(String key, List<KeySequence> ksList)
      {
         List<String> bindings = new ArrayList<>();
         for (KeySequence ks : ksList)
            bindings.add(ks.toString());

         setString(key, StringUtil.join(bindings, "|"));
      }

      protected EditorKeyBindings() {}
   }

   public static class EditorKeyBinding extends JavaScriptObject
   {
      protected EditorKeyBinding() {}

      public final List<KeySequence> getKeyBindings()
      {
         JsArrayString bindings = getBindingsInternal();
         Set<KeySequence> keys = new HashSet<>();
         for (String binding : JsUtil.asIterable(bindings))
         {
            String[] splat = binding.split("\\|");
            for (String item : splat)
            {
               keys.add(KeySequence.fromShortcutString(item));
            }
         }

         List<KeySequence> keyList = new ArrayList<>();
         keyList.addAll(keys);
         return keyList;
      }

      private final native JsArrayString getBindingsInternal()
      /*-{
         var result = this;
         if (typeof result === "string")
            result = [result];
         return result;
      }-*/;
   }

   public EditorCommandManager()
   {
      AceEditor.load(() -> finishInit());
   }

   private void finishInit()
   {
      RStudioGinjector.INSTANCE.injectMembers(this);

      manager_ = AceCommandManager.create();

      bindings_ = new ConfigFileBacked<>(
            files_,
            KEYBINDINGS_PATH,
            false,
            EditorKeyBindings.create());

      events_.addHandler(
            EditorLoadedEvent.TYPE,
            new EditorLoadedEvent.Handler()
            {
               @Override
               public void onEditorLoaded(EditorLoadedEvent event)
               {
                  loadBindings();
               }
            });

      events_.addHandler(
            EditorKeybindingsChangedEvent.TYPE,
            new EditorKeybindingsChangedEvent.Handler()
            {
               @Override
               public void onEditorKeybindingsChanged(EditorKeybindingsChangedEvent event)
               {
                  loadBindings(event.getBindings(), null);
               }
            });

      events_.addHandler(CopySourcePathEvent.TYPE, event ->
      {
         String path = event.getPath();
         if (BrowseCap.isWindowsDesktop())
         {
            // on Windows desktop resolve the "~" to a full path since Windows doesn't
            // natively understand "~"
            path = files_.resolveAliasedPath(FileSystemItem.createFile(path));
         }
         Clipboard.setText(path);
      });
   }

   @Inject
   private void initialize(EventBus events,
                           FilesServerOperations files)
   {
      events_ = events;
      files_ = files;
   }

   public static final native JsArray<AceCommand> getDefaultAceCommands() /*-{
      return $wnd.require("ace/commands/default_commands").commands;
   }-*/;

   public boolean hasBinding(KeySequence keys)
   {
      return manager_.hasBinding(keys);
   }

   public boolean hasPrefix(KeySequence keys)
   {
      return manager_.hasPrefix(keys);
   }

   public void rebindCommand(String id, List<KeySequence> keys)
   {
      manager_.rebindCommand(id, keys);
      events_.fireEvent(new SetEditorCommandBindingsEvent(id, keys));
   }

   public void addBindingsAndSave(final EditorKeyBindings newBindings,
                                  final CommandWithArg<EditorKeyBindings> onLoad)
   {
      bindings_.execute(new CommandWithArg<EditorKeyBindings>()
      {
         @Override
         public void execute(final EditorKeyBindings currentBindings)
         {
            currentBindings.insert(newBindings);
            bindings_.set(currentBindings, new Command()
            {
               @Override
               public void execute()
               {
                  loadBindings(onLoad);
               }
            });
         }
      });
   }

   public void loadBindings()
   {
      loadBindings(null);
   }

   public void loadBindings(final CommandWithArg<EditorKeyBindings> afterLoad)
   {
      bindings_.execute(new CommandWithArg<EditorKeyBindings>()
      {
         @Override
         public void execute(EditorKeyBindings bindings)
         {
            loadBindings(bindings, afterLoad);
         }
      });
   }

   private void loadBindings(final EditorKeyBindings bindings,
                             final CommandWithArg<EditorKeyBindings> afterLoad)
   {
      for (String commandName : bindings.iterableKeys())
      {
         EditorKeyBinding binding = bindings.get(commandName);

         rebindCommand(
               commandName,
               binding.getKeyBindings());

      }

      if (afterLoad != null)
         afterLoad.execute(bindings);
   }

   public void resetBindings()
   {
      resetBindings(null);
   }

   public void resetBindings(final Command afterReset)
   {
      bindings_.set(EditorKeyBindings.create(), new Command()
      {
         @Override
         public void execute()
         {
            manager_ = AceCommandManager.create();
            events_.fireEvent(new ResetEditorCommandsEvent());

            if (afterReset != null)
               afterReset.execute();
         }
      });
   }

   public JsArray<AceCommand> getCommands()
   {
      return manager_.getRelevantCommands();
   }

   private ConfigFileBacked<EditorKeyBindings> bindings_;
   private AceCommandManager manager_;

   private boolean isBindingsLoaded_ = false;
   public static final String KEYBINDINGS_PATH =
         "keybindings/editor_bindings.json";

   // Injected ----
   private EventBus events_;
   private FilesServerOperations files_;
}
