// Copyright (c) 2012 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.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.chromium.debug.core.model.IPredefinedSourceWrapProvider;
import org.chromium.debug.core.model.LaunchParams;
import org.chromium.debug.core.model.LaunchParams.LookupMode;
import org.chromium.debug.ui.PluginUtil;
import org.chromium.debug.ui.TableUtils;
import org.chromium.debug.ui.TableUtils.ColumnBasedLabelProvider;
import org.chromium.debug.ui.TableUtils.ColumnData;
import org.chromium.debug.ui.launcher.LaunchTabGroup.Params;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.osgi.util.NLS;
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.Control;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;

/**
 * A launch configuration tab that holds source look-up and mapping parameters.
 */
public class ScriptMappingTab extends TabBase<ScriptMappingTab.Elements, Params>  {
  interface Elements {
    RadioButtonsLogic<LookupMode> getLookupMode();
    PredefinedWrapChooser getPredefinedWrapChooser();
  }

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

  @Override
  public Elements createElements(Composite parent, final Runnable modifyListener) {
    Composite composite = createDefaultComposite(parent);
    setControl(composite);

    LookupModeControl lookupModeControl =
        new LookupModeControl(composite, getParams().getScriptNameDescription());


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

    final RadioButtonsLogic<LookupMode> lookupModeLogic =
        lookupModeControl.createLogic(radioButtonsListener);

    Group group = new Group(composite, SWT.NONE);
    group.setText(Messages.ScriptMappingTab_RECOGNIZED_WRAPPING);
    {
      GridData data = new GridData();
      data.horizontalAlignment = GridData.FILL;
      group.setLayoutData(data);

      GridLayout layout = new GridLayout();
      layout.numColumns = 1;
      group.setLayout(layout);
    }

    final PredefinedWrapChooser wrapChooser = new PredefinedWrapChooser(group);

    {
      GridData data = new GridData();
      data.horizontalAlignment = GridData.FILL;
      wrapChooser.getControl().setLayoutData(data);
    }

    wrapChooser.addListener(new PredefinedWrapChooser.Listener() {
      @Override public void checkStateChanged() {
        modifyListener.run();
      }
    });

    return new Elements() {
      @Override public RadioButtonsLogic<LookupMode> getLookupMode() {
        return lookupModeLogic;
      }
      @Override public PredefinedWrapChooser getPredefinedWrapChooser() {
        return wrapChooser;
      }
    };
  }

  /**
   * UI control that allows to select supported wrappers from a predefined set.
   * It deals in terms {@link IPredefinedSourceWrapProvider.Entry} ids.
   * The control gives special treatment to ids that weren't resolved allowing user
   * to deselect them.
   */
  private static class PredefinedWrapChooser {
    private final Control root;
    private final CheckboxTableViewer tableViewer;
    private final List<Listener> listeners = new ArrayList<Listener>(1);

    PredefinedWrapChooser(Composite parent) {
      final Table table = new Table(parent, SWT.CHECK | SWT.BORDER | SWT.MULTI |
          SWT.FULL_SELECTION | SWT.V_SCROLL);
      root = table;

      table.setFont(parent.getFont());

      tableViewer = new CheckboxTableViewer(table);

      table.setHeaderVisible(false);

      tableViewer.setContentProvider(TableUtils.OBJECT_ARRAY_CONTENT_PROVIDER);

      TableUtils.ColumnLabelProvider<ItemData> columnOneLabelProvider =
          new TableUtils.ColumnLabelProvider<ItemData>() {
        @Override public Image getColumnImage(ItemData element) {
          return null;
        }
        @Override
        public String getColumnText(ItemData element) {
          return element.accept(columnTextVisitor);
        }
        private final ItemData.Visitor<String> columnTextVisitor = new ItemData.Visitor<String>() {
          @Override public String visitNormal(IPredefinedSourceWrapProvider.Entry entry) {
            return entry.getWrapper().getName();
          }
          @Override public String visitUnresolved(String id) {
            return NLS.bind(Messages.ScriptMappingTab_UNRESOVLED, id);
          }
        };

        @Override
        public TableColumn createColumn(Table table) {
          TableColumn statusCol = new TableColumn(table, SWT.NONE);
          int width = PluginUtil.getFontMetrics(table, table.getFont()).getAverageCharWidth() * 40;
          statusCol.setWidth(width);
          return statusCol;
        }
      };

      List<ColumnData<ItemData, ?>> columnList = new ArrayList<ColumnData<ItemData, ?>>(1);
      columnList.add(ColumnData.create(
          new TableUtils.TrivialAdapter<ItemData>(), columnOneLabelProvider));

      ColumnBasedLabelProvider<ItemData> labelProvider = new ColumnBasedLabelProvider<ItemData>(
          TableUtils.createCastAdapter(ItemData.class), columnList);

      labelProvider.setUpColumns(table);

      tableViewer.setLabelProvider(labelProvider);

      tableViewer.addDoubleClickListener(new IDoubleClickListener() {
        @Override
        public void doubleClick(final DoubleClickEvent event) {
          ISelection selection = event.getSelection();
          if (selection instanceof IStructuredSelection == false) {
            return;
          }
          IStructuredSelection structuredSelection = (IStructuredSelection) selection;
          if (structuredSelection.size() != 1) {
            return;
          }
          ItemData data = (ItemData) structuredSelection.getFirstElement();
          data.accept(new ItemData.Visitor<Void>() {
            @Override
            public Void visitNormal(IPredefinedSourceWrapProvider.Entry entry) {
              MessageBox messageBox =
                  new MessageBox(event.getViewer().getControl().getShell(), SWT.RESIZE);
              String description = entry.getHumanDescription();
              messageBox.setText(Messages.ScriptMappingTab_DESCRIPTION);
              messageBox.setMessage(description);
              messageBox.open();
              return null;
            }

            @Override
            public Void visitUnresolved(String id) {
              // Keep silent.
              return null;
            }
          });
        }
      });
      tableViewer.addSelectionChangedListener(new ISelectionChangedListener() {
        @Override
        public void selectionChanged(SelectionChangedEvent event) {
          for (Listener listener : listeners) {
            listener.checkStateChanged();
          }
        }
      });
    }

    Control getControl() {
      return root;
    }

    void setData(Collection<String> wrapperIds,
        Map<String, IPredefinedSourceWrapProvider.Entry> entriesMap) {
      List<ItemData> itemList = new ArrayList<ItemData>(entriesMap.size());

      Map<String, Integer> idToPosition = new HashMap<String, Integer>(entriesMap.size());

      for (Map.Entry<String, IPredefinedSourceWrapProvider.Entry> en : entriesMap.entrySet()) {
        idToPosition.put(en.getKey(), itemList.size());
        itemList.add(ItemData.createNormal(en.getValue()));
      }

      List<ItemData> selected = new ArrayList<ItemData>(wrapperIds.size());

      for (String id : wrapperIds) {
        ItemData item;
        Integer pos = idToPosition.get(id);
        if (pos == null) {
          item = ItemData.createUnresolved(id);
          itemList.add(item);
        } else {
          item = itemList.get(pos);
        }
        selected.add(item);
      }

      tableViewer.setInput(itemList.toArray());
      tableViewer.setCheckedElements(selected.toArray());
    }

    Collection<String> getValue() {
      Object[] selection = tableViewer.getCheckedElements();
      List<String> result = new ArrayList<String>(selection.length);
      ItemData.Visitor<String> visitor = new ItemData.Visitor<String>() {
        @Override public String visitNormal(IPredefinedSourceWrapProvider.Entry entry) {
          return entry.getId();
        }
        @Override public String visitUnresolved(String id) {
          return id;
        }
      };
      for (Object selectionObject : selection) {
        ItemData item = (ItemData) selectionObject;
        result.add(item.accept(visitor));
      }
      return result;
    }

    void addListener(Listener listener) {
      listeners.add(listener);
    }

    static abstract class ItemData {
      interface Visitor<R> {
        R visitNormal(IPredefinedSourceWrapProvider.Entry entry);
        R visitUnresolved(String id);
      }
      abstract <R> R accept(Visitor<R> visitor);

      static ItemData createNormal(final IPredefinedSourceWrapProvider.Entry entry) {
        return new ItemData() {
          @Override <R> R accept(Visitor<R> visitor) {
            return visitor.visitNormal(entry);
          }
        };
      }

      static ItemData createUnresolved(final String id) {
        return new ItemData() {
          @Override <R> R accept(Visitor<R> visitor) {
            return visitor.visitUnresolved(id);
          }
        };
      }
    }

    interface Listener {
      void checkStateChanged();
    }
  }

  /**
   * Dialog UI group of 2 radio buttons for lookup mode.
   */
  private static class LookupModeControl {
    private final Map<LookupMode, Button> buttons;
    LookupModeControl(Composite container, String scriptNameFormatDescription) {
      buttons = new LinkedHashMap<LookupMode, Button>();
      Group group = new Group(container, 0);
      group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
      group.setText(Messages.ChromiumRemoteTab_LOOKUP_GROUP_TITLE);
      group.setLayout(new GridLayout(1, false));

      buttons.put(LookupMode.EXACT_MATCH, createButtonBlock(group,
          Messages.ChromiumRemoteTab_EXACT_MATCH, Messages.ChromiumRemoteTab_EXACT_MATCH_LINE1,
          Messages.ChromiumRemoteTab_EXACT_MATCH_LINE2));

      buttons.put(LookupMode.AUTO_DETECT, createButtonBlock(group,
          Messages.ChromiumRemoteTab_AUTODETECT, Messages.ChromiumRemoteTab_AUTODETECT_LINE1,
          Messages.ChromiumRemoteTab_AUTODETECT_LINE2 + scriptNameFormatDescription));

      addRadioButtonSwitcher(buttons.values());
    }

    RadioButtonsLogic<LookupMode> createLogic(RadioButtonsLogic.Listener listener) {
      return new RadioButtonsLogic<LookupMode>(buttons, listener);
    }

    private static Button createButtonBlock(Composite parent, String buttonLabel,
        String descriptionLine1, String descriptionLine2) {
      Composite buttonComposite = new Composite(parent, SWT.NONE);
      GridLayout gridLayout = createHtmlStyleGridLayout(3);
      buttonComposite.setLayout(gridLayout);
      Button button = new Button(buttonComposite, SWT.RADIO | SWT.NO_RADIO_GROUP);
      button.setText(buttonLabel);
      Label padding = new Label(buttonComposite, SWT.NONE);
      padding.setText("   "); //$NON-NLS-1$
      Label descriptionLine1Label = new Label(buttonComposite, SWT.NONE);
      descriptionLine1Label.setText(descriptionLine1);
      // Extra label to fill a grid in layout.
      new Label(buttonComposite, SWT.NONE);
      new Label(buttonComposite, SWT.NONE);
      Label descriptionLine2Label = new Label(buttonComposite, SWT.NONE);
      descriptionLine2Label.setText(descriptionLine2);
      return button;
    }
  }

  @Override
  public String getName() {
    return Messages.ScriptMappingTab_TAB_NAME;
  }

  @Override
  protected MessageData isValidImpl(ILaunchConfiguration config) {
    try {
      LaunchParams.PredefinedSourceWrapperIds.resolveEntries(config);
    } catch (CoreException e) {
      return new MessageData(false,
          NLS.bind(Messages.ScriptMappingTab_UNRESOLVED_ERROR_MESSAGE, e.getMessage()));
    }
    return null;
  }

  protected TabFieldList<Elements, Params> getTabFields() {
    return TAB_FIELDS;
  }

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

    list.add(new TabField<String, LookupMode, Elements, Params>(
        LaunchParams.SOURCE_LOOKUP_MODE, TypedMethods.STRING,
        new FieldAccess<LookupMode, Elements>() {
          @Override
          void setValue(LookupMode value, Elements tabElements) {
            tabElements.getLookupMode().select(value);
          }
          @Override
          LookupMode getValue(Elements tabElements) {
            return tabElements.getLookupMode().getSelected();
          }
        },
        new DefaultsProvider<LookupMode, Params>() {
          @Override LookupMode getFallbackValue() {
            // TODO: support default value from eclipse variables.
            return LookupMode.DEFAULT_VALUE;
          }
          @Override LookupMode getInitialConfigValue(Params context) {
            return LookupMode.AUTO_DETECT;
          }
        },
        LookupMode.STRING_CONVERTER));

    list.add(new TabField<String, List<String>, Elements, Params>(
        LaunchParams.PredefinedSourceWrapperIds.CONFIG_PROPERTY, TypedMethods.STRING,
            new FieldAccess<List<String>, Elements>() {
          @Override
          void setValue(List<String> value, Elements tabElements) {
            tabElements.getPredefinedWrapChooser().setData(value,
                IPredefinedSourceWrapProvider.Access.getEntries());
          }
          @Override
          List<String> getValue(Elements tabElements) {
            List<String> result =
                new ArrayList<String>(tabElements.getPredefinedWrapChooser().getValue());
            return result;
          }
        },
        new DefaultsProvider<List<String>, Params>() {
          @Override List<String> getFallbackValue() {
            return Collections.emptyList();
          }
          @Override
          List<String> getInitialConfigValue(Params context) {
            List<String> result;
            if (context.preEnableSourceWrapper()) {
              result = new ArrayList<String>(
                  IPredefinedSourceWrapProvider.Access.getEntries().keySet());
            } else {
              result = Collections.emptyList();
            }
            return result;
          }
        },
        LaunchParams.PredefinedSourceWrapperIds.CONVERTER));

    TAB_FIELDS = createFieldListImpl(list);
  }
}
