/**
 * Copyright (C) 2001-2017 by RapidMiner and the contributors
 * 
 * Complete list of developers available at our web site:
 * 
 * http://rapidminer.com
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Affero General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License along with this program.
 * If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.processeditor;

import com.rapidminer.RapidMiner;
import com.rapidminer.gui.tools.CamelCaseFilter;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.operator.ProcessRootOperator;
import com.rapidminer.tools.GroupTree;
import com.rapidminer.tools.OperatorService;
import com.rapidminer.tools.OperatorService.OperatorServiceListener;
import com.rapidminer.tools.documentation.OperatorDocBundle;
import com.rapidminer.tools.usagestats.ActionStatisticsCollector;

import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import java.io.Serializable;
import java.util.*;


/**
 * This is the model for the group selection tree in the new operator editor panel.
 *
 * @author Ingo Mierswa, Tobias Malbrecht, Sebastian Land
 */
public class NewOperatorGroupTreeModel implements TreeModel, OperatorServiceListener {

	/** Regular expression used as delimiter for search terms. */
	private static final String SEARCH_TERM_DELIMITER = "\\s+";

	/** Consider only the first {@value} search terms. */
	private static final int MAX_SEARCH_TERMS = 5;

	/** Compares operator descriptions based on their usage statistics. */
	private static final class UsageStatsComparator implements Comparator<OperatorDescription>, Serializable {

		private static final long serialVersionUID = 1L;

		@Override
		public int compare(OperatorDescription op1, OperatorDescription op2) {
			int usageCount1 = (int) ActionStatisticsCollector.getInstance().getCount(ActionStatisticsCollector.TYPE_OPERATOR,
					op1.getKey(), ActionStatisticsCollector.OPERATOR_EVENT_EXECUTION);
			int usageCount2 = (int) ActionStatisticsCollector.getInstance().getCount(ActionStatisticsCollector.TYPE_OPERATOR,
					op2.getKey(), ActionStatisticsCollector.OPERATOR_EVENT_EXECUTION);
			return usageCount2 - usageCount1;
		}
	}

	private static final class OperatorPriorityComparator implements Comparator<OperatorDescription> {

		@Override
		public int compare(OperatorDescription o1, OperatorDescription o2) {
			try {
				return Math.negateExact(Integer.compare(o1.getPriority(), o2.getPriority()));
			} catch (ArithmeticException e) {
				// no logging for idiotic error
				return 0;
			}
		}

	}

	private final GroupTree completeTree;

	private GroupTree displayedTree;

	private boolean filterDeprecated = true;

	private String filter = null;

	/** The list of all tree model listeners. */
	private final List<TreeModelListener> treeModelListeners = new LinkedList<>();

	private boolean sortByUsage = false;

	/** sorts by <priority> key of operators */
	private Comparator<OperatorDescription> priorityComparator = new OperatorPriorityComparator();
	/** sorts by local usage count of operators */
	private Comparator<OperatorDescription> usageComparator = new UsageStatsComparator();

    /**
     * Instantiates a new New operator group tree model.
     */
    public NewOperatorGroupTreeModel() {
		this.completeTree = OperatorService.getGroups();
		OperatorService.addOperatorServiceListener(this);

		removeHidden(completeTree);

		this.displayedTree = this.completeTree;
		this.filterDeprecated = true;
		updateTree();
	}

    /**
     * Sets filter deprecated.
     *
     * @param filterDeprecated the filter deprecated
     */
    public void setFilterDeprecated(boolean filterDeprecated) {
		this.filterDeprecated = filterDeprecated;
		updateTree();
	}

	@Override
	public void addTreeModelListener(TreeModelListener l) {
		treeModelListeners.add(l);
	}

    /**
     * Contains boolean.
     *
     * @param o the o
     * @return the boolean
     */
    public boolean contains(Object o) {
		return contains(this.getRoot(), o);
	}

	private boolean contains(Object start, Object o) {
		if (o.equals(start)) {
			return true;
		} else {
			for (int i = 0; i < getChildCount(start); i++) {
				if (contains(getChild(start, i), o)) {
					return true;
				}
			}
		}
		return false;
	}

	@Override
	public Object getChild(Object parent, int index) {
		if (parent instanceof GroupTree) {
			GroupTree tree = (GroupTree) parent;
			int numSubGroups = tree.getSubGroups().size();
			if (index < numSubGroups) {
				return tree.getSubGroup(index);
			} else {
				return tree.getOperatorDescriptions().get(index - numSubGroups);
			}
		} else {
			return null;
		}

	}

	@Override
	public int getChildCount(Object parent) {
		if (parent instanceof GroupTree) {
			GroupTree tree = (GroupTree) parent;
			return tree.getSubGroups().size() + tree.getOperatorDescriptions().size();
		} else {
			return 0;
		}
	}

	@Override
	public int getIndexOfChild(Object parent, Object child) {
		GroupTree tree = (GroupTree) parent;
		if (child instanceof GroupTree) {
			return tree.getIndexOfSubGroup((GroupTree) child);
		} else {
			return tree.getOperatorDescriptions().indexOf(child) + tree.getSubGroups().size();
		}
	}

	@Override
	public Object getRoot() {
		return displayedTree;
	}

	@Override
	public boolean isLeaf(Object node) {
		return !(node instanceof GroupTree);
	}

	@Override
	public void removeTreeModelListener(TreeModelListener l) {
		treeModelListeners.remove(l);
	}

	/** Will be invoked after editing changes of nodes. */
	@Override
	public void valueForPathChanged(TreePath path, Object node) {
		fireTreeChanged(node, path);
	}

	private void fireTreeChanged(Object source, TreePath path) {
		Iterator<TreeModelListener> i = treeModelListeners.iterator();
		while (i.hasNext()) {
			i.next().treeStructureChanged(new TreeModelEvent(source, path));
		}
	}

	private void fireCompleteTreeChanged(Object source) {
		Iterator<TreeModelListener> i = treeModelListeners.iterator();
		while (i.hasNext()) {
			i.next().treeStructureChanged(new TreeModelEvent(this, new TreePath(getRoot())));
		}
	}

    /**
     * Apply filter list.
     *
     * @param filter the filter
     * @return the list
     */
    public List<TreePath> applyFilter(String filter) {
		this.filter = filter;
		return updateTree();
	}

    /**
     * Update tree list.
     *
     * @return the list
     */
    public List<TreePath> updateTree() {
		// int hits = Integer.MAX_VALUE;
		GroupTree filteredTree = this.completeTree.clone();
		if (!"true".equals(System.getProperty(RapidMiner.PROPERTY_DEVELOPER_MODE))) {
			removeDeprecatedGroup(filteredTree);
		}
		List<TreePath> expandedPaths = new LinkedList<>();
		if (filter != null && filter.trim().length() > 0) {
			String[] terms = filter.trim().split(SEARCH_TERM_DELIMITER, MAX_SEARCH_TERMS);
			if (terms.length > 1) {
				Arrays.setAll(terms, i -> terms[i].toLowerCase());
				removeFilteredInstances(terms, filteredTree, expandedPaths, new TreePath(getRoot()));
			} else {
				CamelCaseFilter ccFilter = new CamelCaseFilter(filter);
				removeFilteredInstances(ccFilter, filteredTree, expandedPaths, new TreePath(getRoot()));
			}
		}
		if (filterDeprecated) {
			removeDeprecated(filteredTree);
		}
		this.displayedTree = filteredTree;
		filteredTree.sort(priorityComparator);

		if (sortByUsage) {
			filteredTree.sort(usageComparator);
		}

		fireCompleteTreeChanged(this);
		return expandedPaths;
	}

    /**
     * Gets non deprecated group tree.
     *
     * @param tree the tree
     * @return the non deprecated group tree
     */
    public GroupTree getNonDeprecatedGroupTree(GroupTree tree) {
		GroupTree filteredTree = tree.clone();
		removeDeprecated(filteredTree);
		return filteredTree;
	}

	private void removeHidden(GroupTree tree) {
		Iterator<? extends GroupTree> g = tree.getSubGroups().iterator();
		while (g.hasNext()) {
			GroupTree child = g.next();
			removeHidden(child);
			if (child.getAllOperatorDescriptions().size() == 0) {
				g.remove();
			}
		}
		Iterator<OperatorDescription> o = tree.getOperatorDescriptions().iterator();
		while (o.hasNext()) {
			OperatorDescription description = o.next();
			if (description.getOperatorClass().equals(ProcessRootOperator.class)) {
				o.remove();
			}
		}
	}

	private void removeDeprecatedGroup(GroupTree tree) {
		Iterator<? extends GroupTree> g = tree.getSubGroups().iterator();
		while (g.hasNext()) {
			GroupTree child = g.next();
			if (child.getKey().equals("deprecated")) {
				g.remove();
			} else {
				removeDeprecatedGroup(child);
			}
		}
	}

	private int removeDeprecated(GroupTree tree) {
		int hits = 0;
		Iterator<? extends GroupTree> g = tree.getSubGroups().iterator();
		while (g.hasNext()) {
			GroupTree child = g.next();
			hits += removeDeprecated(child);
			if (child.getAllOperatorDescriptions().size() == 0) {
				g.remove();
			}
		}
		Iterator<OperatorDescription> o = tree.getOperatorDescriptions().iterator();
		while (o.hasNext()) {
			OperatorDescription description = o.next();
			if (description.isDeprecated()) {
				o.remove();
			} else {
				hits++;
			}
		}
		return hits;
	}

	/**
	 * Recursively deletes nodes from the filteredTree when neither the name of the node nor the
	 * name of one of its children or parents matches the filter. Stores paths to nodes that match
	 * the filter (not containing the node itself) in expandedPaths.
	 *
	 * @param filter
	 *            filter for which names to take
	 * @param filteredTree
	 *            the tree to filter
	 * @param expandedPaths
	 *            list of paths that should be expanded when the tree is displayed
	 * @param path
	 *            the current path
	 * @return number of hits below the current path
	 */
	private int removeFilteredInstances(CamelCaseFilter filter, GroupTree filteredTree, List<TreePath> expandedPaths,
			TreePath path) {
		int hits = 0;
		Iterator<? extends GroupTree> g = filteredTree.getSubGroups().iterator();
		while (g.hasNext()) {
			GroupTree child = g.next();
			boolean matches = filter.matches(child.getName());
			if (matches) {
				expandedPaths.add(path);
			}

			hits += removeFilteredInstances(filter, child, expandedPaths, path.pathByAddingChild(child));
			if (child.getAllOperatorDescriptions().size() == 0 && !matches) {
				g.remove();
			}

		}

		// remove non matching operator descriptions if the group does not match, keep all in
		// matching group, count hits even if matching
		boolean groupMatches = filter.matches(filteredTree.getName());
		Iterator<OperatorDescription> o = filteredTree.getOperatorDescriptions().iterator();
		while (o.hasNext()) {
			OperatorDescription description = o.next();
			boolean matches = filter.matches(description.getName()) || filter.matches(description.getShortName());
			if (!matches) {
				for (String tag : description.getTags()) {
					matches = filter.matches(tag);
					if (matches) {
						break;
					}
				}
			}
			if (!filterDeprecated) {
				for (String replaces : description.getReplacedKeys()) {
					matches |= filter.matches(replaces);
				}
			}
			if (!matches && !groupMatches) {
				o.remove();
			} else {
				hits++;
			}
		}

		if (hits > 0) {
			expandedPaths.add(path);
		}
		return hits;
	}

    /**
     * Sets sort by usage.
     *
     * @param sort the sort
     */
    public void setSortByUsage(boolean sort) {
		if (sort != this.sortByUsage) {
			this.sortByUsage = sort;
			updateTree();
		}
	}

	private int removeFilteredInstances(String[] terms, GroupTree filteredTree, List<TreePath> expandedPaths,
			TreePath path) {
		int hits = 0;

		Iterator<? extends GroupTree> g = filteredTree.getSubGroups().iterator();
		while (g.hasNext()) {
			GroupTree child = g.next();
			String lowerCaseName = child.getName().toLowerCase();
			boolean matches = true;
			for (String term : terms) {
				if (!lowerCaseName.contains(term)) {
					matches = false;
					break;
				}
			}

			if (matches) {
				expandedPaths.add(path);
			}

			hits += removeFilteredInstances(terms, child, expandedPaths, path.pathByAddingChild(child));
			if (child.getAllOperatorDescriptions().size() == 0 && !matches) {
				g.remove();
			}
		}

		// remove non matching operator descriptions if the group does not match, keep all in
		// matching group, count hits even if matching
		boolean groupMatches = true;
		String lowerCaseName = filteredTree.getName().toLowerCase();
		for (String term : terms) {
			if (!lowerCaseName.contains(term)) {
				groupMatches = false;
				break;
			}
		}

		Iterator<OperatorDescription> o = filteredTree.getOperatorDescriptions().iterator();
		while (o.hasNext()) {
			OperatorDescription description = o.next();

			boolean matches = true;
			for (String term : terms) {
				// check names
				if (description.getName().toLowerCase().contains(term)) {
					continue;
				}
				if (description.getShortName().toLowerCase().contains(term)) {
					continue;
				}
				// check tags
				boolean foundTag = false;
				for (String tag : description.getTags()) {
					if (tag.toLowerCase().contains(term)) {
						foundTag = true;
						break;
					}
				}
				if (foundTag) {
					continue;
				}
				// replaced keys
				boolean foundReplacedKey = false;
				if (!filterDeprecated) {
					for (String replaces : description.getReplacedKeys()) {
						replaces.toLowerCase().contains(term);
						foundReplacedKey = true;
						break;
					}
				}
				if (foundReplacedKey) {
					continue;
				}
				// term not found
				matches = false;
				break;
			}

			if (!matches && !groupMatches) {
				o.remove();
			} else {
				hits++;
			}
		}

		if (hits > 0) {
			expandedPaths.add(path);
		}

		return hits;
	}

	@Override
	public void operatorRegistered(OperatorDescription description, OperatorDocBundle bundle) {
		updateTree();
	}

	@Override
	public void operatorUnregistered(OperatorDescription description) {
		updateTree();
	}
}
