/*
 * 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.caseditor.editor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.uima.cas.CAS;
import org.apache.uima.cas.Type;
import org.apache.uima.cas.TypeSystem;
import org.apache.uima.cas.text.AnnotationFS;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

/**
 * This is a lightweight popup dialog which creates an annotation of the chosen type.
 */
class QuickTypeSelectionDialog extends PopupDialog {

  /** The editor. */
  private final AnnotationEditor editor;

  /** The filter text. */
  private Text filterText;

  /** The shortcut type map. */
  private Map<Character, Type> shortcutTypeMap = new HashMap<>();

  /** The type shortcut map. */
  private Map<Type, Character> typeShortcutMap = new HashMap<>();

  /**
   * Initializes the current instance.
   *
   * @param parent
   *          the parent
   * @param editor
   *          the editor
   */
  QuickTypeSelectionDialog(Shell parent, AnnotationEditor editor) {
    super(parent, PopupDialog.INFOPOPUPRESIZE_SHELLSTYLE, true, true, true, false, true, null,
            null);

    this.editor = editor;

    // key shortcuts are assigned automatically to types, the shortcut
    // mapping may change if the type system is modified

    String shortcutsString = "qwertzuiopasdfghjklyxcvbnm1234567890";

    Set<Character> shortcuts = new HashSet<>();

    for (int i = 0; i < shortcutsString.length(); i++) {
      shortcuts.add(shortcutsString.charAt(i));
    }

    List<Type> types = new ArrayList<>();
    Collections.addAll(types, getTypes());
    types.sort(new Comparator<Type>() {
      @Override
      public int compare(Type o1, Type o2) {
        return o1.getName().compareTo(o2.getName());
      }
    });

    // Try to create mappings with first letter of the type name as shortcut
    for (Iterator<Type> it = types.iterator(); it.hasNext();) {

      Type type = it.next();

      String name = type.getShortName();

      Character candidateChar = Character.toLowerCase(name.charAt(0));

      if (shortcuts.contains(candidateChar)) {
        putShortcut(candidateChar, type);

        shortcuts.remove(candidateChar);
        it.remove();
      }
    }

    // Try to create mappings with second letter of the type name as shortcut
    for (Iterator<Type> it = types.iterator(); it.hasNext();) {

      Type type = it.next();

      String name = type.getShortName();

      if (name.length() > 2) {
        Character candidateChar = Character.toLowerCase(name.charAt(1));

        if (shortcuts.contains(candidateChar)) {
          putShortcut(candidateChar, type);

          shortcuts.remove(candidateChar);
          it.remove();
        }
      }
    }

    // Now assign letters to the remaining types
    for (Iterator<Type> it = types.iterator(); it.hasNext() && shortcuts.size() > 0;) {

      Character candidateChar = shortcuts.iterator().next();

      putShortcut(candidateChar, it.next());

      shortcuts.remove(candidateChar);
      it.remove();
    }
  }

  /**
   * Put shortcut.
   *
   * @param shortcut
   *          the shortcut
   * @param type
   *          the type
   */
  private void putShortcut(Character shortcut, Type type) {
    shortcutTypeMap.put(shortcut, type);
    typeShortcutMap.put(type, shortcut);
  }

  /**
   * Gets the types.
   *
   * @return the types
   */
  private Type[] getTypes() {

    TypeSystem typeSystem = editor.getDocument().getCAS().getTypeSystem();

    List<Type> types = typeSystem
            .getProperlySubsumedTypes(typeSystem.getType(CAS.TYPE_NAME_ANNOTATION));

    types.add(typeSystem.getType(CAS.TYPE_NAME_ANNOTATION));

    return types.toArray(new Type[types.size()]);
  }

  /**
   * Annotate and close.
   *
   * @param annotationType
   *          the annotation type
   */
  private void annotateAndClose(Type annotationType) {
    if (annotationType != null) {
      Point textSelection = editor.getSelection();

      AnnotationFS annotation = editor.getDocument().getCAS().createAnnotation(annotationType,
              textSelection.x, textSelection.y);

      editor.getDocument().addFeatureStructure(annotation);

      if (annotation.getType().equals(editor.getAnnotationMode())) {
        editor.setAnnotationSelection(annotation);
      }
    }

    QuickTypeSelectionDialog.this.close();
  }

  @Override
  protected Control createDialogArea(Composite parent) {
    Composite composite = (Composite) super.createDialogArea(parent);

    // TODO: focus always goes to the text box, but should
    // go to the Tree control, find out why, can SWT.NO_FOCUS be used
    // to fix it ?

    filterText = new Text(composite, SWT.NO_FOCUS);
    filterText.setBackground(parent.getBackground());
    filterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

    Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL | SWT.LINE_DOT);
    separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

    final TreeViewer typeTree = new TreeViewer(composite, SWT.SINGLE | SWT.V_SCROLL);
    typeTree.getControl()
            .setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL));

    typeTree.getControl().setFocus();

    filterText.addKeyListener(new KeyListener() {

      @Override
      public void keyPressed(KeyEvent e) {
        if (e.keyCode == SWT.ARROW_DOWN || e.keyCode == SWT.ARROW_UP) {
          typeTree.getControl().setFocus();

          Tree tree = (Tree) typeTree.getControl();

          if (tree.getItemCount() > 0) {

            tree.setSelection(tree.getItem(0));
          }
        }
      }

      @Override
      public void keyReleased(KeyEvent e) {
        typeTree.refresh(false);
      }
    });

    typeTree.setContentProvider(new ITreeContentProvider() {

      @Override
      public Object[] getChildren(Object parentElement) {
        return null;
      }

      @Override
      public Object getParent(Object element) {
        return null;
      }

      @Override
      public boolean hasChildren(Object element) {
        return false;
      }

      @Override
      public Object[] getElements(Object inputElement) {
        return (Type[]) inputElement;
      }

      @Override
      public void dispose() {
      }

      @Override
      public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
      }
    });

    typeTree.setFilters(new ViewerFilter[] { new ViewerFilter() {
      @Override
      public boolean select(Viewer viewer, Object parentElement, Object element) {

        // check if the string from the filterText is contained in the type name
        Type type = (Type) element;

        return type.getName().contains(filterText.getText());
      }
    } });

    typeTree.setLabelProvider(new ILabelProvider() {

      @Override
      public Image getImage(Object element) {
        return null;
      }

      @Override
      public String getText(Object element) {

        Type type = (Type) element;

        Character key = typeShortcutMap.get(type);

        if (key != null) {
          return "[" + key + "] " + type.getShortName();
        } else {
          return type.getShortName();
        }
      }

      @Override
      public void addListener(ILabelProviderListener listener) {

      }

      @Override
      public void dispose() {
      }

      @Override
      public boolean isLabelProperty(Object element, String property) {
        return false;
      }

      @Override
      public void removeListener(ILabelProviderListener listener) {
      }
    });

    typeTree.getControl().addKeyListener(new KeyListener() {

      @Override
      public void keyPressed(KeyEvent e) {
        Type type = shortcutTypeMap.get(Character.toLowerCase(e.character));

        if (type != null) {
          annotateAndClose(type);
        }
      }

      @Override
      public void keyReleased(KeyEvent e) {
      }
    });

    typeTree.getControl().addMouseMoveListener(new MouseMoveListener() {

      @Override
      public void mouseMove(MouseEvent e) {

        Tree tree = (Tree) typeTree.getControl();

        TreeItem item = tree.getItem(new Point(e.x, e.y));

        if (item != null) {
          tree.setSelection(item);
        }
      }
    });

    // TODO open listener needs a double click, single click should be enough
    // because there is already a selection below the mouse
    typeTree.addOpenListener(new IOpenListener() {

      @Override
      public void open(OpenEvent event) {
        StructuredSelection selection = (StructuredSelection) event.getSelection();

        annotateAndClose((Type) selection.getFirstElement());
      }
    });

    Collection<Type> shownAnnotationTypes = editor.getShownAnnotationTypes();

    Type[] types = shownAnnotationTypes.toArray(new Type[shownAnnotationTypes.size()]);

    typeTree.setInput(types);

    ISelection modeSelection = new StructuredSelection(new Object[] { editor.getAnnotationMode() });

    typeTree.setSelection(modeSelection, true);

    return composite;
  }

  @Override
  protected Point getInitialSize() {
    return new Point(250, 300);
  }
}
