/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.uima.taeconfigurator.editors.ui;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.uima.UIMAFramework;
import org.apache.uima.analysis_engine.AnalysisEngineDescription;
import org.apache.uima.analysis_engine.metadata.FlowControllerDeclaration;
import org.apache.uima.analysis_engine.metadata.SofaMapping;
import org.apache.uima.collection.CasConsumerDescription;
import org.apache.uima.flow.FlowControllerDescription;
import org.apache.uima.resource.ResourceCreationSpecifier;
import org.apache.uima.resource.ResourceSpecifier;
import org.apache.uima.taeconfigurator.editors.MultiPageEditor;
import org.apache.uima.taeconfigurator.editors.ui.dialogs.EditSofaBindingsDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.forms.IManagedForm;

/**
 * The Class SofaMapSection.
 */
public class SofaMapSection extends AbstractSection {

  /** The section client. */
  private Composite sectionClient;

  /** The add button. */
  private Button addButton;

  /** The edit button. */
  private Button editButton;

  /** The remove button. */
  private Button removeButton;

  /** The tree. */
  private Tree tree;

  /** The Constant INPUTS. */
  private static final String INPUTS = "Inputs";

  /** The Constant OUTPUTS. */
  private static final String OUTPUTS = "Outputs";

  /** The Constant INPUT. */
  private static final boolean INPUT = true;

  /** The Constant OUTPUT. */
  private static final boolean OUTPUT = false;

  /** The Constant titleMsg. */
  private static final String titleMsg = "This section shows all defined Sofas for an Aggregate and their mappings to the component Sofas.\n"
          + "Add Aggregate Sofa Names using the Capabilities section; Select an Aggregate Sofa Name and Add/Edit mappings for that Sofa in this section.\n";

  /**
   * Instantiates a new sofa map section.
   *
   * @param aEditor
   *          the a editor
   * @param parent
   *          the parent
   */
  public SofaMapSection(MultiPageEditor aEditor, Composite parent) {
    super(aEditor, parent, "Sofa Mappings (Only used in aggregate Descriptors)", titleMsg);
  }

  /*
   * Called by the page constructor after all sections are created, to initialize them.
   * (non-Javadoc)
   * 
   * @see org.eclipse.ui.forms.IFormPart#initialize(org.eclipse.ui.forms.IManagedForm)
   */
  @Override
  public void initialize(IManagedForm form) {

    super.initialize(form);
    sectionClient = new2ColumnComposite(getSection());

    tree = newTree(sectionClient);
    Composite buttonContainer = newButtonContainer(sectionClient);
    addButton = newPushButton(buttonContainer, S_ADD,
            "Click here to add a component Sofa binding.");
    editButton = newPushButton(buttonContainer, S_EDIT, S_EDIT_TIP);
    removeButton = newPushButton(buttonContainer, S_REMOVE, S_REMOVE_TIP);

    tree.addListener(SWT.MouseDoubleClick, this); // edit gesture

    enableBorders(sectionClient);
    toolkit.paintBordersFor(sectionClient);
    if (!isAggregate())
      getSection().setExpanded(false);
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.eclipse.ui.forms.IFormPart#refresh()
   */
  @Override
  public void refresh() {
    super.refresh();
    tree.removeAll();
    if (!isAggregate()) {
      getSection().setText("Sofa Mappings (Only used in aggregate Descriptors)");
      getSection().setDescription("This part is only used for Aggregate Descriptors");
      getSection().setExpanded(false);
    } else {
      getSection().setDescription(titleMsg);

      String[][] sns = getCapabilitySofaNames();
      String[] inputSofaNames = sns[0];
      String[] outputSofaNames = sns[1];
      // getAggrSofas the names declared in the capability
      // plus any undeclared names in the mappings
      // sorted... alphabetically
      String[] inputAggrSofas = getAggrSofas(inputSofaNames, outputSofaNames);

      fillMap(inputAggrSofas, INPUT);
      fillMap(outputSofaNames, OUTPUT);
      maybeSetSelection(tree, 0);

      if (0 == (inputAggrSofas.length + outputSofaNames.length)) {
        getSection().setText("Sofa Mappings (No Sofas are defined)");
        getSection().setExpanded(false);
      } else {
        getSection().setText("Sofa Mappings");
        getSection().setExpanded(true);
      }

    }
    enable();
  }

  /**
   * Gets the aggr sofas.
   *
   * @param inputCapabilityNames
   *          the input capability names
   * @param outputCapabilityNames
   *          the output capability names
   * @return the aggr sofas
   */
  private String[] getAggrSofas(String[] inputCapabilityNames, String[] outputCapabilityNames) {
    SofaMapping[] allMappings = getSofaMappings();
    Set names = new TreeSet();
    Set undeclaredNames = new TreeSet();
    if (null != inputCapabilityNames)
      for (int i = 0; i < inputCapabilityNames.length; i++)
        names.add(inputCapabilityNames[i]);
    if (null != allMappings) {
      for (int i = 0; i < allMappings.length; i++) {
        String sofaName = allMappings[i].getAggregateSofaName();
        if (0 > Arrays.binarySearch(inputCapabilityNames, sofaName)
                && 0 > Arrays.binarySearch(outputCapabilityNames, sofaName))
          undeclaredNames.add(sofaName);
      }
    }

    // It is an error to have a mapping without having the aggregate name
    // declared as either an input our output. If the name is not
    // declared, (silently) consider it to have been an input.
    if (undeclaredNames.size() > 0)
      names.addAll(undeclaredNames);

    return (String[]) names.toArray(stringArray0);
  }

  /**
   * Fill map.
   *
   * @param aggrKeys
   *          the aggr keys
   * @param isInput
   *          the is input
   */
  private void fillMap(String[] aggrKeys, boolean isInput) {

    TreeItem d = new TreeItem(tree, SWT.NONE);
    d.setText(isInput ? INPUTS : OUTPUTS);
    for (int i = 0; i < aggrKeys.length; i++) {
      TreeItem a = new TreeItem(d, SWT.NONE);
      a.setText(aggrKeys[i]);
      fillBindings(a, aggrKeys[i]);
      a.setExpanded(true);
    }
    d.setExpanded(true);
  }

  /**
   * Fill bindings.
   *
   * @param parent
   *          the parent
   * @param aggrSofa
   *          the aggr sofa
   */
  private void fillBindings(TreeItem parent, String aggrSofa) {
    // bindings are a string of key-name / sofa-name or "<default>"
    String[] bindings = getSofaBindingsForAggrSofa(aggrSofa);
    for (int j = 0; j < bindings.length; j++) {
      TreeItem b = new TreeItem(parent, SWT.NONE);
      b.setText(bindings[j]);
    }
  }

  /**
   * Gets the sofa bindings for aggr sofa.
   *
   * @param aggrSofa
   *          the aggr sofa
   * @return the sofa bindings for aggr sofa
   */
  private String[] getSofaBindingsForAggrSofa(String aggrSofa) {
    SofaMapping[] sofaMappings = getSofaMappings();
    if (null == sofaMappings)
      return stringArray0;
    Set bindings = new TreeSet();
    for (int i = 0; i < sofaMappings.length; i++) {
      SofaMapping sofaMapping = sofaMappings[i];
      if (sofaMapping.getAggregateSofaName().equals(aggrSofa))
        if (null != sofaMapping.getComponentSofaName()
                && !"".equals(sofaMapping.getComponentSofaName()))
          bindings.add(sofaMapping.getComponentKey() + '/' + sofaMapping.getComponentSofaName());
        else
          bindings.add(sofaMapping.getComponentKey());
    }
    String[] results = (String[]) bindings.toArray(stringArray0);
    Arrays.sort(results);
    return results;
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.eclipse.swt.widgets.Listener#handleEvent(org.eclipse.swt.widgets.Event)
   */
  @Override
  public void handleEvent(Event event) {
    // Note: to add aggrSofa names, use capabilitySection.
    // Updates there are propagated here.

    // only enabled when one existing AggrSofa
    // or its child is selected
    if (event.widget == editButton || event.type == SWT.MouseDoubleClick) {
      if (!editButton.getEnabled())
        return;
      // Edit a map: a new aggr capability + a delegate sofa, or
      // a delegate sofa to an existing map.
      TreeItem selected = tree.getSelection()[0];
      TreeItem parent = selected.getParentItem();
      if (null != parent.getParentItem()) {
        selected = parent;
      }
      editAggrMap(selected);
    } else if (event.widget == addButton) {
      // Add one or more new mappings
      TreeItem selected = tree.getSelection()[0];
      TreeItem parent = selected.getParentItem();
      if (null != parent.getParentItem()) {
        selected = parent;
      }
      addAggrMap(selected);
    } else if (event.widget == removeButton) {
      // only enabled for aggr or component
      TreeItem selected = tree.getSelection()[0];
      TreeItem parent = selected.getParentItem();
      if (null == parent.getParentItem())
        removeAggr(selected);
      else
        removeComponentFromAggr(selected);
    }
    enable();
  }

  /** The Constant AVAIL_ONLY. */
  private static final boolean AVAIL_ONLY = true;

  /**
   * Edits the aggr map.
   *
   * @param selected
   *          the selected
   */
  private void editAggrMap(TreeItem selected) {
    // pop up window: shows all available component mappings
    // plus current mappings for this aggrSofa
    // Available: a) not mapped
    // User selects mappings to update
    // update model: add (multiple) mappings
    // Remove all under item
    // update model.
    String aggrSofa = selected.getText();
    Map availAndBoundSofas = getAvailAndBoundSofas(aggrSofa, !AVAIL_ONLY);
    if (availAndBoundSofas.size() == 0) {
      Utility.popMessage("No available sofas",
              "Because there are no sofas in the delegates that are not already bound, no sofa mapping can be created.",
              MessageDialog.WARNING);
      return;
    }

    EditSofaBindingsDialog dialog = new EditSofaBindingsDialog(this, aggrSofa, availAndBoundSofas);
    if (dialog.open() == Window.CANCEL)
      return;
    removeAggr(aggrSofa);
    addAggr(aggrSofa, dialog.selectedSofaNames);
    removeChildren(selected);
    fillBindings(selected, aggrSofa);
    selected.setExpanded(true);
    setFileDirty();
  }

  /**
   * Adds the aggr map.
   *
   * @param selected
   *          the selected
   */
  private void addAggrMap(TreeItem selected) {
    // pop up window: shows all available component mappings
    // minus current mappings for this aggrSofa
    // Available: a) not mapped,
    // User selects mappings to add
    String aggrSofa = selected.getText();
    Map availAndBoundSofas = getAvailAndBoundSofas(aggrSofa, AVAIL_ONLY);
    if (availAndBoundSofas.size() == 0) {
      Utility.popMessage("No available sofas",
              "Because there are no sofas in the delegates that are not already bound, no sofa mapping can be created.",
              MessageDialog.WARNING);
      return;
    }

    EditSofaBindingsDialog dialog = new EditSofaBindingsDialog(this, aggrSofa, availAndBoundSofas);
    if (dialog.open() == Window.CANCEL)
      return;
    addAggr(aggrSofa, dialog.selectedSofaNames);
    removeChildren(selected);
    fillBindings(selected, aggrSofa);
    selected.setExpanded(true);
    setFileDirty();
  }

  /**
   * Adds the sofas to all component sofa map.
   *
   * @param allComponentSofas
   *          the all component sofas
   * @param key
   *          the key
   * @param delegate
   *          the delegate
   * @param isInput
   *          the is input
   */
  private void addSofasToAllComponentSofaMap(Map allComponentSofas, String key,
          ResourceSpecifier delegate, boolean isInput) {
    // delegates can be AnalysisEngines, CasConsmers, flowControllers, or remotes
    if (delegate instanceof AnalysisEngineDescription || delegate instanceof CasConsumerDescription
            || delegate instanceof FlowControllerDescription) {
      Set[] inAndOut = getCapabilitySofaNames((ResourceCreationSpecifier) delegate, key);
      Set inOut = inAndOut[isInput ? 0 : 1];
      if (!isInput) { // Aggr "output" can be mapped to delegate "input"
        inOut.addAll(inAndOut[0]);
      }
      if (inOut.size() == 0) {
        // no sofas defined in this delegate
        // create default sofa
        allComponentSofas.put(key, null);
      }
      for (Iterator i2 = inOut.iterator(); i2.hasNext();) {
        allComponentSofas.put(i2.next(), null);
      }
    }
  }

  /**
   * Gets the avail and bound sofas.
   *
   * @param aggrSofa
   *          the aggr sofa
   * @param availOnly
   *          the avail only
   * @return a Map, keys = component/sofaname, value = aggrsofa or null
   */
  private Map getAvailAndBoundSofas(String aggrSofa, boolean availOnly) {
    boolean isInput = isInput(aggrSofa);
    Map allComponentSofas = new TreeMap(); // key = component/sofa, value = AggrSofa bound to

    // put all delegate component/sofa items in a Map
    for (Iterator it = editor.getResolvedDelegates().entrySet().iterator(); it.hasNext();) {
      Map.Entry entry = (Map.Entry) it.next();
      addSofasToAllComponentSofaMap(allComponentSofas, (String) entry.getKey(),
              (ResourceSpecifier) entry.getValue(), isInput);
    }
    // pick up any sofa info from flow controller
    FlowControllerDeclaration fcd = editor.getResolvedFlowControllerDeclaration();
    if (null != fcd) {
      addSofasToAllComponentSofaMap(allComponentSofas, fcd.getKey(), fcd.getSpecifier(), isInput);
    }

    // mark the bound ones with a value which is the aggr sofa they're bound to
    // also, add any that were not spec'd in the delegates (because
    // for instance, they were remote)
    SofaMapping[] sofaMappings = getSofaMappings();
    for (int i = 0; i < sofaMappings.length; i++) {
      SofaMapping sm = sofaMappings[i];
      String key = sm.getComponentKey();
      if (null != sm.getComponentSofaName())
        key = key + '/' + sm.getComponentSofaName();
      if (availOnly)
        allComponentSofas.remove(key);
      else
        allComponentSofas.put(key, sm.getAggregateSofaName());
    }

    // remove all that are bound to other Aggr sofa names
    // because although an Aggr sofa can be bound to many delegate sofas,
    // a delegate sofa can only be bound to one aggr one.

    for (Iterator i3 = allComponentSofas.entrySet().iterator(); i3.hasNext();) {
      Map.Entry entry = (Map.Entry) i3.next();
      String boundAggrSofa = (String) entry.getValue();
      if (null != boundAggrSofa && !boundAggrSofa.equals(aggrSofa))
        i3.remove();
    }

    return allComponentSofas;
  }

  /**
   * Checks if is input.
   *
   * @param sofaName
   *          the sofa name
   * @return true, if is input
   */
  private boolean isInput(String sofaName) {
    String[][] sns = getCapabilitySofaNames();
    for (int i = 0; i < sns[0].length; i++) {
      if (sofaName.equals(sns[0][i]))
        return true;
    }
    return false;
  }

  /**
   * Adds the aggr.
   *
   * @param aggrSofa
   *          the aggr sofa
   * @param sofaNames
   *          the sofa names
   */
  private void addAggr(String aggrSofa, String[] sofaNames) {
    SofaMapping[] newSofas = new SofaMapping[sofaNames.length];
    for (int i = 0; i < sofaNames.length; i++) {
      newSofas[i] = UIMAFramework.getResourceSpecifierFactory().createSofaMapping();
      newSofas[i].setAggregateSofaName(aggrSofa);
      newSofas[i].setComponentKey(getComponentOnly(sofaNames[i]));
      newSofas[i].setComponentSofaName(getSofaOnly(sofaNames[i]));
    }
    SofaMapping[] oldSofas = getSofaMappings();
    SofaMapping[] result = new SofaMapping[oldSofas.length + newSofas.length];
    System.arraycopy(oldSofas, 0, result, 0, oldSofas.length);
    System.arraycopy(newSofas, 0, result, oldSofas.length, newSofas.length);
    editor.getAeDescription().setSofaMappings(result);
  }

  /**
   * Removes the aggr.
   *
   * @param aggrSofa
   *          the aggr sofa
   */
  private void removeAggr(String aggrSofa) {
    Comparator comparator = new Comparator() {
      @Override
      public int compare(Object aggrSofaName, Object o2) {
        SofaMapping sofaMapping = (SofaMapping) o2;
        if (sofaMapping.getAggregateSofaName().equals(aggrSofaName))
          return 0;
        else
          return -1;
      }
    };
    editor.getAeDescription().setSofaMappings((SofaMapping[]) Utility
            .removeElementsFromArray(getSofaMappings(), aggrSofa, SofaMapping.class, comparator));
  }

  /**
   * Removes the aggr.
   *
   * @param selected
   *          the selected
   */
  private void removeAggr(TreeItem selected) {
    if (Window.CANCEL == Utility.popOkCancel("Confirm delete of sofa mappings",
            "Please confirm deletion of all sofa mappings for this Aggregate Sofa name.  Note this will not delete the Sofa name.  To do that, remove the name from the Component Capabilities panel (the other panel on this page).",
            MessageDialog.WARNING))
      return;
    removeAggr(selected.getText());
    removeChildren(selected);
    setFileDirty();
  }

  /**
   * Removes a delegate map from a particular aggr sofa mapping.
   *
   * @param selected
   *          the selected
   */
  private void removeComponentFromAggr(TreeItem selected) {
    final String aggrName = selected.getParentItem().getText();
    Comparator comparator = new Comparator() {
      @Override
      public int compare(Object componentAndSofa, Object o2) {
        SofaMapping sofaMapping = (SofaMapping) o2;
        if (!sofaMapping.getAggregateSofaName().equals(aggrName))
          return -1;
        String component = getComponentOnly((String) componentAndSofa);
        if (!sofaMapping.getComponentKey().equals(component))
          return -1;
        String sofa = getSofaOnly((String) componentAndSofa);
        if (null == sofa || sofa.equals(""))
          if (null == sofaMapping.getComponentSofaName()
                  || "".equals(sofaMapping.getComponentSofaName()))
            return 0;
          else
            return -1;
        else if (sofa.equals(sofaMapping.getComponentSofaName()))
          return 0;
        else
          return -1;
      }
    };

    editor.getAeDescription()
            .setSofaMappings((SofaMapping[]) Utility.removeElementsFromArray(getSofaMappings(),
                    selected.getText(), SofaMapping.class, comparator));
    selected.dispose();
    setFileDirty();
  }

  /**
   * Called when removing a delegate from the aggr. Removes from the sofaMappings, any and all
   * mappings associated with the delegate.
   *
   * @param componentKey
   *          the component key
   * @param delegate
   *          the delegate
   * @param pEditor
   *          the editor
   */
  public static void removeSofaMappings(String componentKey, ResourceSpecifier delegate,
          MultiPageEditor pEditor) {
    if (delegate instanceof AnalysisEngineDescription
            || delegate instanceof CasConsumerDescription) {
      Set[] inOut = getCapabilitySofaNames((ResourceCreationSpecifier) delegate, componentKey);
      inOut[0].addAll(inOut[1]);
      final Set allDelegateComponentSofas = inOut[0];
      Comparator comparator = new Comparator() {
        @Override
        public int compare(Object ignore, Object elementOfArray) {
          SofaMapping sofaMapping = (SofaMapping) elementOfArray;
          String key = sofaMapping.getComponentKey();
          if (null != sofaMapping.getComponentSofaName())
            key = key + '/' + sofaMapping.getComponentSofaName();
          if (allDelegateComponentSofas.contains(key)) {
            return 0;
          }
          return -1;
        }
      };

      pEditor.getAeDescription().setSofaMappings((SofaMapping[]) Utility.removeElementsFromArray(
              getSofaMappings(pEditor), null, SofaMapping.class, comparator));
    }
  }

  /**
   * Gets the sofa only.
   *
   * @param componentAndSofa
   *          the component and sofa
   * @return the sofa only
   */
  private String getSofaOnly(String componentAndSofa) {
    int locOfSlash = componentAndSofa.indexOf('/');
    if (locOfSlash < 0)
      return null;
    return componentAndSofa.substring(locOfSlash + 1);
  }

  /**
   * Gets the component only.
   *
   * @param componentAndSofa
   *          the component and sofa
   * @return the component only
   */
  private String getComponentOnly(String componentAndSofa) {
    int locOfSlash = componentAndSofa.indexOf('/');
    if (locOfSlash < 0)
      return componentAndSofa;
    return componentAndSofa.substring(0, locOfSlash);
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.taeconfigurator.editors.ui.AbstractSection#enable()
   */
  @Override
  public void enable() {
    boolean oneSelected = tree.getSelectionCount() == 1;
    boolean topLevelSelected = false;
    if (oneSelected) {
      TreeItem selected = tree.getSelection()[0];
      topLevelSelected = (null == selected.getParentItem());
    }
    addButton.setEnabled(oneSelected && !topLevelSelected);
    editButton.setEnabled(oneSelected && !topLevelSelected);
    removeButton.setEnabled(oneSelected && !topLevelSelected);
  }

}
