/*******************************************************************************
 * Copyright (c) 2008, 2010 Wind River Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.dsf.debug.ui.viewmodel.breakpoints;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.ui.concurrent.ViewerDataRequestMonitor;
import org.eclipse.cdt.dsf.ui.viewmodel.AbstractVMNode;
import org.eclipse.cdt.dsf.ui.viewmodel.ModelProxyInstalledEvent;
import org.eclipse.cdt.dsf.ui.viewmodel.VMChildrenUpdate;
import org.eclipse.cdt.dsf.ui.viewmodel.VMDelta;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.internal.ui.breakpoints.provisional.IBreakpointUIConstants;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IChildrenCountUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IChildrenUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IHasChildrenUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelDelta;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.debug.ui.contexts.DebugContextEvent;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.ui.IWorkbenchWindow;

/**
 * Base class for breakpoint VM Nodes.  Concrete implementations must 
 * implement the breakpoint object to be populated into the view.  
 * Also this node only implements the content provider so sub-classes
 * must also implement a label provider, element editor, etc.
 * 
 * @since 2.1
 */
public abstract class AbstractBreakpointVMNode extends AbstractVMNode {

    public AbstractBreakpointVMNode(BreakpointVMProvider provider) {
        super(provider);
    }

    /**
     * Class that creates the element object for the corresponding breakpoints.  
     * This element object will be populated in the breakpoitns view to represent
     * the given breakpoint.
     */
    abstract protected Object createBreakpiontElement(IBreakpoint bp);
    
    @Override
	public void update(final IHasChildrenUpdate[] updates) {
        for (final IHasChildrenUpdate update : updates) {
            if (!checkUpdate(update)) continue;
            ((BreakpointVMProvider)getVMProvider()).getNestingCategoryBreakpoints(
                update.getElementPath(), 
                new ViewerDataRequestMonitor<IBreakpoint[]>(getExecutor(), update) {
                    @Override
                    protected void handleCompleted() {
                        if (isSuccess()) {
                            update.setHasChilren(getData().length != 0);
                        } else {
                            update.setHasChilren(false);
                        }
                        update.done();
                    }
                });
        }
    }

    @Override
	public void update(final IChildrenCountUpdate[] updates) {
        for (final IChildrenCountUpdate update : updates) {
            if (!checkUpdate(update)) continue;
            ((BreakpointVMProvider)getVMProvider()).getNestingCategoryBreakpoints(
                update.getElementPath(), 
                new ViewerDataRequestMonitor<IBreakpoint[]>(getExecutor(), update) {
                    @Override
                    protected void handleCompleted() {
                        if (isSuccess()) {
                            update.setChildCount(getData().length);
                        } else {
                            update.setChildCount(0);
                        }
                        update.done();
                    }
                });
        }
    }
    
    @Override
	public void update(IChildrenUpdate[] updates) {
        for (final IChildrenUpdate update : updates) {
            if (!checkUpdate(update)) continue;
            ((BreakpointVMProvider)getVMProvider()).getNestingCategoryBreakpoints(
                update.getElementPath(), 
                new ViewerDataRequestMonitor<IBreakpoint[]>(getExecutor(), update) {
                    @Override
                    protected void handleCompleted() {
                        if (isSuccess()) {
                            @SuppressWarnings("unchecked")
                            Comparator<Object> comparator = 
                                (Comparator<Object>)getVMProvider().getPresentationContext().getProperty(
                                	IBreakpointUIConstants.PROP_BREAKPOINTS_ELEMENT_COMPARATOR);
                            if (comparator != null) {
                                Arrays.sort(getData(), comparator);
                            }
                            fillUpdateWithBreakpointElements(update, getData());
                        }
                        update.done();
                    }
                });
        }
    }    

    private void fillUpdateWithBreakpointElements(IChildrenUpdate update, IBreakpoint[] bps) {
        int updateIdx = update.getOffset() != -1 ? update.getOffset() : 0;
        int endIdx = updateIdx + (update.getLength() != -1 ? update.getLength() : bps.length);
        while (updateIdx < endIdx && updateIdx < bps.length) {
            update.setChild(createBreakpiontElement(bps[updateIdx]), updateIdx);
            updateIdx++;
        }
    }
    
    @Override
	public int getDeltaFlags(Object event) {
        if (event instanceof BreakpointsChangedEvent) {
            BreakpointsChangedEvent bpChangedEvent = ((BreakpointsChangedEvent)event);
            if (BreakpointsChangedEvent.Type.ADDED.equals(bpChangedEvent.getType())) {
                return IModelDelta.CONTENT | IModelDelta.SELECT | IModelDelta.EXPAND;
            }
            return IModelDelta.CONTENT;
        }
        else if (BreakpointVMProvider.isPresentationContextEvent(event)) {
            PropertyChangeEvent propertyEvent = (PropertyChangeEvent)event;
            if (IBreakpointUIConstants.PROP_BREAKPOINTS_FILTER_SELECTION.equals(propertyEvent.getProperty()) ||
                IBreakpointUIConstants.PROP_BREAKPOINTS_ELEMENT_COMPARATOR.equals(propertyEvent.getProperty())) 
            {
                return IModelDelta.CONTENT;
            } else if (IBreakpointUIConstants.PROP_BREAKPOINTS_ORGANIZERS.equals(propertyEvent.getProperty())) {
                return IModelDelta.EXPAND | IModelDelta.CONTENT;
            } else if (IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION.equals(propertyEvent.getProperty()) &&
                       Boolean.TRUE.equals(propertyEvent.getNewValue()) )
            {
                return IModelDelta.EXPAND | IModelDelta.SELECT;
            } 
        } 
        else if (event instanceof DebugContextEvent && (((DebugContextEvent)event).getFlags() & DebugContextEvent.ACTIVATED) != 0) {
            int flags = IModelDelta.NO_CHANGE;
            if ( Boolean.TRUE.equals(getVMProvider().getPresentationContext().getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_FILTER_SELECTION)) ) {
                flags |= IModelDelta.CONTENT;
            } 
            if (Boolean.TRUE.equals(getVMProvider().getPresentationContext().getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION)) ) {
                flags |= IModelDelta.EXPAND | IModelDelta.SELECT;
            }
            return flags;
        } else if (event instanceof ModelProxyInstalledEvent) {
            // Upon model proxy installed, check whether we need to select a 
            // breakpoint in linking with Debug view
            if (Boolean.TRUE.equals(getVMProvider().getPresentationContext().getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION)) ) {
                return IModelDelta.EXPAND | IModelDelta.SELECT;
            }            
        }
        return 0;
    }

    @Override
	public void buildDelta(Object event, VMDelta parent, int nodeOffset, RequestMonitor rm) {
        if (event instanceof BreakpointsChangedEvent) {
            BreakpointsChangedEvent bpChangedEvent = ((BreakpointsChangedEvent)event);
            if (BreakpointsChangedEvent.Type.ADDED.equals(bpChangedEvent.getType())) {
                buildBreakpointAddedDelta(bpChangedEvent, parent, nodeOffset, rm);
                // Do not call rm.done() in this method!
                return;
            } else {
                parent.setFlags(parent.getFlags() | IModelDelta.CONTENT);
            }
        }
        else if (BreakpointVMProvider.isPresentationContextEvent(event)) {
            PropertyChangeEvent propertyEvent = (PropertyChangeEvent)event;
            if (IBreakpointUIConstants.PROP_BREAKPOINTS_FILTER_SELECTION.equals(propertyEvent.getProperty()) ||
            		IBreakpointUIConstants.PROP_BREAKPOINTS_ELEMENT_COMPARATOR.equals(propertyEvent.getProperty())) 
            {
                parent.setFlags(parent.getFlags() | IModelDelta.CONTENT);
            } else if (IBreakpointUIConstants.PROP_BREAKPOINTS_ORGANIZERS.equals(propertyEvent.getProperty())) {
                parent.setFlags(parent.getFlags() | IModelDelta.EXPAND | IModelDelta.CONTENT);
            } else if (IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION.equals(propertyEvent.getProperty()) &&
                       Boolean.TRUE.equals(propertyEvent.getNewValue()) )
            {
                IWorkbenchWindow window = getVMProvider().getPresentationContext().getWindow();
                if (window != null) {
                    ISelection activeContext = DebugUITools.getDebugContextManager().getContextService(window).getActiveContext();
                    buildTrackSelectionDelta(activeContext, parent, nodeOffset, rm);
                    // Do not call rm.done() in this method!
                    return;
                }
            } 
        } 
        else if (event instanceof DebugContextEvent && (((DebugContextEvent)event).getFlags() & DebugContextEvent.ACTIVATED) != 0) {
            if ( Boolean.TRUE.equals(getVMProvider().getPresentationContext().getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_FILTER_SELECTION)) ) {
                parent.setFlags(parent.getFlags() | IModelDelta.CONTENT);
            } 
            if (Boolean.TRUE.equals(getVMProvider().getPresentationContext().getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION)) ) {
                buildTrackSelectionDelta(((DebugContextEvent)event).getContext(), parent, nodeOffset, rm);
                // Do not call rm.done() in this method!
                return;
            }
        } else if (event instanceof ModelProxyInstalledEvent) {
            if (Boolean.TRUE.equals(getVMProvider().getPresentationContext().getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_TRACK_SELECTION)) ) {
                IWorkbenchWindow window = getVMProvider().getPresentationContext().getWindow();
                if (window != null) {
                    ISelection activeContext = DebugUITools.getDebugContextManager().getContextService(window).getActiveContext();
                    buildTrackSelectionDelta(activeContext, parent, nodeOffset, rm);
                    return;
                }
            }
        }
        
        rm.done();
    }

    private void buildBreakpointAddedDelta(final BreakpointsChangedEvent event, final VMDelta parent, final int nodeOffset, final RequestMonitor rm) {
        getVMProvider().updateNode(this, new VMChildrenUpdate(
            parent, getVMProvider().getPresentationContext(), -1, -1, 
            new DataRequestMonitor<List<Object>>(getExecutor(), rm) {
                @Override
                protected void handleSuccess() {
                    for (int i = 0; i < event.getBreakpoints().length; i++) {
                        IBreakpoint eventBp = event.getBreakpoints()[i];
                        int bpIndex = findBreakpointIndex(eventBp, getData());
                        if (bpIndex >= 0) {
                            // Select only the first breakpoint that was added
                            if (i == 0) {
                                parent.addNode(getData().get(bpIndex), bpIndex, IModelDelta.SELECT);
                            }
                            // For all other added breakpoints, expand the parent.
                            parent.setFlags(parent.getFlags() | IModelDelta.CONTENT | IModelDelta.EXPAND);
                        }
                    }
                    rm.done();
                }
            }));
    }
    
    protected void buildTrackSelectionDelta(ISelection debugContext, final VMDelta parent, final int nodeOffset, final RequestMonitor rm) {
        assert getVMProvider() instanceof BreakpointVMProvider;
        
        ((BreakpointVMProvider)getVMProvider()).getBreakpointsForDebugContext(
            debugContext, 
            new DataRequestMonitor<IBreakpoint[]>(getExecutor(), rm) {
                @Override
                protected void handleSuccess() {
                    if (getData().length == 0) {
                        // No breakpoints to select, we're done.
                        rm.done();
                        return;
                    }
                    final IBreakpoint[] bpsToSelect = getData();
                    
                    getVMProvider().updateNode(AbstractBreakpointVMNode.this, new VMChildrenUpdate(
                        parent, getVMProvider().getPresentationContext(), -1, -1, 
                        new DataRequestMonitor<List<Object>>(getExecutor(), rm) {
                            @Override
                            protected void handleSuccess() {
                                for (int i = 0; i < bpsToSelect.length; i++) {
                                    int bpIndex = findBreakpointIndex(bpsToSelect[i], getData());
                                    if (bpIndex >= 0) {
                                        // Select only the first breakpoint that was added
                                        if (i == 0) {
                                            parent.addNode(getData().get(bpIndex), bpIndex, IModelDelta.SELECT);
                                        }
                                        // For all other added breakpoints, expand the parent.
                                        parent.setFlags(parent.getFlags() | IModelDelta.EXPAND);
                                    }
                                }
                                rm.done();
                            }
                        }));
                }
                
                @Override
                protected void handleErrorOrWarning() {
                    rm.done();
                }
            });
    }
    
    private int findBreakpointIndex(IBreakpoint bp, List<Object> bpElements) {
        for (int j = 0; j < bpElements.size(); j++) {
            IBreakpoint elementBp = (IBreakpoint)DebugPlugin.getAdapter(bpElements.get(j), IBreakpoint.class);
            if (elementBp != null && elementBp.equals(bp)) {
                return j;
            }
        }
        return -1;
    }
}
