/*   **********************************************************************  **
 **   Copyright notice                                                       **
 **                                                                          **
 **   (c) 2003-2006 RSSOwl Development Team                                  **
 **   http://www.rssowl.org/                                                 **
 **                                                                          **
 **   All rights reserved                                                    **
 **                                                                          **
 **   This program and the accompanying materials are made available under   **
 **   the terms of the Eclipse Public License 1.0 which accompanies this     **
 **   distribution, and is available at:                                     **
 **   http://www.rssowl.org/legal/epl-v10.html                               **
 **                                                                          **
 **   A copy is found in the file epl-v10.html and important notices to the  **
 **   license from the team is found in the textfile LICENSE.txt distributed **
 **   in this package.                                                       **
 **                                                                          **
 **   This copyright notice MUST APPEAR in all copies of the file!           **
 **                                                                          **
 **   Contributors:                                                          **
 **     RSSOwl - initial API and implementation (bpasero@rssowl.org)         **
 **                                                                          **
 **  **********************************************************************  */

package net.sourceforge.rssowl.util.shop;

import net.sourceforge.rssowl.controller.GUI;
import net.sourceforge.rssowl.controller.MenuManager;
import net.sourceforge.rssowl.util.GlobalSettings;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.swt.widgets.Widget;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
import java.util.Vector;

/**
 * Factory class to tweak Widgets in RSSOwl
 * 
 * @author <a href="mailto:bpasero@rssowl.org">Benjamin Pasero </a>
 * @version 1.2.3
 */
public class WidgetShop {

  /**
   * Global counter for all opened non-modal Dialogs. These are Feed-Search,
   * Feed-Discovery and Feed-Validator Dialogs in RSSOwl. The number is used to
   * control the position of the next opening Dialog such as it will not overlap
   * over an existing Dialog.
   */
  public static int openDialogsCount = 0;

  /** This utility class constructor is hidden */
  private WidgetShop() {
  // Protect default constructor
  }

  /**
   * From the given Vector of words calculate all StyleRanges using the Color
   * and Font Style that is given.
   * 
   * @param textField Textfield where the text is in
   * @param words Words to be highlighted
   * @param foreground Color of the text-foreground
   * @param background Color of the text-background
   * @param fontstyle Fontstyle for the highlighted words
   * @param caseSensitive FALSE if the case of the word to highlight should be
   * ignored
   * @param underline If TRUE, underline the StyleRange
   * @return Vector A Vector containing all calculated StyleRanges
   */
  public static Vector calculateStyleRanges(StyledText textField, Vector words, Color foreground, Color background, int fontstyle, boolean caseSensitive, boolean underline) {

    /** Use Vector for the StyleRanges */
    Vector styleRanges = new Vector();

    /** Text with words to style */
    String text = textField.getText();

    /** Regard case sensitivity */
    if (!caseSensitive)
      text = textField.getText().toLowerCase();

    /** Foreach word to style */
    for (int a = 0; a < words.size(); a++) {
      int start = 0;
      String curWord = (String) words.get(a);

      /** ToLowerCase if case is regarded */
      if (!caseSensitive)
        curWord = curWord.toLowerCase();

      /** Save current position */
      int pos;

      /** For each occurance of the word in the text */
      while ((pos = text.indexOf(curWord, start)) > -1) {

        /** New stylerange for the word */
        StyleRange styleRange = new StyleRange();
        styleRange.start = pos;
        styleRange.length = (curWord.length());
        styleRange.fontStyle = fontstyle;
        styleRange.foreground = foreground;
        styleRange.background = background;
        styleRange.underline = underline;
        styleRanges.add(styleRange);

        /** Goto next words */
        start = styleRange.start + styleRange.length;
      }
    }

    return styleRanges;
  }

  /**
   * Apply a wildcard popup menu to the text. Wildcards a displayed as "[TEXT]"
   * and represent replaceable parameters.
   * 
   * @param text The control to append the menu
   * @param wildcards The wildcards to add to the menu
   */
  public static void createWildCardMenu(final Text text, String[] wildcards) {
    Menu wildCardMenu = new Menu(text);

    /** Foreach wildcards */
    for (int a = 0; a < wildcards.length; a++) {
      final String wildcard = wildcards[a];
      MenuItem menuItem = new MenuItem(wildCardMenu, SWT.POP_UP);
      menuItem.setText(wildcards[a]);
      if (!GlobalSettings.isMac())
        menuItem.setImage(PaintShop.iconBackward);
      menuItem.addSelectionListener(new SelectionAdapter() {
        public void widgetSelected(SelectionEvent e) {
          text.insert(wildcard);
        }
      });
    }
    text.setMenu(wildCardMenu);
  }

  /**
   * Get the Shell Title to be used for RSSOwl.
   * 
   * @return String The Shell's Title.
   */
  public static String getShellTitle() {
    String appendix = GlobalSettings.workOffline ? " - [" + GUI.i18n.getTranslation("MENU_WORK_OFFLINE") + "]" : "";

    /** Special pre-release Title */
    if (GlobalSettings._IS_PRE_RELEASE)
      return BrowserShop.getOwlAgent() + appendix;

    /** On Mac, do not show application description */
    if (GlobalSettings.isMac())
      return "RSSOwl" + appendix;

    /** Default */
    return "RSSOwl - RSS / RDF / Atom Newsreader" + appendix;
  }

  /**
   * Set the given StyleRanges from the Vector to the StyledText
   * 
   * @param textField Textfield where the text is in
   * @param styleRanges Vector containing StyleRanges
   */
  public static void highlightText(StyledText textField, Vector styleRanges) {

    /** Sort the StyleRanges from smallest start to largest */
    TreeSet ranges = new TreeSet(new Comparator() {
      public int compare(Object arg0, Object arg1) {
        StyleRange range1 = (StyleRange) arg0;
        StyleRange range2 = (StyleRange) arg1;

        if (range1.start < range2.start)
          return -1;
        else if (range1.start > range2.start)
          return 1;

        return 0;
      }
    });

    /** Filter out StyleRanges that collide with existing ones */
    for (int a = 0; a < styleRanges.size(); a++) {
      StyleRange range = (StyleRange) styleRanges.get(a);
      if (!collides(ranges, range))
        ranges.add(range);
    }

    /** Apply Ranges */
    try {
      textField.setStyleRanges((StyleRange[]) ranges.toArray(new StyleRange[ranges.size()]));
    } catch (IllegalArgumentException e) {
      /** Paranoia-Catch: StyledText rewritten in Eclipse 3.2 */
    }
  }

  /**
   * Check wether the given StyleRange collides with a List of styleranges.
   * 
   * @param styleRanges A List of StylerRanges.
   * @param newRange The new StyleRange to check.
   * @return boolean TRUE if the given StyleRange overlaps any of the
   * StyleRanges.
   */
  private static boolean collides(TreeSet styleRanges, StyleRange newRange) {
    for (Iterator iter = styleRanges.iterator(); iter.hasNext();) {
      StyleRange range = (StyleRange) iter.next();

      if (range.start < newRange.start + newRange.length && range.start + range.length > newRange.start)
        return true;
    }
    return false;
  }

  /**
   * Change fontstyle / background / foreground of the given words adding a
   * StyleRange to the given TextField.
   * 
   * @param textField Textfield where the text is in
   * @param words Words to be highlighted
   * @param foreground Color of the text-foreground
   * @param background Color of the text-background
   * @param fontstyle Fontstyle for the highlighted words
   * @param caseSensitive FALSE if the case of the word to highlight should be
   * ignored
   * @param underline If TRUE, underline the StyleRange
   */
  public static void highlightText(StyledText textField, Vector words, Color foreground, Color background, int fontstyle, boolean caseSensitive, boolean underline) {
    highlightText(textField, calculateStyleRanges(textField, words, foreground, background, fontstyle, caseSensitive, underline));
  }

  /**
   * Set Mnemonics to the given Array of Buttons.
   * 
   * @param buttons The Buttons
   */
  public static void initMnemonics(Button buttons[]) {

    /** First check if mnemonics should be displayed */
    if (!GlobalSettings.shouldShowMnemonics())
      return;

    /** Store chars that have been used as mnemonic */
    ArrayList chars = new ArrayList();

    /** For each Button */
    for (int a = 0; a < buttons.length; a++) {
      String name = buttons[a].getText();

      /** Replace any & that are existing */
      name = name.replaceAll("&", "");

      /** For each char in the name */
      for (int b = 0; b < name.length(); b++) {

        /** Check if char is available and no whitespace */
        if (name.substring(b, b + 1) != null && !name.substring(b, b + 1).equals(" ")) {

          /** Check if char has been used as mnemonic before */
          if (!chars.contains(name.substring(b, b + 1).toLowerCase())) {

            /** Create mnemonic */
            StringBuffer buttonText = new StringBuffer(name.substring(0, b));
            buttonText.append("&").append(name.substring(b, name.length()));

            /** Set mnemonic */
            buttons[a].setText(buttonText.toString());

            /** Add char as used mnemonic */
            chars.add(name.substring(b, b + 1).toLowerCase());
            break;
          }
        }
      }
    }
  }

  /**
   * Set Mnemonics to the given Array of ToolItems.
   * 
   * @param items The ToolItems
   */
  public static void initMnemonics(ToolItem items[]) {

    /** First check if mnemonics should be displayed */
    if (!GlobalSettings.shouldShowMnemonics())
      return;

    /** Store chars that have been used as mnemonic */
    ArrayList chars = new ArrayList();

    /** For each Button */
    for (int a = 0; a < items.length; a++) {
      String name = items[a].getText();

      /** Replace any & that are existing */
      name = name.replaceAll("&", "");

      /** For each char in the name */
      for (int b = 0; b < name.length(); b++) {

        /** Check if char is available and no whitespace */
        if (name.substring(b, b + 1) != null && !name.substring(b, b + 1).equals(" ")) {

          /** Check if char has been used as mnemonic before */
          if (!chars.contains(name.substring(b, b + 1).toLowerCase())) {

            /** Create mnemonic */
            StringBuffer itemText = new StringBuffer(name.substring(0, b));
            itemText.append("&").append(name.substring(b, name.length()));

            /** Set mnemonic */
            items[a].setText(itemText.toString());

            /** Add char as used mnemonic */
            chars.add(name.substring(b, b + 1).toLowerCase());
            break;
          }
        }
      }
    }
  }

  /**
   * Check the given widget for being NULL or disposed. Return false in that
   * case.
   * 
   * @param widget The widget to check
   * @return boolean TRUE if the widget is alive
   */
  public static boolean isset(Widget widget) {
    return (widget != null && !widget.isDisposed());
  }

  /**
   * Check the given Object for being a valid Widget
   * 
   * @param obj The Object to check
   * @return boolean TRUE if the Object is a widget and alive
   */
  public static boolean isWidget(Object obj) {
    return (obj != null && obj instanceof Widget && isset((Widget) obj));
  }

  /**
   * Setup Drop Support for the given Text Widget. This allows to drop Data via
   * TextTransfer into the Widget. Some checks are made, before inserting the
   * Text. If a Link is supplied, it will be preferred.<br>
   * <br>
   * After successfull Drop, the Runnable will be started, if supplied.
   * 
   * @param text The Text Widget to enable Drop Support.
   * @param runOnDrop The Runnable to be run after successfull Drop, or NULL.
   */
  public static void setupDropSupport(final Text text, final Runnable runOnDrop) {

    /** Allow to Drop Text into Text Widget */
    final DropTarget dropTarget = new DropTarget(text, DND.DROP_MOVE);
    dropTarget.setTransfer(new Transfer[] { TextTransfer.getInstance() });
    dropTarget.addDropListener(new DropTargetAdapter() {
      public void drop(DropTargetEvent event) {

        /** Some Text is given */
        if (event.data != null && StringShop.isset(event.data.toString())) {

          /** First try to Extract Links */
          Vector links = new Vector();
          RegExShop.extractLinksFromText(event.data.toString(), links);
          if (links.size() > 0)
            text.setText(links.get(0).toString());

          /** No links given, just display the Text */
          else {
            String data = event.data.toString();

            /** This is a dragged TreeItem Category */
            if (data.indexOf(StringShop.CAT_TOKENIZER) >= 0) {
              String dataArray[] = data.split(StringShop.CAT_TOKENIZER);
              text.setText(dataArray[dataArray.length - 1]);
            }

            /** This is any other dragged Object containing Text */
            else {
              text.setText(org.jdom.Text.normalizeString(data));
            }
          }

          /** Transfer Focus into Text Widget */
          text.setFocus();

          /**
           * Start Runnable if supplied<br>
           * <br>
           * We have to make sure that no Exception is leaving this Method,
           * because on some OS this can result in a system-hang. Therefor catch
           * any Exception.
           */
          if (runOnDrop != null) {
            try {
              runOnDrop.run();
            } catch (Exception e) {
              GUI.logger.log("setupDropSupport()", e);
            }
          }
        }

        /** No valid Data given */
        else {
          event.feedback = DND.DROP_NONE;
        }
      }
    });

    /** Dispose DND Target on Text Disposal */
    text.addDisposeListener(new DisposeListener() {
      public void widgetDisposed(DisposeEvent e) {
        dropTarget.dispose();
      }
    });
  }

  /**
   * Tweak the Text widget with adding listeners to call the selectAll() Method.
   * The Method is called on MouseDoubleClick and on CTRL+A / CMD+A pressed.
   * 
   * @param text The Text widget to tweak
   */
  public static void tweakTextWidget(final Text text) {

    /** Check Widget */
    if (!isset(text))
      return;

    /** MouseDoubleClick Event */
    text.addMouseListener(new MouseAdapter() {
      public void mouseDoubleClick(MouseEvent e) {
        text.selectAll();
      }
    });

    /** KeyPressed Event */
    text.addKeyListener(new KeyAdapter() {
      public void keyPressed(KeyEvent e) {
        if (((e.stateMask & SWT.CTRL) != 0 || (e.stateMask & SWT.COMMAND) != 0) && (e.keyCode == 'a' || e.keyCode == 'A'))
          text.selectAll();
      }
    });

    /** Update Edit on Focus Gained */
    text.addFocusListener(new FocusAdapter() {
      public void focusGained(FocusEvent e) {
        MenuManager.handleEditMenuState();
      }
    });
  }
}