/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Patrick Chuong (Texas Instruments) - Improve usability of the breakpoint view (Bug 238956)
 *****************************************************************/
package org.eclipse.debug.internal.ui.views.breakpoints;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.Platform;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.internal.ui.breakpoints.provisional.IBreakpointContainer;
import org.eclipse.debug.internal.ui.breakpoints.provisional.IBreakpointOrganizer;
import org.eclipse.debug.internal.ui.breakpoints.provisional.OtherBreakpointCategory;
import org.eclipse.debug.internal.ui.model.elements.ElementContentProvider;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelDelta;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.ModelDelta;
import org.eclipse.debug.ui.IDebugUIConstants;

/**
 * This class contains the list of container or a list of breakpoint, elements are sorted according to rules
 * in the comparator.
 */
public class BreakpointContainer extends ElementContentProvider implements IAdaptable, IBreakpointContainer {
	/**
	 * Child breakpoints - inserting new element into this collection should use the insertBreakpoint method
	 */
	final private List<IBreakpoint> fBreakpoints = new ArrayList<>();

    /**
     * Child containers - inserting new element into this container should use the insertChildContainer method
     */
	final private List<BreakpointContainer> fChildContainers = new ArrayList<>();

    /**
     * The category for this container
     */
    private IAdaptable fCategory;

    /**
     * The breakpoint organizer for this container
     */
    private IBreakpointOrganizer fOrganizer;

    /**
     * The nested breakpoint organizer
     */
    private IBreakpointOrganizer[] fNesting;

    /**
     * A flag to indicate this is the default container
     */
    private boolean fDefaultContainer;

    /**
     * Parent container
     */
    private BreakpointContainer fParent;

    /**
     * The comparator, will be use to compare the order for inserting new element into this container
     */
    private ElementComparator fComparator;

    /**
     * Constructor, intended to be call when creating the root container.
     *
     * @param organizers the breakpoint organizer for this container
     * @param comparator the element comparator, can be <code>null</code>. If <code>null</code> than new element
     * will be added to the end of the list.
     */
    public BreakpointContainer(IBreakpointOrganizer[] organizers, ElementComparator comparator) {
    	fNesting = organizers;
    	fComparator = comparator;
    }

    /**
     * Constructor, intended to be call within this class only.
     *
     * @param parent the parent breakpoint container
     * @param category the category for this container
     * @param organizer the organizer for this container
     * @param comparator the element comparator
     * @param nesting the nested breakpoint organizer
     */
    private BreakpointContainer(BreakpointContainer parent, IAdaptable category, IBreakpointOrganizer organizer,
    		ElementComparator comparator, IBreakpointOrganizer[] nesting) {
    	this(category, organizer, nesting);
    	fParent = parent;
    	fComparator = comparator;
    }

    /**
     * Constructor, intended to be call when reorganizing the content.
     *
     * @param category the breakpoint category
     * @param organizer the breakpoint organizer
     * @param nesting the nested breakpoint organizer
     */
    BreakpointContainer(IAdaptable category, IBreakpointOrganizer organizer, IBreakpointOrganizer[] nesting) {
    	fCategory = category;
    	fOrganizer = organizer;
    	fNesting = nesting;
    }

    /**
     * Initialize the default containers.
     *
     * @param parentDelta the parent delta, addition child delta will be added to the parent
     */
    public void initDefaultContainers(ModelDelta parentDelta) {
    	// seed with all nested categories
    	if (fNesting != null && fNesting.length > 0) {
    		IAdaptable[] emptyCategories = fNesting[0].getCategories();
    		if (emptyCategories != null) {
    			for (int i = 0; i < emptyCategories.length; i++) {
    				IAdaptable empty = emptyCategories[i];
    				BreakpointContainer container = findExistingContainer(fChildContainers, empty);
    				if (container == null) {
    					IBreakpointOrganizer[] siblings = new IBreakpointOrganizer[fNesting.length - 1];
    					System.arraycopy(fNesting, 1, siblings, 0, siblings.length);
    					container = new BreakpointContainer(this, empty, fNesting[0], fComparator, siblings);
    					insertChildContainer(container);
    					container.fDefaultContainer = true;

    					int size = container.getChildren().length;
    					parentDelta.addNode(container, fChildContainers.indexOf(container), IModelDelta.INSTALL|IModelDelta.ADDED|IModelDelta.EXPAND, size);

    				}
    			}
    		}
    	}
    }

    /**
     * Insert the breakpoint to this container.
     *
     * @param breakpoint the new breakpoint
     * @return the index of the breakpoint in the cache, -1 if the breakpoint already exist
     */
    private int insertBreakpoint(IBreakpoint breakpoint) {
    	if (fBreakpoints.contains(breakpoint) || breakpoint == null) {
    		return -1;
    	}
    	int index = fBreakpoints.size();
    	for (; fComparator != null && index > 0; index--) {
    		if (fComparator.compare(fBreakpoints.get(index-1), breakpoint) < 0) {
				break;
			}
    	}
    	if (index < 0) {
    		index = 0;
    	}
    	fBreakpoints.add(index, breakpoint);
    	return index;
    }

    /**
     * Insert the child container this container.
     *
     * @param container the child container
     * @return the index of the container in the cache, -1 if the child container already exist
     */
    private int insertChildContainer(BreakpointContainer container) {
    	int index = fChildContainers.size();
    	for (; fComparator != null && index > 0; index--) {
    		if (fComparator.compare(fChildContainers.get(index-1), container) < 0) {
				break;
			}
    	}

    	if (index < 0) {
			index = 0;
		}
    	fChildContainers.add(index, container);

    	return index;
    }


    /**
     * Returns the element comparator.
     *
     * @return the element comparator
     */
    public ElementComparator getElementComparator() {
    	return fComparator;
    }

    /**
     * Returns the parent container, can be <code>null</code>.
     *
     * @return the parent container
     */
    public BreakpointContainer getParent() {
    	return fParent;
    }

    /**
     * Determine whether there is any nested container.
     *
     * @return true if has nested container
     */
    private boolean hasNesting() {
    	return fNesting != null && fNesting.length > 0;
    }

    /**
     * Get the categories for the breakpoint with the given organizer.
     *
     * @param breakpoint the breakpoint
     * @param organizer the organizer
     * @return the categories
     */
    private static IAdaptable[] getCategories(IBreakpoint breakpoint, IBreakpointOrganizer organizer) {
    	IAdaptable[] categories = organizer.getCategories(breakpoint);
    	if (categories == null || categories.length == 0) {
			categories = OtherBreakpointCategory.getCategories(organizer);
		}
    	return categories;
    }

    /**
     * Find existing breakpoint container in the container array the given category.
     *
     * @param containers the container array
     * @param category the category
     * @return the breakpoint container, can be <code>null</code>.
     */
	private static BreakpointContainer findExistingContainer(List<BreakpointContainer> containers, IAdaptable category) {
		for (BreakpointContainer c : containers) {
    		if (category.equals(c.getCategory())) {
				return c;
			}
    	}
    	return null;
    }

    // TODO [pchuong]: can be remove if BreakpointsContentProvider no longer uses this class
    void addBreakpoint(IBreakpoint breakpoint) {
    	addBreakpoint(breakpoint, new ModelDelta(null, IModelDelta.NO_CHANGE));
    }

    /**
     * Add a breakpoint to the container, additional delta will be added to the root delta.
     *
     * @param breakpoint the breakpoint to added
     * @param rootDelta the root delta of this container
     * @see #removeBreakpoint
     */
    public void addBreakpoint(IBreakpoint breakpoint, ModelDelta rootDelta) {
    	final int bpIndex = insertBreakpoint(breakpoint);
    	if (bpIndex < 0) {
			return;
		}

        if (hasNesting()) {
            IBreakpointOrganizer organizer = fNesting[0];

            // get the breakpoint categories from the organizer
            IAdaptable[] categories = getCategories(breakpoint, organizer);

            for (int i = 0; i < categories.length; ++i) {
            	ModelDelta childDelta = null;
            	IAdaptable category = categories[i];
            	BreakpointContainer container = findExistingContainer(fChildContainers, category);

            	// create a new container if it doesn't exist
            	if (container == null) {
            		IBreakpointOrganizer[] nesting = null;
            		if (fNesting.length > 1) {
            			 nesting = new IBreakpointOrganizer[fNesting.length - 1];
                         System.arraycopy(fNesting, 1, nesting, 0, nesting.length);
            		}
            		container = new BreakpointContainer(this, category, organizer, fComparator, nesting);
            		insertChildContainer(container);
            		childDelta = rootDelta.addNode(container, fChildContainers.indexOf(container), IModelDelta.INSERTED|IModelDelta.INSTALL, -1);

            	} else {
					childDelta = rootDelta.addNode(container, fChildContainers.indexOf(container), IModelDelta.STATE, -1);
            	}

           		container.addBreakpoint(breakpoint, childDelta);
           		childDelta.setChildCount(container.getChildren().length);
            }

        } else {
        	// TODO [pchuong]: There seems to be some kind of problem when the INSERTED flag is used,
        	//				   there is a additional checkbox added to the end of the tree.
        	//				   Also the tree seems to have a strange visual effect when using the INSERTED
        	//				   flag for the child node instead of ADDED flag. Note: all breakpoint delta
        	//				   is using the ADDED flag in this class.
       		rootDelta.addNode(breakpoint, bpIndex, IModelDelta.ADDED|IModelDelta.INSTALL, 0);
       		// rootDelta.addNode(breakpoint, bpIndex, IModelDelta.INSERTED|IModelDelta.INSTALL, 0);

        	rootDelta.setFlags(rootDelta.getFlags() | IModelDelta.EXPAND);
        }
    }

    /**
     * Remove a breakpoint from the container, additional delta will be added to the root delta.
     *
     * @param breakpoint the breakpoint to remove
     * @param rootDelta the root delta of this container
     * @return if the breakpoint was successfully removed
     * @see #addBreakpoint
     */
    public boolean removeBreakpoint(IBreakpoint breakpoint, ModelDelta rootDelta) {
    	boolean removed = fBreakpoints.remove(breakpoint);

    	if (removed) {
    		boolean addRemoveBpDelta = getContainers().length == 0;

			Iterator<BreakpointContainer> it = fChildContainers.iterator();
    		while (it.hasNext()) {
    			BreakpointContainer container = it.next();
				// if the breakpoint contains in the container and it is the only breakpoint,
				// than remove the container from the collection
    			if (container.contains(breakpoint)) {
    				ModelDelta childDelta = null;
    				if ((!container.isDefaultContainer()) && (container.getBreakpoints().length <= 1)) {
	    				it.remove();
	    				childDelta = rootDelta.addNode(container, IModelDelta.REMOVED|IModelDelta.UNINSTALL);

	    			} else {
	    				childDelta = rootDelta.addNode(container, IModelDelta.STATE);
	    			}
    				// remove the breakpoint from the nested containers
	    			container.removeBreakpoint(breakpoint, childDelta);
    			}
    		}

    		if (addRemoveBpDelta) {
    			rootDelta.addNode(breakpoint, IModelDelta.REMOVED|IModelDelta.UNINSTALL);
    		}
    	}
    	return removed;
    }

    /**
     * A helper method to copy the organizers between two containers.
     *
     * @param destContainer the destination container
     * @param sourceContainer the source container
     */
    public static void copyOrganizers(BreakpointContainer destContainer, BreakpointContainer sourceContainer) {
    	destContainer.fNesting = sourceContainer.fNesting;
    	destContainer.fOrganizer = sourceContainer.fOrganizer;
		destContainer.fCategory = sourceContainer.fCategory;
    }

    /**
     * A helper method to update the breakpoint cache of the container and it's ancestors.
     *
     * @param container the breakpoint container
     * @param breakpoints the breakpoint to update
     * @param add true if breakpoint should be added to the cache, otherwise remove the breakpoint from the cache
     */
	private static void updateSelfAndAncestorsBreakpointCache(BreakpointContainer container, List<IBreakpoint> breakpoints, boolean add) {
    	if (container != null) {
    		container.fBreakpoints.removeAll(breakpoints);
    		if (add) {
				container.fBreakpoints.addAll(breakpoints);
			}
    		updateSelfAndAncestorsBreakpointCache(container.getParent(), breakpoints, add);
    	}
    }

    /**
     * A helper method to add a breakpoint to an existing container.
     *
     * @param destContainer the destination container
     * @param breakpoint the breakpoint to add
     * @param destContainerDelta the destination container delta, additional delta will be added to this delta
     */
    static public void addBreakpoint(BreakpointContainer destContainer, IBreakpoint breakpoint, ModelDelta destContainerDelta) {
    	int index = destContainer.insertBreakpoint(breakpoint);
    	Assert.isTrue(index >= 0);

		List<IBreakpoint> breakpoints = destContainer.fBreakpoints;
    	destContainerDelta.addNode(breakpoint, index/*breakpoints.indexOf(breakpoint)*/, IModelDelta.ADDED|IModelDelta.INSTALL, 0);
    	destContainerDelta.setFlags(destContainerDelta.getFlags() | IModelDelta.EXPAND);

    	// add the breakpoints to the parent containers.
    	updateSelfAndAncestorsBreakpointCache(destContainer.getParent(), breakpoints, true);
    }

    /**
     * A helper method to add a child container to an existing container.
     *
     * @param destContainer the destination container
     * @param sourceContainer the source container
     * @param destContainerDelta the delta of the destination container, additional delta will be added to this delta
     */
    static public void addChildContainer(BreakpointContainer destContainer, BreakpointContainer sourceContainer, ModelDelta destContainerDelta) {
    	destContainer.insertChildContainer(sourceContainer);
    	sourceContainer.fParent = destContainer;

    	// add the breakpoints to the parent containers.
		List<IBreakpoint> breakpoints = Arrays.asList(sourceContainer.getBreakpoints());
    	updateSelfAndAncestorsBreakpointCache(destContainer, breakpoints, true);

    	int index = destContainer.fChildContainers.indexOf(sourceContainer);
    	int size = sourceContainer.getChildren().length;
    	ModelDelta childDelta  = destContainerDelta.addNode(sourceContainer, index, IModelDelta.INSERTED|IModelDelta.INSTALL|IModelDelta.EXPAND, size);

    	appendContainerDelta(sourceContainer, childDelta);
    }

    /**
     * A helper method to append delta to the breakpoint container. This method is used by addContainer only.
     *
     * @param container the container to append child delta
     * @param containerDelta the delta of the breakpoint container, additional delta will be added to this delta
     */
    static private void appendContainerDelta(BreakpointContainer container, ModelDelta containerDelta) {
    	Object[] children = container.getChildren();
    	for (int i = 0; i < children.length; ++i) {
    		boolean isBreakpoint = children[0] instanceof IBreakpoint;
    		int numChild =  isBreakpoint ? 0 : children.length;
    		int flag = isBreakpoint ? IModelDelta.ADDED|IModelDelta.INSTALL
    				: IModelDelta.INSERTED|IModelDelta.INSTALL|IModelDelta.EXPAND;
    		ModelDelta childDelta = containerDelta.addNode(children[i], i, flag, numChild);

    		if (children[i] instanceof BreakpointContainer) {
    			BreakpointContainer childContainer = (BreakpointContainer) children[i];
    			appendContainerDelta(childContainer, childDelta);
    		}
    	}
    }

    /**
     * A helper method to remove the breakpoint from the container.
     *
     * @param container the container to remove the breakpoint
     * @param breakpoint the breakpoint to remove
     * @param containerDelta the delta of the breakpoint container, additional delta will be added to this delta
     */
    static public void removeBreakpoint(BreakpointContainer container, IBreakpoint breakpoint, ModelDelta containerDelta) {
    	container.removeBreakpoint(breakpoint, containerDelta);
		List<IBreakpoint> breakpoints = new ArrayList<>();
    	breakpoints.add(breakpoint);
    	updateSelfAndAncestorsBreakpointCache(container.getParent(), breakpoints, false);
    }

    /**
     * Remove all child elements including the given container itself.
     *
     * @param container the breakpoint container
     * @param delta the parent delta
     */
    static public void removeAll(BreakpointContainer container, ModelDelta delta) {
    	BreakpointContainer parent = container.getParent();
    	if (parent != null) {
    		parent.fChildContainers.remove(container);
    		delta = delta.addNode(container, IModelDelta.UNINSTALL|IModelDelta.REMOVED);
    	}

    	if (container.fChildContainers.size() == 0) {
			List<IBreakpoint> breakpoints = new ArrayList<>();
			Iterator<IBreakpoint> iterator = container.fBreakpoints.iterator();
    		while (iterator.hasNext()) {
				IBreakpoint obj = iterator.next();
				breakpoints.add(obj);
				delta.addNode(obj, IModelDelta.UNINSTALL|IModelDelta.REMOVED);
				iterator.remove();
    		}

	    	// remove the breakpoints from the parent containers.
			updateSelfAndAncestorsBreakpointCache(container.getParent(), breakpoints, false);
    		return;
    	}

		Iterator<BreakpointContainer> iterator = container.fChildContainers.iterator();
    	while (iterator.hasNext()) {
    		BreakpointContainer childContainer = iterator.next();
    		ModelDelta childDelta = delta.addNode(childContainer, IModelDelta.REMOVED|IModelDelta.UNINSTALL);
    		iterator.remove();
    		removeAll(childContainer, childDelta);
    	}
    }

    /**
     * Returns whether this is the default container.
     *
     * @return true if it is a default container
     */
    boolean isDefaultContainer() {
    	return fDefaultContainer;
    }

    /**
     * Returns the breakpoints in this container
     *
     * @return the breakpoints in this container
     */
    @Override
	public IBreakpoint[] getBreakpoints() {
        return fBreakpoints.toArray(new IBreakpoint[fBreakpoints.size()]);
    }

    /**
     * Returns this container's category.
     *
     * @return container category
     */
    @Override
	public IAdaptable getCategory() {
        return fCategory;
    }

    /**
     * Returns children as breakpoints or nested containers.
     *
     * @return children as breakpoints or nested containers
     */
    public Object[] getChildren() {
        if (fChildContainers.isEmpty()) {
            return getBreakpoints();
        }
        return getContainers();
    }

    /**
     * Returns the index of the given child element (breakpoint or container.
     *
     * @param child Child to calculate index of.
     * @return index of child
     */
    public int getChildIndex(Object child) {
        if (fChildContainers.isEmpty()) {
            return fBreakpoints.indexOf(child);
        }
        return fChildContainers.indexOf(child);
    }

    /**
     * Returns the containers nested in this container, possibly empty.
     *
     * @return the containers nested in this container, can be empty.
     */
    public BreakpointContainer[] getContainers() {
        return fChildContainers.toArray(new BreakpointContainer[fChildContainers.size()]);
    }

    /**
     * Returns this container's organizer.
     *
     * @return this container's organizer
     */
    @Override
	public IBreakpointOrganizer getOrganizer() {
        return fOrganizer;
    }

    /**
     * Returns whether this container contains the given breakpoint.
     *
     * @param breakpoint the breakpoint to check
     * @return true if this container contains the given breakpoint
     */
    @Override
	public boolean contains(IBreakpoint breakpoint) {
        return fBreakpoints.contains(breakpoint);
    }

    /**
     * Returns the child containers for the given breakpoint.
     *
     * @param breakpoint the breakpoint to get containers for
     * @return child containers
     */
    public BreakpointContainer[] getContainers(IBreakpoint breakpoint) {
        if (contains(breakpoint)) {
        	BreakpointContainer[] containers = getContainers();
            if (containers.length == 0) {
                return new BreakpointContainer[]{this};
            }
			ArrayList<BreakpointContainer> list = new ArrayList<>();
            for (int i = 0; i < containers.length; i++) {
            	BreakpointContainer container = containers[i];
            	BreakpointContainer[] subcontainers = container.getContainers(breakpoint);
                if (subcontainers != null) {
                    for (int j = 0; j < subcontainers.length; j++) {
                        list.add(subcontainers[j]);
                    }
                }
            }
            return list.toArray(new BreakpointContainer[list.size()]);
        }
        return new BreakpointContainer[0];
    }

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
        if (obj instanceof BreakpointContainer) {
        	BreakpointContainer container = (BreakpointContainer) obj;
            // With Group by "Advanced" the same category can contain a different subset of breakpoints,
            // therefore to have the same category is not enough to be equal.
            if (! (fParent != null && container.fParent != null && fParent.equals(container.fParent) ||
            		fParent == null && container.fParent == null) ) {
                return false;
            }
        	if (getCategory() != null && container.getCategory() != null) {
        		return getCategory().equals(container.getCategory());
        	} else {
        		return true;
        	}
        }
        return super.equals(obj);
	}

    /*
     * (non-Javadoc)
     * @see org.eclipse.debug.internal.ui.model.elements.ElementContentProvider#getChildCount(java.lang.Object, org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext, org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdate)
     */
	@Override
	protected int getChildCount(Object element, IPresentationContext context, IViewerUpdate monitor) throws CoreException {
		return getChildren().length;
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.debug.internal.ui.model.elements.ElementContentProvider#getChildren(java.lang.Object, int, int, org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext, org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdate)
	 */
	@Override
	protected Object[] getChildren(Object parent, int index, int length, IPresentationContext context, IViewerUpdate monitor) throws CoreException {
		return getElements(getChildren(), index, length);
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.debug.internal.ui.model.elements.ElementContentProvider#supportsContextId(java.lang.String)
	 */
	@Override
	protected boolean supportsContextId(String id) {
		return id.equals(IDebugUIConstants.ID_BREAKPOINT_VIEW);
	}

	/*
	 * (non-Javadoc)
	 * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
	 */
	@Override
	public <T> T getAdapter(Class<T> adapter) {
		return Platform.getAdapterManager().getAdapter(this, adapter);
	}
}
