// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.debug.ui.launcher;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.chromium.debug.core.ChromiumDebugPlugin;
import org.chromium.debug.core.ChromiumSourceDirector;
import org.chromium.debug.core.SourceNameMapperContainer;
import org.chromium.debug.core.model.BreakpointSynchronizer.Direction;
import org.chromium.debug.core.model.LaunchParams;
import org.chromium.debug.core.model.LaunchParams.LookupMode;
import org.chromium.debug.core.model.LaunchParams.ValueConverter;
import org.chromium.debug.ui.launcher.LaunchTabGroup.Params;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.model.ISourceLocator;
import org.eclipse.debug.core.sourcelookup.IPersistableSourceLocator2;
import org.eclipse.debug.core.sourcelookup.ISourceContainer;
import org.eclipse.debug.core.sourcelookup.ISourceLookupDirector;
import org.eclipse.debug.internal.ui.SWTFactory;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.jface.preference.BooleanFieldEditor;
import org.eclipse.jface.preference.FieldEditor;
import org.eclipse.jface.preference.IntegerFieldEditor;
import org.eclipse.jface.preference.PreferenceStore;
import org.eclipse.jface.preference.StringFieldEditor;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Group;

/**
 * The "Remote" tab for the Chromium JavaScript launch tab group.
 * @param <ELEMENTS> type used for access to created dialog elements; it is used internally
 *     and allows to subclass {@link ChromiumRemoteTab} with additional dialog controls
 */
public abstract class ChromiumRemoteTab<ELEMENTS> extends TabBase<ELEMENTS, Params> {
  private static final String HOST_FIELD_NAME = "host_field"; //$NON-NLS-1$
  private static final String PORT_FIELD_NAME = "port_field"; //$NON-NLS-1$
  private static final String ADD_NETWORK_CONSOLE_FIELD_NAME =
      "add_network_console_field"; //$NON-NLS-1$

   // However, recommended range is [1024, 32767].
  private static final int minimumPortValue = 0;
  private static final int maximumPortValue = 65535;

  private final SourceContainerChecker sourceContainerChecker = new SourceContainerChecker();

  /**
   * Possibly checks host property in config.
   */
  public static abstract class HostChecker {
    public abstract String getWarning(ILaunchConfiguration config) throws CoreException;

    public static HostChecker LOCAL_ONLY = new HostChecker() {
      @Override
      public String getWarning(ILaunchConfiguration config) throws CoreException {
        String host = config.getAttribute(LaunchParams.CHROMIUM_DEBUG_HOST, ""); //$NON-NLS-1$
        if (!LOCAL_HOST_NAMES.contains(host.toLowerCase())) {
          return Messages.ChromiumRemoteTab_CONNECTION_FROM_LOCALHOST_WARNING;
        }
        return null;
      }
    };

    private static final Collection<String> LOCAL_HOST_NAMES =
        Arrays.asList("localhost", "127.0.0.1"); //$NON-NLS-1$ //$NON-NLS-2$
  }


  ChromiumRemoteTab(Params params) {
    super(params);
  }

  @Override
  protected ELEMENTS createElements(Composite parent, Runnable modifyListener) {
    Composite composite = createDefaultComposite(parent);
    setControl(composite);

    PreferenceStore store = new PreferenceStore();

    composite.setFont(parent.getFont());

    return createDialogElements(composite, modifyListener, store);
  }

  protected abstract ELEMENTS createDialogElements(Composite composite,
      Runnable modifyListener, PreferenceStore store);

  protected static TabElements createBasicTabElements(Composite composite,
      final Runnable modifyListener, PreferenceStore store, Params params) {
    final StringFieldEditor debugHost;
    final IntegerFieldEditor debugPort;
    final BooleanFieldEditor addNetworkConsole;
    {
      Group connectionGroup = new Group(composite, 0);
      connectionGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
      connectionGroup.setText(Messages.ChromiumRemoteTab_CONNECTION_GROUP);
      connectionGroup.setLayout(new GridLayout(1, false));


      IPropertyChangeListener propertyModifyListener = new IPropertyChangeListener() {
        public void propertyChange(PropertyChangeEvent event) {
          modifyListener.run();
        }
      };


      Composite propertiesComp = createInnerComposite(connectionGroup, 2);

      // Host text field
      debugHost = new StringFieldEditor(HOST_FIELD_NAME,
          Messages.ChromiumRemoteTab_HostLabel, propertiesComp);
      debugHost.setPropertyChangeListener(propertyModifyListener);
      debugHost.setPreferenceStore(store);

      // Port text field
      debugPort = new IntegerFieldEditor(PORT_FIELD_NAME,
          Messages.ChromiumRemoteTab_PortLabel, propertiesComp);
      debugPort.setPropertyChangeListener(propertyModifyListener);
      debugPort.setPreferenceStore(store);

      addNetworkConsole =
          new BooleanFieldEditor(ADD_NETWORK_CONSOLE_FIELD_NAME,
              Messages.ChromiumRemoteTab_ShowDebuggerNetworkCommunication, propertiesComp);
      addNetworkConsole.setPreferenceStore(store);
      addNetworkConsole.setPropertyChangeListener(propertyModifyListener);
    }

    return new TabElements() {
      @Override public StringFieldEditor getHost() {
        return debugHost;
      }
      @Override public IntegerFieldEditor getPort() {
        return debugPort;
      }
      @Override public BooleanFieldEditor getAddNetworkConsole() {
        return addNetworkConsole;
      }
    };
  }

  public String getName() {
    return Messages.ChromiumRemoteTab_RemoteTabName;
  }

  @Override
  protected MessageData isValidImpl(ILaunchConfiguration config) throws CoreException {
    int port = config.getAttribute(LaunchParams.CHROMIUM_DEBUG_PORT, -1);
    if (port < minimumPortValue || port > maximumPortValue) {
      return new MessageData(false, Messages.ChromiumRemoteTab_InvalidPortNumberError);
    }
    String message = getWarning(config);
    return new MessageData(true, message);
  }

  /**
   * Checks config for warnings and returns first found or null.
   */
  protected String getWarning(ILaunchConfiguration config) throws CoreException {
    HostChecker hostChecker = getParams().getHostChecker();
    if (hostChecker != null) {
      String hostWarning = hostChecker.getWarning(config);
      if (hostWarning != null) {
        return hostWarning;
      }
    }
    return sourceContainerChecker.check(config);
  }


  @Override
  public Image getImage() {
    return DebugUITools.getImage(IDebugUIConstants.IMG_LCL_DISCONNECT);
  }

  interface TabElements {
    StringFieldEditor getHost();
    IntegerFieldEditor getPort();
    BooleanFieldEditor getAddNetworkConsole();
  }

  static final TabFieldList<TabElements, Params> BASIC_TAB_FIELDS;
  static {
    List<TabField<?, ?, ? super TabElements, Params>> list =
        new ArrayList<ChromiumRemoteTab.TabField<?, ?, ? super TabElements, Params>>(4);

    list.add(new TabField<String, String, TabElements, Params>(
        LaunchParams.CHROMIUM_DEBUG_HOST, TypedMethods.STRING,
        new FieldEditorAccess<String, TabElements>(TypedMethods.STRING) {
          @Override
          FieldEditor getFieldEditor(TabElements tabElements) {
            return tabElements.getHost();
          }
          String getEditorErrorValue() {
            return ""; //$NON-NLS-1$
          }
        },
        new DefaultsProvider<String, Params>() {
          @Override String getFallbackValue() {
            return PluginVariablesUtil.getValue(PluginVariablesUtil.DEFAULT_HOST);
          }
          @Override String getInitialConfigValue(Params context) {
            return getFallbackValue();
          }
        },
        ValueConverter.<String>getTrivial()));

    list.add(new TabField<Integer, Integer, TabElements, Params>(
        LaunchParams.CHROMIUM_DEBUG_PORT, TypedMethods.INT,
        new FieldEditorAccess<Integer, TabElements>(TypedMethods.INT) {
          @Override
          FieldEditor getFieldEditor(TabElements tabElements) {
            return tabElements.getPort();
          }
          String getEditorErrorValue() {
            return "-1"; //$NON-NLS-1$
          }
        },
        new DefaultsProvider<Integer, Params>() {
          @Override Integer getFallbackValue() {
            return PluginVariablesUtil.getValueAsInt(PluginVariablesUtil.DEFAULT_PORT);
          }
          @Override Integer getInitialConfigValue(Params context) {
            return getFallbackValue();
          }
        },
        ValueConverter.<Integer>getTrivial()));

    list.add(new TabField<Boolean, Boolean, TabElements, Params>(
        LaunchParams.ADD_NETWORK_CONSOLE, TypedMethods.BOOL,
        new FieldEditorAccess<Boolean, TabElements>(TypedMethods.BOOL) {
          FieldEditor getFieldEditor(TabElements tabElements) {
            return tabElements.getAddNetworkConsole();
          }
          String getEditorErrorValue() {
            return ""; //$NON-NLS-1$
          }
        },
        new DefaultsProvider<Boolean, Params>() {
          @Override Boolean getFallbackValue() {
            return false;
          }
          @Override Boolean getInitialConfigValue(Params context) {
            return null;
          }
        },
        ValueConverter.<Boolean>getTrivial()));

    BASIC_TAB_FIELDS = createFieldListImpl(list);
  }

  private static class SourceContainerChecker {
    public String check(ILaunchConfiguration config) {
      LookupMode lookupMode;
      try {
        lookupMode = ChromiumSourceDirector.readLookupMode(config);
      } catch (CoreException e) {
        ChromiumDebugPlugin.log(e);
        return null;
      }
      if (lookupMode != LookupMode.AUTO_DETECT) {
        return null;
      }

      ISourceLookupDirector director;
      try {
        director = read(config);
      } catch (CoreException e) {
        return null;
      }
      if (director == null) {
        return null;
      }
      for (ISourceContainer sourceContainer : director.getSourceContainers()) {
        if (sourceContainer instanceof SourceNameMapperContainer) {
          return Messages.ChromiumRemoteTab_AUTO_DETECT_CONTAINER_WARNING;
        }
      }
      return null;
    }

    private ISourceLookupDirector read(ILaunchConfiguration config) throws CoreException {
      String memento = config.getAttribute(
          ILaunchConfiguration.ATTR_SOURCE_LOCATOR_MEMENTO, (String)null);
      if (memento == null) {
        return null;
      }
      String type = config.getAttribute(ILaunchConfiguration.ATTR_SOURCE_LOCATOR_ID, (String)null);
      if (type == null) {
        type = config.getType().getSourceLocatorId();
      }
      ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager();
      ISourceLocator locator = launchManager.newSourceLocator(type);
      if (locator instanceof IPersistableSourceLocator2 == false) {
        return null;
      }
      ISourceLookupDirector director = (ISourceLookupDirector) locator;
      director.initializeFromMemento(memento, config);
      return director;
    }
  }

  static abstract class DevToolsProtocolBase extends ChromiumRemoteTab<DevToolsElements> {
    DevToolsProtocolBase(Params params) {
      super(params);
    }

    @Override
    protected DevToolsElements createDialogElements(Composite composite,
        final Runnable modifyListener, PreferenceStore store) {
      final TabElements basicElements =
          createBasicTabElements(composite, modifyListener, store, getParams());

      RadioButtonsLogic.Listener radioButtonsListener =
          new RadioButtonsLogic.Listener() {
            public void selectionChanged() {
              modifyListener.run();
            }
          };

      final RadioButtonsLogic<Integer> breakpointRadioButtons;
      {
        Group breakpointGroup = new Group(composite, 0);
        breakpointGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        breakpointGroup.setText(Messages.ChromiumRemoteTab_BREAKPOINT_GROUP);
        breakpointGroup.setLayout(new GridLayout(1, false));

        Map<Integer, Button> buttonMap = new LinkedHashMap<Integer, Button>(3);
        for (LaunchParams.BreakpointOption option : LaunchParams.BREAKPOINT_OPTIONS) {
          Button button = new Button(breakpointGroup, SWT.RADIO);
          button.setFont(composite.getFont());
          button.setText(option.getLabel());
          GridData gd = new GridData();
          button.setLayoutData(gd);
          SWTFactory.setButtonDimensionHint(button);
          int index = buttonMap.size();
          buttonMap.put(index, button);
        }

        breakpointRadioButtons =
            new RadioButtonsLogic<Integer>(buttonMap, radioButtonsListener);
      }

      return new DevToolsElements() {
        @Override
        public RadioButtonsLogic<Integer> getBreakpointRadioButtons() {
          return breakpointRadioButtons;
        }

        @Override
        public TabElements getBase() {
          return basicElements;
        }
      };
    }

    @Override
    protected TabFieldList<? super DevToolsElements, ? super Params> getTabFields() {
      return TAB_FIELD_LIST;
    }

    private static final TabFieldList<? super DevToolsElements, ? super Params> TAB_FIELD_LIST;
    static {
      TabField<String, String, DevToolsElements, Params> breakpointSyncField =
          new TabField<String, String, DevToolsElements, Params>(
          LaunchParams.BREAKPOINT_SYNC_DIRECTION, TypedMethods.STRING,
              new FieldAccess<String, DevToolsElements>() {
            @Override
            void setValue(String value, DevToolsElements tabElements) {
              int breakpointOptionIndex = LaunchParams.findBreakpointOption(value);
              tabElements.getBreakpointRadioButtons().select(breakpointOptionIndex);
            }
            @Override
            String getValue(DevToolsElements tabElements) {
              int breakpointOption = tabElements.getBreakpointRadioButtons().getSelected();
              return LaunchParams.BREAKPOINT_OPTIONS.get(breakpointOption)
                  .getDirectionStringValue();
            }
          },
          new DefaultsProvider<String, Params>() {
            @Override String getFallbackValue() {
              return Direction.MERGE.toString();
            }
            @Override String getInitialConfigValue(Params context) {
              return null;
            }
          },
          ValueConverter.<String>getTrivial());

      List<TabFieldList<? super DevToolsElements, ? super Params>> subLists =
          new ArrayList<TabBase.TabFieldList<? super DevToolsElements,? super Params>>(2);
      subLists.add(createFieldListAdapting(BASIC_TAB_FIELDS,
          new Adapter<DevToolsElements, TabElements>() {
            @Override
            public TabElements get(DevToolsElements from) {
              return from.getBase();
            }
          }));
      subLists.add(createFieldListImpl(Collections.singletonList(breakpointSyncField)));

      TAB_FIELD_LIST = createCompositeFieldList(subLists);
    }
  }

  interface DevToolsElements {
    RadioButtonsLogic<Integer> getBreakpointRadioButtons();
    TabElements getBase();
  }

  static class DevToolsProtocol extends DevToolsProtocolBase {
    public DevToolsProtocol() {
      super(PARAMS);
    }

    private static final Params PARAMS = new Params(
        ChromiumRemoteTab.HostChecker.LOCAL_ONLY,
        Messages.ChromiumRemoteTab_URL, false);
  }

  static class Standalone extends DevToolsProtocolBase {
    public Standalone() {
      super(PARAMS);
    }

    private static final Params PARAMS = new Params(null,
        Messages.ChromiumRemoteTab_FILE_PATH, true);
  }
}
