/*
 * 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.io.IOException;
import java.io.PrintWriter;
import java.text.MessageFormat;

import org.apache.uima.resource.metadata.Import;
import org.apache.uima.taeconfigurator.CDEpropertyPage;
import org.apache.uima.taeconfigurator.InternalErrorCDE;
import org.apache.uima.taeconfigurator.editors.MultiPageEditor;
import org.apache.uima.taeconfigurator.editors.ui.dialogs.CommonInputDialog;
import org.apache.uima.taeconfigurator.editors.ui.dialogs.ExportImportablePartDialog;
import org.apache.uima.taeconfigurator.files.MultiResourceSelectionDialog;
import org.apache.uima.util.InvalidXMLException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.ui.forms.IManagedForm;

/**
 * Imports - used by aggregates types type priorities indexes external resource specifications.
 */
public abstract class ImportSection extends AbstractSection {

  /**
   * Gets the model import array.
   *
   * @return the model import array
   */
  protected abstract Import[] getModelImportArray();

  /**
   * Sets the model import array.
   *
   * @param newImports
   *          the new model import array
   */
  protected abstract void setModelImportArray(Import[] newImports);

  /**
   * Checks if is valid import.
   *
   * @param title
   *          the title
   * @param message
   *          the message
   * @return true, if is valid import
   */
  protected abstract boolean isValidImport(String title, String message);

  /**
   * Finish import change action.
   */
  protected abstract void finishImportChangeAction();

  /**
   * Gets the description from import.
   *
   * @param source
   *          the source
   * @return the description from import
   * @throws InvalidXMLException
   *           the invalid XML exception
   * @throws IOException
   *           Signals that an I/O exception has occurred.
   */
  protected abstract String getDescriptionFromImport(String source)
          throws InvalidXMLException, IOException;

  /**
   * Checks if is appropriate.
   *
   * @return true, if is appropriate
   */
  protected abstract boolean isAppropriate(); // if false, don't show section

  /**
   * Clear model base value.
   */
  protected abstract void clearModelBaseValue(); // used to clear exported value

  /** The Constant TABLE_HOVER_REQUERY_TIME. */
  protected static final long TABLE_HOVER_REQUERY_TIME = 15000;

  /** The last table hover item. */
  protected TableItem lastTableHoverItem = null;

  /** The l last table hover millis. */
  protected long lLastTableHoverMillis = -1;

  /** The s last table hover help. */
  protected String sLastTableHoverHelp = "";

  /** The b disable tool tip help. */
  protected boolean bDisableToolTipHelp = false;

  /** The Constant TABLE_INDICATOR_BY_NAME. */
  private static final String TABLE_INDICATOR_BY_NAME = "By Name";

  /** The Constant TABLE_INDICATOR_BY_LOCATION. */
  private static final String TABLE_INDICATOR_BY_LOCATION = "By Location";

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

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

  /** The set data path button. */
  private Button setDataPathButton;

  /** The import table. */
  Table importTable;

  /**
   * Instantiates a new import section.
   *
   * @param aEditor
   *          the a editor
   * @param parent
   *          the parent
   * @param title
   *          the title
   * @param description
   *          the description
   */
  public ImportSection(MultiPageEditor aEditor, Composite parent, String title,
          String description) {
    super(aEditor, parent, title, description);
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * org.apache.uima.taeconfigurator.editors.ui.AbstractSection#initialize(org.eclipse.ui.forms.
   * IManagedForm)
   */
  @Override
  public void initialize(IManagedForm form) {
    super.initialize(form);

    // set up Composite to hold widgets in the section
    Composite sectionClient = newComposite(getSection());
    enableBorders(sectionClient);

    Composite buttonContainer = new2ColumnComposite(sectionClient);
    ((GridData) buttonContainer.getLayoutData()).grabExcessVerticalSpace = false;
    ((GridData) buttonContainer.getLayoutData()).grabExcessHorizontalSpace = false;
    ((GridData) buttonContainer.getLayoutData()).horizontalAlignment = SWT.RIGHT;

    addButton = newPushButton(buttonContainer, "Add...", "Click here to add an import");
    removeButton = newPushButton(buttonContainer, "Remove",
            "Click here to remove the selected import.");
    setDataPathButton = newPushButton(buttonContainer, "Set DataPath",
            "Click here to view or set the data path to use when resolving imports by name.");
    importTable = newTable(sectionClient, SWT.FULL_SELECTION, NO_MIN_HEIGHT);

    newTableColumn(importTable).setText("Kind");
    newTableColumn(importTable).setText("Location/Name");

    importTable.setHeaderVisible(true);
    packTable(importTable);

    // in addition to normal keyup and mouse up:
    importTable.addListener(SWT.MouseHover, this);
    importTable.addListener(SWT.MouseDown, this);

    toolkit.paintBordersFor(sectionClient);
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.eclipse.ui.forms.AbstractFormPart#refresh()
   */
  @Override
  public void refresh() {
    super.refresh();
    importTable.removeAll();

    if (isAppropriate()) {

      Import[] importItems = getModelImportArray();
      if (importItems != null) {
        for (int i = 0; i < importItems.length; i++) {
          if (importItems[i] != null) {
            TableItem tableItem = new TableItem(importTable, SWT.NONE);
            if (importItems[i].getLocation() != null) {
              tableItem.setText(0, TABLE_INDICATOR_BY_LOCATION);
              tableItem.setText(1, importItems[i].getLocation());
            } else {
              tableItem.setText(0, TABLE_INDICATOR_BY_NAME);
              tableItem.setText(1, importItems[i].getName());
            }
          }
        }
        packTable(importTable);
      }
    }

    enable();
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * org.apache.uima.taeconfigurator.editors.ui.AbstractTableSection#handleEvent(org.eclipse.swt.
   * widgets.Event)
   */
  @Override
  public void handleEvent(Event event) {

    if (event.widget == addButton) {
      handleAdd();
    } else if (event.widget == removeButton) {
      handleRemove();
    } else if (event.widget == setDataPathButton) {
      handleSetDataPath();
    } else if (event.type == SWT.MouseDown && event.button == 3) {
      handleTableContextMenuRequest(event);
    } else if (event.type == SWT.MouseHover && !bDisableToolTipHelp) {
      handleTableHoverHelp(event);
    } else if (event.type == SWT.KeyUp) {
      if (event.character == SWT.DEL) {
        handleRemove();
      }
    } else if (event.widget == importTable && event.type == SWT.Selection) {
      enable();
    }
  }

  /**
   * Handle remove.
   */
  public void handleRemove() {
    int nSelectedIndex = importTable.getSelectionIndex();

    Import[] oldImports = getModelImportArray();

    setModelImportArray((Import[]) Utility.removeElementFromArray(oldImports,
            oldImports[nSelectedIndex], Import.class));

    if (!isValidImport("Error Removing Import", "An error was caused by removing an import.")) {
      setModelImportArray(oldImports);
      // no refresh here: the remove action didn't happen, so leave the GUI alone
      return;
    }

    refresh();
    importTable.setSelection(nSelectedIndex - 1);
    enable();
    finishImportChangeAction();
    setFileDirty();
  }

  /**
   * Handle set data path.
   */
  private void handleSetDataPath() {
    CommonInputDialog dialog = new CommonInputDialog(this, "Set DataPath",
            "The DataPath is a series of locations which will be used when looking up imports and external resources.\nEnter a series of absolute path names, separated by the character used to separate classpath names on this platform.",
            CommonInputDialog.ALLOK, CDEpropertyPage.getDataPath(editor.getProject()));
    if (dialog.open() == Window.CANCEL)
      return;
    CDEpropertyPage.setDataPath(editor.getProject(), dialog.getValue());
  }

  /**
   * Handle add.
   */
  private void handleAdd() {
    Shell shell = getSection().getShell();
    MultiResourceSelectionDialog dialog = new MultiResourceSelectionDialog(shell,
            editor.getFile().getProject().getParent(),
            "Select one or more descriptor files to import:", editor.getFile().getLocation(),
            editor);
    dialog.setTitle("Import File(s) Selection");
    if (dialog.open() == Window.CANCEL)
      return;
    // results is an array of either IFile or File objects
    // depending on if the file was from the Eclipse Workspace or from the file system
    Object[] results = dialog.getResult();

    if (results == null || results.length == 0)
      return;

    if (!addImports(results, dialog.isImportByName))
      return;

    refresh();
    finishImportChangeAction();
    setFileDirty();

  }

  /**
   * Called with either byLocation non-null or byName non-null Adds multiple (by location) or one
   * (by name).
   *
   * @param locations
   *          objects returned from dialog
   * @param isByName
   *          true if imports should be done by name
   * @return false if any import caused an error, true of all OK
   */
  public boolean addImports(Object[] locations, boolean isByName) {
    Import[] currentImports = getModelImportArray();
    Import imp;

    int nCountCurrentImports = (currentImports == null) ? 0 : currentImports.length;
    int numberToAdd = locations.length;

    Import[] newImports = new Import[nCountCurrentImports + numberToAdd];
    if (null != currentImports)
      System.arraycopy(currentImports, 0, newImports, 0, nCountCurrentImports);

    for (int i = 0; i < locations.length; i++) {
      FileAndShortName fsn = new FileAndShortName(locations[i]);
      imp = createImport(fsn.fileName, isByName);
      if (alreadyImported(imp))
        return false;
      newImports[nCountCurrentImports + i] = imp;
    }

    setModelImportArray(newImports);

    if (!isValidImport("Error Adding Import(s)",
            "An error was caused by adding Import(s); operation cancelled.  Please correct the error and retry.")) {
      setModelImportArray(currentImports);
      return false;
    }
    return true;
  }

  /**
   * Already imported.
   *
   * @param imp
   *          the imp
   * @return true, if successful
   */
  private boolean alreadyImported(Import imp) {
    String currentFileBeingEdited = editor.getFile().getLocation().toString();
    currentFileBeingEdited = editor.getDescriptorRelativePath(currentFileBeingEdited);
    if (currentFileBeingEdited.equals(imp.getLocation())) {
      Utility.popMessage("Error - importing self", MessageFormat.format(
              "The import {0} is the same as the current file being edited. A file can''t be imported into itself.",
              new Object[] { imp.getLocation() }), MessageDialog.ERROR);
      return true;
    }

    Import[] currentImports = getModelImportArray();
    if (null == currentImports)
      return false;
    for (int i = 0; i < currentImports.length; i++) {
      if (currentImports[i].equals(imp)) {
        Utility.popMessage("Error - duplicate import",
                MessageFormat.format("The import {0} is already present",
                        new Object[] { null != imp.getName() ? imp.getName() : imp.getLocation() }),
                MessageDialog.ERROR);
        return true;
      }
    }
    return false;
  }

  /*
   * (non-Javadoc)
   * 
   * @see org.apache.uima.taeconfigurator.editors.ui.AbstractSection#enable()
   */
  @Override
  public void enable() {
    int nSelectionIndex = importTable.getSelectionIndex();
    boolean addEnable = (this instanceof TypeImportSection) ? (!isAggregate()) : true;

    addButton.setEnabled(addEnable);
    removeButton.setEnabled(nSelectionIndex > -1);
  }

  /**
   * Handle table context menu request.
   *
   * @param event
   *          the event
   */
  private void handleTableContextMenuRequest(Event event) {
    TableItem item = importTable.getItem(new Point(event.x, event.y));
    int nSelectedIndex = getIndex(item);
    bDisableToolTipHelp = true;
    requestPopUpOverImport(getModelImportArray()[nSelectedIndex], importTable, event);
    bDisableToolTipHelp = false;
  }

  /**
   * Handle table hover help.
   *
   * @param event
   *          the event
   */
  private void handleTableHoverHelp(Event event) {
    TableItem item = importTable.getItem(new Point(event.x, event.y));
    if (null != item) {
      String sDesc;

      long lCurrentTimeInMillis = System.currentTimeMillis();
      if (item == lastTableHoverItem
              && lCurrentTimeInMillis - lLastTableHoverMillis < TABLE_HOVER_REQUERY_TIME) {
        sDesc = sLastTableHoverHelp;
      } else {
        int itemIndex = (event.y - importTable.getHeaderHeight()) / importTable.getItemHeight();
        String thisFile = item.getText(1);

        sDesc = thisFile + ' ';
        Import[] importItems = getModelImportArray();
        if (itemIndex < 0 || itemIndex >= importItems.length) {
          System.err.println("***ERROR Item index hover out of range" + itemIndex
                  + ", size of array = " + importItems.length);
          System.err.println(this.getClass().getName());
          return;
        }
        Import importItem = getModelImportArray()[itemIndex];
        // if by location, it's relative to the descriptor.
        String absolutePath = editor.getAbsolutePathFromImport(importItem);

        String description = null;
        try {
          description = getDescriptionFromImport(absolutePath);
        } catch (InvalidXMLException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
        if ((null != description) && (!description.equals(""))) {
          sDesc += "\n\n" + parseToFitInToolTips(description);
        }

        lastTableHoverItem = item;
        lLastTableHoverMillis = System.currentTimeMillis();
        sLastTableHoverHelp = sDesc;
      }

      importTable.setToolTipText(sDesc);
    } else {
      importTable.setToolTipText("");
    }
  }

  /**
   * Export importable part.
   *
   * @param xmlStartElement
   *          first element exported
   * @param partTemplate
   *          the part template
   */
  protected void exportImportablePart(String xmlStartElement, String partTemplate) {
    String xmlEndElement = xmlStartElement.replaceFirst("<", "</");
    ExportImportablePartDialog dialog = new ExportImportablePartDialog(this);
    if (dialog.open() == Window.CANCEL)
      return;
    PrintWriter printWriter = setupToPrintFile(dialog.genFilePath);
    if (null != printWriter) {
      String wholeModel = editor.prettyPrintModel();
      int start = wholeModel.indexOf(xmlStartElement);
      int end = wholeModel.lastIndexOf(xmlEndElement);
      if (start < 0 || end < 0)
        throw new InternalErrorCDE("invalid state");
      start += xmlStartElement.length();
      printWriter.println(MessageFormat.format(partTemplate,
              new Object[] { dialog.baseFileName, wholeModel.substring(start, end) + "\n" }));
      printWriter.close();
      clearModelBaseValue();

      setFileDirty(); // do as soon as file changes, in case later error aborts processing
      Import imp = createImport(dialog.genFilePath, dialog.isImportByName);
      setModelImportArray(
              (Import[]) Utility.addElementToArray(getModelImportArray(), imp, Import.class));
      isValidImport("Error Exporting a part and Importing it",
              "An unexpected error was caused by the export operation");
      refresh();
      Object file = editor.getIFileOrFile(dialog.genFilePath);
      if (file instanceof IFile) {
        try {
          IFile ifile = (IFile) file;
          ifile.refreshLocal(1, null);
          ((IFile) file).setPersistentProperty(
                  new QualifiedName(PLUGIN_ID, IMPORTABLE_PART_CONTEXT),
                  editor.getFile().getLocation().toString());
        } catch (CoreException e) {
          throw new InternalErrorCDE("unexpected exception", e);
        }
      }

    }
  }

}
