/*   **********************************************************************  **
 **   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.controller.dnd;

import net.sourceforge.rssowl.controller.FavoritesTree;
import net.sourceforge.rssowl.controller.GUI;
import net.sourceforge.rssowl.controller.thread.SettingsManager;
import net.sourceforge.rssowl.model.Category;
import net.sourceforge.rssowl.model.Channel;
import net.sourceforge.rssowl.model.Favorite;
import net.sourceforge.rssowl.model.TabItemData;
import net.sourceforge.rssowl.model.TreeItemData;
import net.sourceforge.rssowl.util.GlobalSettings;
import net.sourceforge.rssowl.util.shop.ProxyShop;
import net.sourceforge.rssowl.util.shop.RegExShop;
import net.sourceforge.rssowl.util.shop.WidgetShop;

import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceAdapter;
import org.eclipse.swt.dnd.DragSourceEvent;
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.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

import java.util.Vector;

/**
 * Basic Drag and Drop support for the tree holding favorites in RSSOwl.
 * Currently favorites, categories and blogrolls may be dragged and dropped into
 * other categories. <br />
 * <br />
 * The tree is also the drop target for tabs that are dragged to create a new
 * favorite
 * 
 * @author <a href="mailto:bpasero@rssowl.org">Benjamin Pasero </a>
 * @version 1.2.3
 */
public class FavoritesTreeDND {

  /** Array of transfer types */
  private static final Transfer[] types = new Transfer[] { TextTransfer.getInstance() };

  private DragSource source;
  private DropTarget target;
  Item dragSourceItem;
  FavoritesTree rssOwlFavoritesTree;
  boolean sourceIsCategory;
  Tree tree;

  /**
   * Instantiate a new FavoritesTreeDND
   * 
   * @param rssOwlFavoritesTree The favorites tree to apply basic drag and drop
   * support
   */
  public FavoritesTreeDND(FavoritesTree rssOwlFavoritesTree) {
    this.rssOwlFavoritesTree = rssOwlFavoritesTree;
    tree = rssOwlFavoritesTree.getFavoritesTree();
    initDragAndDrop();
  }

  /**
   * Dispose DragSource and DragTarget
   */
  public void dispose() {
    source.dispose();
    target.dispose();
  }

  /**
   * Get the item that is currently being dragged. This may either be a treeitem
   * or a tabitem
   * 
   * @return Item The item that is currently dragged
   */
  public Item getDragSourceItem() {
    return dragSourceItem;
  }

  /**
   * Set the item that is currently being dragged. This may either be a treeitem
   * or a tabitem.
   * 
   * @param dragSourceItem The dragged item
   */
  public void setDragSourceItem(Item dragSourceItem) {
    this.dragSourceItem = dragSourceItem;
  }

  /**
   * Create the Drag Source on the Tree. Valid drag sources are favorites,
   * categories and the root blogroll element
   */
  private void createDragSource() {

    /**
     * Drag source is using operation DROP_MOVE: A copy of the data is added to
     * the drop target and the original data is removed from the drag source
     */
    source = new DragSource(tree, DND.DROP_MOVE);
    source.setTransfer(types);
    source.addDragListener(new DragSourceAdapter() {

      /**
       * Reset Drag Source Item to NULL
       */
      public void dragFinished(DragSourceEvent event) {
        dragSourceItem = null;
      }

      /**
       * Data that is transfered in this Drag will be the text of the dragged
       * treeitem.
       */
      public void dragSetData(DragSourceEvent event) {
        TreeItemData sourceData = (TreeItemData) dragSourceItem.getData();

        /** Source is Favorite, save name as drag data */
        if (sourceData.isFavorite())
          event.data = sourceData.getName();

        /**
         * Source is Category / Root blogroll, save category path as drag data
         */
        else if (sourceData.isCategory() || sourceData.isBlogroll())
          event.data = rssOwlFavoritesTree.getTreePath((TreeItem) dragSourceItem, true);

        /** Remember if the source was a category or a favorite */
        sourceIsCategory = sourceData.isCategory() || sourceData.isBlogroll();
      }

      /**
       * Start the Drag.
       */
      public void dragStart(DragSourceEvent event) {

        /** First ensure that the Rename Box is disposed if open */
        rssOwlFavoritesTree.stopRenaming();

        /** Retrieve a valid drag source treeitem */
        TreeItem validDragSourceTreeItem = getValidDragSourceTreeItem();

        /** TreeItem is valid */
        if (validDragSourceTreeItem != null) {
          event.doit = true;
          dragSourceItem = validDragSourceTreeItem;
        }

        /** TreeItem is invalid */
        else {
          event.doit = false;
        }
      }
    });
  }

  /**
   * Create the drop target. Currently only categories and favorites are valid
   * drop targets.
   */
  private void createDropTarget() {

    /** Drop Target is supporting operations DROP_MOVE and DROP_COPY */
    target = new DropTarget(tree, DND.DROP_MOVE | DND.DROP_COPY);
    target.setTransfer(types);
    target.addDropListener(new DropTargetAdapter() {

      /**
       * Give feedback on drag over event. Only allow to drop source into
       * targets that are categories or favorites.
       */
      public void dragOver(DropTargetEvent event) {
        TreeItem targetTreeItem = (TreeItem) event.item;
        boolean isValidTarget = false;

        /**
         * User is dragging a treeitem into the tree
         */
        if (WidgetShop.isset(targetTreeItem) && WidgetShop.isset(dragSourceItem) && dragSourceItem instanceof TreeItem) {

          /** Target Data */
          TreeItemData targetData = (TreeItemData) targetTreeItem.getData();

          /** Source Data */
          TreeItemData sourceData = (TreeItemData) dragSourceItem.getData();

          /** User draggs a category or root blogroll */
          if (sourceData.isCategory() || sourceData.isBlogroll()) {

            /** Check if target is valid for the category / root blogroll */
            isValidTarget = (targetData.isCategory() || targetData.isFavorite()) && isValidTargetForCategory((TreeItem) dragSourceItem, targetTreeItem, event);
          }

          /** User draggs a favorite */
          else {

            /** Check if target is valid for the favorite */
            isValidTarget = (targetData.isCategory() || targetData.isFavorite()) && isValidTargetForFavorite((TreeItem) dragSourceItem, targetTreeItem);
          }
        }

        /**
         * User is dragging a tabitem into the tree
         */
        else if (WidgetShop.isset(targetTreeItem) && WidgetShop.isset(dragSourceItem) && dragSourceItem instanceof CTabItem) {

          /** Target Data */
          TreeItemData targetData = (TreeItemData) targetTreeItem.getData();

          /** Source Data */
          TabItemData sourceData = (TabItemData) dragSourceItem.getData();

          /** Check if target is valid for the tabitem */
          isValidTarget = (targetData.isCategory() || targetData.isFavorite());

          /** Also check Source TabItem */
          if (isValidTarget)
            isValidTarget = (sourceData.isFeed() || sourceData.isBrowser()) && !sourceData.isAggregatedCat();
        }

        /**
         * In order to support Drag and Drop from other Applications into
         * RSSOwl, also support the case that a DataType is given at all.
         */
        else if (WidgetShop.isset(targetTreeItem) && event.currentDataType != null) {

          /** Target Data */
          TreeItemData targetData = (TreeItemData) targetTreeItem.getData();

          /** Check if target is valid for the tabitem */
          isValidTarget = (targetData.isCategory() || targetData.isFavorite());
        }

        /** Target is valid, give good feedback and allow operation */
        if (isValidTarget) {
          event.feedback = DND.FEEDBACK_SCROLL | DND.FEEDBACK_SELECT | DND.FEEDBACK_EXPAND;

          /** Check if User drags a Category to Root */
          if (WidgetShop.isset(dragSourceItem) && dragSourceItem instanceof TreeItem) {

            /** Source TreeItem Data */
            TreeItemData sourceData = (TreeItemData) dragSourceItem.getData();

            /**
             * Only allow drag a Non-Root Category into Root for Categories and
             * if Target Category is a Root Element of the Favorites Tree
             */
            if ((sourceData.isCategory() || sourceData.isBlogroll()) && targetTreeItem.getParentItem() == null && ((TreeItem) dragSourceItem).getParentItem() != null) {
              Point pt = GUI.display.map(null, tree, event.x, event.y);
              Rectangle bounds = targetTreeItem.getBounds();

              /** User is dragging before a Root Element */
              if (pt.y < bounds.y + bounds.height / 3)
                event.feedback = DND.FEEDBACK_SCROLL | DND.FEEDBACK_INSERT_BEFORE;
            }
          }

          /** If Source is TreeItem: DROP_MOVE, else if CTabItem: DROP_COPY */
          event.detail = (dragSourceItem instanceof TreeItem) ? DND.DROP_MOVE : DND.DROP_COPY;
        }

        /** Target is invalid, disallow operation, but give Feedback */
        else {
          event.feedback = DND.FEEDBACK_SCROLL | DND.FEEDBACK_SELECT;
          event.detail = DND.DROP_NONE;
        }
      }

      /**
       * Drop the data into the target. If the target is a category or root
       * blogroll, the source favorite will move into it. If the target is a
       * favorite the source favorite will move into the same category.
       */
      public void drop(DropTargetEvent event) {

        /** No event data is given */
        if (event.data == null || event.item == null) {
          event.detail = DND.DROP_NONE;
          return;
        }

        /** Retrieve the target treeitem */
        TreeItem targetTreeItem = (TreeItem) event.item;

        /**
         * User is dropping a treeitem into the tree
         */
        if (dragSourceItem instanceof TreeItem) {

          /**
           * Get Drag Data. This is the text of the source treeitem if the
           * source is a favorite. It will be the catPath if source is a
           * category or a root blogroll.
           */
          String sourceTreeItemText = (String) event.data;

          /** Check if User drags to Root */
          boolean isDragToRoot = false;
          if (sourceIsCategory && targetTreeItem.getParentItem() == null && ((TreeItem) dragSourceItem).getParentItem() != null) {
            Point pt = GUI.display.map(null, tree, event.x, event.y);
            Rectangle bounds = targetTreeItem.getBounds();

            if (pt.y < bounds.y + bounds.height / 3)
              isDragToRoot = true;
          }

          /** Move the source category / root blogroll into the target category */
          if (sourceIsCategory && !isDragToRoot)
            moveCategory(sourceTreeItemText, ((TreeItemData) targetTreeItem.getData()).isCategory() ? targetTreeItem : targetTreeItem.getParentItem());

          /** Move the source category / root blogroll into the Root of the Tree */
          else if (sourceIsCategory && isDragToRoot)
            moveCategoryToRoot(sourceTreeItemText);

          /** Move the source favorite into the target category */
          else
            moveFavorite(sourceTreeItemText, ((TreeItemData) targetTreeItem.getData()).isCategory() ? targetTreeItem : targetTreeItem.getParentItem());
        }

        /**
         * User is dropping a tabitem or tableitem into the tree
         */
        else if (dragSourceItem instanceof CTabItem || dragSourceItem instanceof TableItem) {

          /**
           * Get Drag Data. This is the URL and Title of the item divided by
           * line separator as token.
           */
          String splitToken = System.getProperty("line.separator");
          String sourceItemText = (String) event.data;
          String favoriteUrl = sourceItemText.split(splitToken)[0];
          String favoriteTitle = sourceItemText.split(splitToken)[1];

          /** Create the new favorite and set use proxy if a proxy is set */
          Favorite newFavorite = new Favorite(favoriteUrl, favoriteTitle, null);
          newFavorite.setUseProxy(ProxyShop.isUseProxy());

          /** Retrieve Channel Object to grab Meta Data */
          Channel draggedChannel;
          if (dragSourceItem instanceof CTabItem)
            draggedChannel = GUI.rssOwlGui.getRSSOwlNewsTabFolder().getSelectedChannel();
          else
            draggedChannel = GUI.rssOwlGui.getFeedCacheManager().getCachedNewsfeed(favoriteUrl);

          /** Copy Meta Data */
          if (draggedChannel != null && draggedChannel.getLink().equals(newFavorite.getUrl())) {
            newFavorite.syncMetaData(draggedChannel);
            newFavorite.setUnreadNewsCount(draggedChannel.getUnreadNewsCount());
          }

          /** Apply current time as creation and last visit time */
          newFavorite.setLastVisitDate(System.currentTimeMillis());
          newFavorite.setCreationDate(System.currentTimeMillis());

          /** Create a new favorite in the target category */
          createFavorite(newFavorite, ((TreeItemData) targetTreeItem.getData()).isCategory() ? targetTreeItem : targetTreeItem.getParentItem());
        }

        /**
         * The user is dropping something different into the Tree. To be
         * compatible with a Drop that comes from outside RSSOwl, check the
         * dropped Data for URLs. If a URL is found, create a new Favorite out
         * of it.
         */
        else {

          /** Extract Links from the dropped Data */
          String data = event.data != null ? event.data.toString().trim() : "";
          Vector links = new Vector();
          RegExShop.extractLinksFromText(data, links);

          /** At least one Link was found. Create a Favorite out of it */
          if (links.size() > 0) {
            final String favoriteUrl = (String) links.get(0);

            /** Try to retrieve the Target Category */
            String catPath = rssOwlFavoritesTree.getTreePath(((TreeItemData) targetTreeItem.getData()).isCategory() ? targetTreeItem : targetTreeItem.getParentItem(), true);
            final Category selectedCat = rssOwlFavoritesTree.getSelectedCat(catPath);

            /**
             * Open the "New Favorite" dialog<br>
             * Run the Dialog in an asyncExec Runnable in order to give the
             * Drag-Source feedback that the DND Operation has finished.
             */
            GUI.display.asyncExec(new Runnable() {
              public void run() {
                if (GUI.isAlive())
                  GUI.rssOwlGui.getEventManager().actionNewFavorite(favoriteUrl, "", selectedCat);
              }
            });
          }
        }
      }
    });
  }

  /**
   * Init Drag and Drop on the tree widget
   */
  private void initDragAndDrop() {
    createDragSource();
    createDropTarget();
  }

  /**
   * Create a new favorite in the selected category
   * 
   * @param rssOwlFavorite The new favorite to create
   * @param parent The category to add the favorite
   */
  void createFavorite(Favorite rssOwlFavorite, TreeItem parent) {

    /** Determine the CatPath of target category */
    String catPath = rssOwlFavoritesTree.getTreePath(parent, true);
    if (catPath.equals(""))
      return;

    /** Create new favorite */
    rssOwlFavoritesTree.addFavorite(catPath, rssOwlFavorite.getUrl(), rssOwlFavorite.getTitle(), rssOwlFavorite);

    /** Update Favorites Tree */
    rssOwlFavoritesTree.buildFavoritesTree(true);
  }

  /**
   * This method will return the selected treeitem if its a favorite, category
   * or root blogroll. Any other selection, or no selection will return null.
   * 
   * @return TreeItem The selected treeitem or null
   */
  TreeItem getValidDragSourceTreeItem() {

    /** Get selected treeitem */
    TreeItem[] selection = tree.getSelection();

    /** User has not selected a treeitem */
    if (selection.length <= 0)
      return null;

    TreeItemData sourceData = (TreeItemData) selection[0].getData();

    /**
     * Source is a category from a blogroll. If its the root element the drag
     * source is valid
     */
    if (sourceData.isBlogroll()) {

      /** Retrieve the Category from the selection */
      String categoryPath = rssOwlFavoritesTree.getTreePath(selection[0], true);
      Category blogrollCategory = rssOwlFavoritesTree.getSelectedCat(categoryPath);

      /** The parent category must not be a blogroll */
      if (!blogrollCategory.getParent().isBlogroll())
        return selection[0];
    }

    /** TreeItem is a favorite or category and therefor valid */
    if (sourceData.isFavorite() || sourceData.isCategory())
      return selection[0];

    /** TreeItem is not a valid drag source treeitem */
    return null;
  }

  /**
   * Check wether the dragged category is dragged over a valid target category.
   * Invalid categories are: <br />
   * o Target and Source have same parents and Target is no category <br />
   * o Target and Source are equal <br />
   * o Target is Parent of Source <br />
   * o Target is a nested child of the Source <br />
   * 
   * @param source The source treeitem
   * @param target The target treeitem
   * @param event The DropTargetEvent carrying some additional Information
   * @return boolean TRUE if target is valid
   */
  boolean isValidTargetForCategory(TreeItem source, TreeItem target, DropTargetEvent event) {
    TreeItemData targetData = (TreeItemData) target.getData();
    TreeItemData sourceData = (TreeItemData) source.getData();

    /** Valid Case: Source is a Category / Blogroll and moved to the Root */
    if ((sourceData.isCategory() || sourceData.isBlogroll()) && source.getParentItem() != null && target.getParentItem() == null) {
      Point pt = GUI.display.map(null, tree, event.x, event.y);
      Rectangle bounds = target.getBounds();

      /** User is dragging before a Root Element */
      if (pt.y < bounds.y + bounds.height / 3)
        return true;
    }

    /**
     * Invalid case: Target and Source have same parents and target is no
     * category
     */
    if (target.getParentItem() == source.getParentItem() && !targetData.isCategory())
      return false;

    /** Invalid case: Target is Parent of Source */
    if (target == source.getParentItem())
      return false;

    /** Invalid case: Target is Source */
    if (target == source)
      return false;

    /** Check if target is a child of the source */
    while (target.getParentItem() != null) {

      /** Target is a nested child of the source */
      if (target.getParentItem() == source)
        return false;

      /** Proceed with parent of target */
      target = target.getParentItem();
    }
    return true;
  }

  /**
   * Check wether the dragged favorite is dragged over a valid target. Invalid
   * targets are: o Target and Source have same parents and Target is no
   * category <br />
   * o Target is Parent of Source <br />
   * 
   * @param source The source treeitem
   * @param target The target treeitem
   * @return boolean TRUE if target is valid
   */
  boolean isValidTargetForFavorite(TreeItem source, TreeItem target) {
    TreeItemData targetData = (TreeItemData) target.getData();

    /**
     * Invalid case: Target and Source have same parents and target is not a
     * category
     */
    if (target.getParentItem() == source.getParentItem() && !targetData.isCategory())
      return false;

    /** Invalid case: Target is Parent of Source */
    if (target == source.getParentItem())
      return false;

    return true;
  }

  /**
   * Move the category specified by the catPath into the treeitem
   * 
   * @param catPath Qualified path to the moved category
   * @param parent The treeitem to add the category into
   */
  void moveCategory(String catPath, TreeItem parent) {

    /** Determine Source Category */
    Category sourceCategory = rssOwlFavoritesTree.getSelectedCat(catPath);

    /** Determine Target Category */
    Category targetCategory = rssOwlFavoritesTree.getSelectedCat(rssOwlFavoritesTree.getTreePath(parent, true));

    /** Remove source category from its parent */
    sourceCategory.getParent().getSortedSubCatTitles().remove(sourceCategory.getName());
    sourceCategory.getParent().getSubCategories().remove(sourceCategory.getName());

    /** Explicitly set the new parent of the source category */
    sourceCategory.setParent(targetCategory);

    /** Add source category into target category */
    targetCategory.addCategory(sourceCategory, true);

    /** Let the target expand */
    targetCategory.setExpanded(true);

    /** Update Selection Path */
    GlobalSettings.selectedTreeItem = sourceCategory.toCatPath();

    /** Re-Built the favorites tree */
    rssOwlFavoritesTree.buildFavoritesTree(true);

    /** Request Save of Settings */
    SettingsManager.getInstance().requestSave();
  }

  /**
   * Move the category specified by the catPath to the Root of the Tree.
   * 
   * @param catPath Qualified path to the moved category
   */
  void moveCategoryToRoot(String catPath) {

    /** Determine Source Category */
    Category sourceCategory = rssOwlFavoritesTree.getSelectedCat(catPath);

    /** Determine Target Category */
    Category targetCategory = Category.getRootCategory();

    /** Remove source category from its parent */
    sourceCategory.getParent().getSortedSubCatTitles().remove(sourceCategory.getName());
    sourceCategory.getParent().getSubCategories().remove(sourceCategory.getName());

    /** Explicitly set the new parent of the source category */
    sourceCategory.setParent(targetCategory);

    /** Add source category into target category */
    targetCategory.addCategory(sourceCategory, true);

    /** Let the target expand */
    targetCategory.setExpanded(true);

    /** Update Selection Path */
    GlobalSettings.selectedTreeItem = sourceCategory.toCatPath();

    /** Re-Built the favorites tree */
    rssOwlFavoritesTree.buildFavoritesTree(true);

    /** Request Save of Settings */
    SettingsManager.getInstance().requestSave();
  }

  /**
   * Move the favorite represented by its name into the treeitem which is a
   * category.
   * 
   * @param favoriteTitle The title of the favorite
   * @param parent The category to move the favorite into
   */
  void moveFavorite(String favoriteTitle, TreeItem parent) {

    /** Determine the URL of the favorite */
    String favoriteUrl;
    if (Category.titleExists(favoriteTitle))
      favoriteUrl = Category.getLinkForTitle(favoriteTitle);
    else
      return;

    /** Determine the Favorite that will be moved */
    Favorite rssOwlFavorite;
    if (Category.getFavPool().containsKey(favoriteUrl))
      rssOwlFavorite = (Favorite) Category.getFavPool().get(favoriteUrl);
    else
      return;

    /** Determine the CatPath of the new category */
    String catPath = rssOwlFavoritesTree.getTreePath(parent, true);
    if (catPath.equals(""))
      return;

    /** Determine the Category that stores this favorite */
    Category favCategory = rssOwlFavorite.getRSSOwlCategory();

    /** Delete favorite from that category */
    favCategory.removeFavorite(favoriteTitle, false, true);

    /** Update the favorite */
    rssOwlFavoritesTree.addFavorite(catPath, favoriteUrl, favoriteTitle, rssOwlFavorite);

    /** Update Favorites Tree */
    rssOwlFavoritesTree.buildFavoritesTree(true);
  }
}