/*
 * NewConnectionShinyHost.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.connections.ui;


import org.rstudio.core.client.Debug;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.dom.DomUtils;
import org.rstudio.core.client.widget.LayoutGrid;
import org.rstudio.core.client.widget.Operation;
import org.rstudio.core.client.widget.ProgressIndicator;
import org.rstudio.core.client.widget.RStudioFrame;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.Desktop;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.dependencies.DependencyManager;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.shiny.model.ShinyServerOperations;
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.server.remote.RResult;
import org.rstudio.studio.client.shiny.events.ShinyFrameNavigatedEvent;
import org.rstudio.studio.client.shiny.model.ShinyApplicationParams;
import org.rstudio.core.client.theme.ThemeColors;
import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
import org.rstudio.studio.client.workbench.views.connections.ConnectionsConstants;
import org.rstudio.studio.client.workbench.views.connections.events.NewConnectionDialogUpdatedEvent;
import org.rstudio.studio.client.workbench.views.connections.model.ConnectionOptions;
import org.rstudio.studio.client.workbench.views.connections.model.ConnectionsServerOperations;
import org.rstudio.studio.client.workbench.views.connections.model.NewConnectionInfo;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.inject.Inject;

public class NewConnectionShinyHost extends Composite
                                    implements ShinyFrameNavigatedEvent.Handler,
                                               NewConnectionDialogUpdatedEvent.Handler
{
   @Inject
   private void initialize(EventBus events,
                           GlobalDisplay globalDisplay,
                           ConnectionsServerOperations server,
                           ShinyServerOperations shinyServer,
                           DependencyManager dependencyManager,
                           UserPrefs userPrefs)
   {
      events_ = events;
      globalDisplay_ = globalDisplay;
      server_ = server;
      shinyServer_ = shinyServer;
      dependencyManager_ = dependencyManager;
      userPrefs_ = userPrefs;
   }

   public void onBeforeActivate(Operation operation, NewConnectionInfo info)
   {
      shinyFrameHandler_ = events_.addHandler(ShinyFrameNavigatedEvent.TYPE, this);
      dialogUpdatedHandler_ = events_.addHandler(NewConnectionDialogUpdatedEvent.TYPE, this);

      initialize(operation, info);
   }
          
   public void onActivate(ProgressIndicator indicator)
   {
   }

   private void terminateShinyApp(final Operation operation)
   {
      shinyServer_.stopShinyApp(ShinyApplicationParams.ID_FOREGROUND,
            new ServerRequestCallback<VoidResponse>()
      {
         public void onResponseReceived(VoidResponse v)
         {
            operation.execute();
         }
         
         @Override
         public void onError(ServerError error)
         {
         }
      });
   }

   public void onDeactivate(Operation operation)
   {
      if (shinyFrameHandler_ != null)
      {
         shinyFrameHandler_.removeHandler();
         shinyFrameHandler_ = null;
      }

      if (dialogUpdatedHandler_ != null)
      {
         dialogUpdatedHandler_.removeHandler();
         dialogUpdatedHandler_ = null;
      }

      terminateShinyApp(operation);
   }
   
   public NewConnectionShinyHost()
   {
      RStudioGinjector.INSTANCE.injectMembers(this);
      
      initWidget(createWidget());
   }

   private void showError(String errorMessage)
   {
      globalDisplay_.showErrorMessage(constants_.errorCaption(), errorMessage);
   }
   
   private void initialize(final Operation operation, final NewConnectionInfo info)
   {
      dependencyManager_.withShiny(constants_.connectionUserActionLabel() + info.getName(), new Command()
      {
         @Override
         public void execute()
         {
            // initialize miniUI
            server_.launchEmbeddedShinyConnectionUI(info.getPackage(), info.getName(), new ServerRequestCallback<RResult<VoidResponse>>()
            {
               @Override
               public void onResponseReceived(RResult<VoidResponse> response)
               {
                  if (response.failed()) {
                     showError(response.errorMessage());
                  }
                  else {
                     operation.execute();
                  }
               }

               @Override
               public void onError(ServerError error)
               {
                  Debug.logError(error);
               }
            });
         }
      });
   }
   
   private Widget createWidget()
   {
      VerticalPanel container = new VerticalPanel();
      
      // create iframe for miniUI
      frame_ = new RStudioFrame(constants_.shinyMiniUITitle());
      frame_.setSize("100%", "140px");

      container.add(frame_);
      
      // add the code panel
      codePanel_ = new ConnectionCodePanel();
      codePanel_.addStyleName(RES.styles().dialogCodePanel());
      
      final Command updateCodeCommand = new Command() {
         @Override
         public void execute()
         {
            codePanel_.setCode("", null);
         }
      };
      updateCodeCommand.execute();

      LayoutGrid codeGrid = new LayoutGrid(1, 1);
      codeGrid.addStyleName(RES.styles().codeGrid());
      codeGrid.setCellPadding(0);
      codeGrid.setCellSpacing(0);
      codeGrid.setWidget(0, 0, codePanel_);
      container.add(codeGrid);
     
      return container;
   }

   private void appendStyleOnLoad(final RStudioFrame frame)
   {
      appendStyle(frame);
      
      frame.addLoadHandler(new LoadHandler()
      {      
         @Override
         public void onLoad(LoadEvent arg0)
         {
            appendStyle(frame);
         }
      });
   }

   private boolean isDialogDarkTheme()
   {
      boolean useDark = userPrefs_ != null &&
         userPrefs_.useDarkThemeModalDialogs().getValue();
      Element container = Document.get().getElementById("rstudio_container");
      return useDark && container != null &&
         container.hasClassName("rstudio-themes-dark");
   }

   private String buildCustomCss()
   {
      String css =
         "body {\n" +
         "  background: none;\n" +
         "  font-family : \"Lucida Sans\", \"DejaVu Sans\", \"Lucida Grande\", \"Segoe UI\", Verdana, Helvetica, sans-serif;\n" +
         "  font-size : 12px;\n" +
         "  -ms-user-select : none;\n" +
         "  -moz-user-select : none;\n" +
         "  -webkit-user-select : none;\n" +
         "  user-select : none;\n" +
         "  margin: 0;\n" +
         "  margin-top: 7px;\n" +
         "}\n";

      if (isDialogDarkTheme())
      {
         String bg = ThemeColors.darkDialogControlBackground;
         String border = ThemeColors.darkGreyBorder;
         String bgDeep = ThemeColors.darkGreyMostInactiveBackground;

         css +=
            "body, label, .control-label { color: #eee !important; }\n" +
            "select, input[type=\"text\"], input[type=\"password\"], " +
            "input[type=\"number\"], textarea, .form-control {\n" +
            "  background-color: " + bg + " !important;\n" +
            "  border-color: " + border + " !important;\n" +
            "  color: #eee !important;\n" +
            "}\n" +
            "select option {\n" +
            "  background-color: " + bg + ";\n" +
            "  color: #eee;\n" +
            "}\n" +
            "select option:checked {\n" +
            "  background-color: " + bgDeep + ";\n" +
            "  color: #eee;\n" +
            "}\n";
      }

      return css;
   }

   private native void injectCssIntoIFrame(Element iframe, String css) /*-{
      try {
         var doc = iframe.contentDocument || iframe.contentWindow.document;
         var style = doc.createElement('style');
         style.textContent = css;
         (doc.head || doc.documentElement).appendChild(style);
      } catch (e) {
         // cross-origin iframe; styling is best-effort
         $wnd.console.log('injectCssIntoIFrame: ' + e);
      }
   }-*/;

   private void appendStyle(RStudioFrame frame)
   {
      String css = buildCustomCss();

      if (Desktop.hasDesktopFrame())
      {
         // In Desktop, the Shiny iframe is cross-origin (different port)
         // so we cannot access its document directly. Use the Electron
         // bridge to inject CSS via WebFrameMain.executeJavaScript().
         Desktop.getFrame().injectShinyDialogCss(css);
      }
      else
      {
         // In Server, the iframe is same-origin (proxied). Use native
         // JS to inject into the iframe's document context directly,
         // ensuring the style element is created in the correct document.
         injectCssIntoIFrame(frame.getIFrame(), css);
      }
   }

   public ConnectionOptions collectInput()
   {
      // collect the result
      ConnectionOptions result = ConnectionOptions.create(
         codePanel_.getCode(),
         codePanel_.getConnectVia());
      
      // return result
      return result;
   }

   @Override
   public void onShinyFrameNavigated(ShinyFrameNavigatedEvent event)
   {
      String url = event.getURL();
      
      if (Desktop.hasDesktopFrame())
         Desktop.getFrame().setShinyDialogUrl(StringUtil.notNull(url));

      frame_.setUrl(DomUtils.makeAbsoluteUrl(url));
      appendStyleOnLoad(frame_);
   }
   
   @Override
   public void onNewConnectionDialogUpdated(NewConnectionDialogUpdatedEvent event)
   {
      codePanel_.setCode(event.getCode(), "");
   }
   
   public interface Styles extends CssResource
   {
      String helpLink();
      String codeViewer();
      String codeGrid();
      String codePanelHeader();
      String dialogCodePanel();
      String infoPanel();
      String leftLabel();
   }

   public interface Resources extends ClientBundle
   {
      @Source("NewConnectionShinyHost.css")
      Styles styles();
   }
   
   public static Resources RES = GWT.create(Resources.class);
   public static void ensureStylesInjected() 
   {
      RES.styles().ensureInjected();
   }
   
   private ConnectionCodePanel codePanel_;
     
   private EventBus events_;
   private HandlerRegistration shinyFrameHandler_;
   private HandlerRegistration dialogUpdatedHandler_;
   private RStudioFrame frame_;
   private GlobalDisplay globalDisplay_;
   private ConnectionsServerOperations server_;
   private ShinyServerOperations shinyServer_;
   private DependencyManager dependencyManager_;
   private UserPrefs userPrefs_;
   private static final ConnectionsConstants constants_ = GWT.create(ConnectionsConstants.class);
}
