/*******************************************************************************
 * Copyright (c) 2006, 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.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.debug.service.IBreakpoints.IBreakpointsTargetDMContext;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.cdt.dsf.ui.viewmodel.AbstractVMAdapter;
import org.eclipse.cdt.dsf.ui.viewmodel.AbstractVMProvider;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMModelProxy;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMNode;
import org.eclipse.cdt.dsf.ui.viewmodel.datamodel.IDMVMContext;
import org.eclipse.cdt.dsf.ui.viewmodel.datamodel.RootDMVMNode;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IBreakpointsListener;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.internal.ui.breakpoints.provisional.IBreakpointOrganizer;
import org.eclipse.debug.internal.ui.breakpoints.provisional.IBreakpointUIConstants;
import org.eclipse.debug.internal.ui.breakpoints.provisional.OtherBreakpointCategory;
import org.eclipse.debug.internal.ui.elements.adapters.DefaultBreakpointsViewInput;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IColumnPresentation;
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.IViewerInputUpdate;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.debug.ui.IBreakpointOrganizerDelegate;
import org.eclipse.debug.ui.contexts.DebugContextEvent;
import org.eclipse.debug.ui.contexts.IDebugContextListener;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.ui.IWorkbenchWindow;

/**
 * The breakpoint provider is used to populate the contents of the breakpoints 
 * view.
 * 
 * @since 2.1
 */ 
public class BreakpointVMProvider extends AbstractVMProvider 
{
    private IPropertyChangeListener fPresentationContextListener = new IPropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent event) {
            handleEventInExecThread(event);
        }        
    };

    private IBreakpointsListener fBreakpointsListener = new IBreakpointsListener() {
        @Override
        public void breakpointsRemoved(IBreakpoint[] breakpoints, IMarkerDelta[] deltas) {
            handleEventInExecThread(new BreakpointsChangedEvent(BreakpointsChangedEvent.Type.REMOVED, breakpoints));
        }
        
        @Override
        public void breakpointsChanged(IBreakpoint[] breakpoints, IMarkerDelta[] deltas) {
            handleEventInExecThread(new BreakpointsChangedEvent(BreakpointsChangedEvent.Type.CHANGED, breakpoints));
        }
        
        @Override
        public void breakpointsAdded(IBreakpoint[] breakpoints) {
            handleEventInExecThread(new BreakpointsChangedEvent(BreakpointsChangedEvent.Type.ADDED, breakpoints));
        }
        
    };
 
    private IDebugContextListener fDebugContextListener = new IDebugContextListener() {
        @Override
        public void debugContextChanged(final DebugContextEvent event) {
            handleEventInExecThread(event);
        }
    };
    
    private class ContainerBreakpointsCache extends DataCache<List<BreakpointOrganizerVMContext>> {
        
        private BreakpointOrganizerVMNode fOrganizerVMNode;
        private TreePath fParentPath; 
        
        public ContainerBreakpointsCache(BreakpointOrganizerVMNode organizerVMNode, TreePath parentPath) {
            super(getExecutor());
            fOrganizerVMNode = organizerVMNode;
            fParentPath = parentPath;
        }
        
        @Override
        protected void retrieve(final DataRequestMonitor<List<BreakpointOrganizerVMContext>> rm) {
            getNestingCategoryBreakpoints(
                fParentPath, 
                new DataRequestMonitor<IBreakpoint[]>(getExecutor(), rm) {
                    @SuppressWarnings({ "cast", "unchecked" })
                    @Override
                    protected void handleSuccess() {
                        Map<IAdaptable, List<IBreakpoint>> bpsLists = new HashMap<IAdaptable, List<IBreakpoint>>();
                        for (IBreakpoint bp : getData()) {
                            IAdaptable[] bpCategories = fOrganizerVMNode.getOrganizer().getCategories(bp);
                            if (bpCategories == null || bpCategories.length == 0) {
                                bpCategories = OtherBreakpointCategory.getCategories(fOrganizerVMNode.getOrganizer());
                            } 
                            
                            for (IAdaptable category : bpCategories) {
                                List<IBreakpoint> categoryBPs = bpsLists.get(category);
                                if (categoryBPs == null) {
                                    categoryBPs = new ArrayList<IBreakpoint>();
                                    bpsLists.put(category, categoryBPs);
                                }
                                categoryBPs.add(bp);
                            }
                        }
                        
                        // Only show the empty containers for the top-level node.
                        if (fParentPath.getSegmentCount() == 0) {
                            final IAdaptable[] independentCategories = fOrganizerVMNode.getOrganizer().getCategories();
                            if (independentCategories != null) {
                                for (IAdaptable category : independentCategories) {
                                    if (!bpsLists.containsKey(category)) {
                                        bpsLists.put(category, (List<IBreakpoint>)Collections.EMPTY_LIST);
                                    }
                                }
                            }
                        }
                        
                        List<BreakpointOrganizerVMContext> vmcs = new ArrayList<BreakpointOrganizerVMContext>(bpsLists.size());
                        for (Map.Entry<IAdaptable, List<IBreakpoint>> entry : bpsLists.entrySet()) {
                            List<IBreakpoint> bpsList = entry.getValue();  
                            IBreakpoint[] bpsArray =  bpsList.toArray(new IBreakpoint[bpsList.size()]);
                            
                            vmcs.add(createBreakpointOrganizerVMContext(fOrganizerVMNode, entry.getKey(), bpsArray));                                
                        }
                        Comparator<Object> comparator = (Comparator<Object>)getPresentationContext().getProperty(
                        		IBreakpointUIConstants.PROP_BREAKPOINTS_ELEMENT_COMPARATOR);
                        if (comparator != null) {
                            Collections.sort(vmcs, comparator);
                        }
                        
                        rm.setData(vmcs);
                        rm.done();
                    }
                });
        }
    };

    private final Map<TreePath, ContainerBreakpointsCache> fContainerBreakpointsCacheMap = 
        new HashMap<TreePath, ContainerBreakpointsCache>();

    private DataCache<IBreakpoint[]> fFilteredBreakpointsCache = new DataCache<IBreakpoint[]>(getExecutor()) {
        @Override
        protected void retrieve(org.eclipse.cdt.dsf.concurrent.DataRequestMonitor<IBreakpoint[]> rm) {
            calcFileteredBreakpoints(rm);
        }
    };
    
    public BreakpointVMProvider(AbstractVMAdapter adapter, IPresentationContext context) {
        super(adapter, context);
        
        // Create the top level node which provides the anchor starting point.
        // This node is referenced by the BreakpointVMInput element so it 
        // should not change when the view layout is updated.
        setRootNode(new RootDMVMNode(this));
        // Configure the rest of the layout nodes.
        configureLayout();
        
        context.addPropertyChangeListener(fPresentationContextListener);
        DebugPlugin.getDefault().getBreakpointManager().addBreakpointListener(fBreakpointsListener);
        IWorkbenchWindow window = context.getWindow();
        if (window != null) {
            DebugUITools.getDebugContextManager().getContextService(window).addDebugContextListener(
                fDebugContextListener);
        }
        
    }

    @Override
    protected IVMModelProxy createModelProxyStrategy(Object rootElement) {
        return new BreakpointVMModelProxyStrategy(this, rootElement);
    }
    
    protected IVMNode createBreakpointVMNode() {
        return new BreakpointVMNode(this);
    }
    
    /**
     * Configures the nodes of this provider.  This method may be over-ridden by
     * sub classes to create an alternate configuration in this provider.
     */
    protected void configureLayout() {
        IBreakpointOrganizer[] organizers = (IBreakpointOrganizer[])
            getPresentationContext().getProperty(IBreakpointUIConstants.PROP_BREAKPOINTS_ORGANIZERS);
        
        IVMNode parentNode = getRootVMNode();
        if (organizers != null) {
            for (IBreakpointOrganizer organizer : organizers) {
                IVMNode organizerNode = new BreakpointOrganizerVMNode(this, organizer);
                addChildNodes(parentNode, new IVMNode[] {organizerNode});
                parentNode = organizerNode;
            }
        }
        
        IVMNode bpsNode = createBreakpointVMNode();
        addChildNodes(parentNode, new IVMNode[] {bpsNode});
    }
    

    @Override
    public void dispose() {
        getPresentationContext().removePropertyChangeListener(fPresentationContextListener);
        DebugPlugin.getDefault().getBreakpointManager().removeBreakpointListener(fBreakpointsListener);
        IWorkbenchWindow window = getPresentationContext().getWindow();
        if (window != null) {
            DebugUITools.getDebugContextManager().getContextService(window).removeDebugContextListener(
                fDebugContextListener);
        }        
        super.dispose();
    }
    
    @Override
    public IColumnPresentation createColumnPresentation(IPresentationContext context, Object element) {
        return null;
    }
    
    @Override
    public String getColumnPresentationId(IPresentationContext context, Object element) {
        return null;
    }

    @Override
    public void update(IViewerInputUpdate update) {
        IDMContext activeDMContext = null;
        if (update.getElement() instanceof IDMVMContext) {
            activeDMContext = ((IDMVMContext)update.getElement()).getDMContext();
            activeDMContext = DMContexts.getAncestorOfType(activeDMContext, IBreakpointsTargetDMContext.class);
        }
        if (activeDMContext != null) {
            update.setInputElement(new BreakpointVMInput(getRootVMNode(), activeDMContext));
        } else {
            // If no breakpoints target found in active context, delegate the breakpoint
            // presentation to the default: breakpoint manager.
            update.setInputElement(new DefaultBreakpointsViewInput(update.getPresentationContext()));
        }
        update.done();
    }
    
    public void getNestingCategoryBreakpoints(TreePath path, final DataRequestMonitor<IBreakpoint[]> rm) {
        BreakpointOrganizerVMContext nestingOrganizerVmc = null;
        while (path.getSegmentCount() > 0) {
            if (path.getLastSegment() instanceof BreakpointOrganizerVMContext) {
                nestingOrganizerVmc = (BreakpointOrganizerVMContext)path.getLastSegment();
                break;
            }
            path = path.getParentPath();
        }
        if (nestingOrganizerVmc == null) {
            getFileteredBreakpoints(rm);
        } else {
            final BreakpointOrganizerVMContext _nestingOrganizerVmc = nestingOrganizerVmc;
            getBreakpointOrganizerVMCs(
                (BreakpointOrganizerVMNode)_nestingOrganizerVmc.getVMNode(), path.getParentPath(),
                new DataRequestMonitor<List<BreakpointOrganizerVMContext>>(getExecutor(), rm) {
                    @Override
                    protected void handleSuccess() {
                        int newVmcIdx = getData().indexOf(_nestingOrganizerVmc);
                        
                        if (newVmcIdx >= 0) {
                            rm.setData(getData().get(newVmcIdx).getBreakpoints());
                        } else {
                            rm.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.INVALID_HANDLE, "Breakpoint category not found", null)); //$NON-NLS-1$
                        }
                        rm.done();
                    }
                });
        }
    }
    
    public void getBreakpointOrganizerVMCs(BreakpointOrganizerVMNode organizerVMNode, TreePath path, 
        DataRequestMonitor<List<BreakpointOrganizerVMContext>> rm) 
    {
        ContainerBreakpointsCache cache = fContainerBreakpointsCacheMap.get(path);
        if (cache == null) {
            cache = new ContainerBreakpointsCache(organizerVMNode, path);
            fContainerBreakpointsCacheMap.put(path, cache);
        }
        
        cache.request(rm);
    }


    protected BreakpointOrganizerVMContext createBreakpointOrganizerVMContext(BreakpointOrganizerVMNode node, IAdaptable category, IBreakpoint[] breakpoints) {
        return new BreakpointOrganizerVMContext(node, category, breakpoints);
    }
    

    public void getFileteredBreakpoints(final DataRequestMonitor<IBreakpoint[]> rm) {
        fFilteredBreakpointsCache.request(rm);
    }
    
    protected void calcFileteredBreakpoints(DataRequestMonitor<IBreakpoint[]> rm) {
        rm.setData(DebugPlugin.getDefault().getBreakpointManager().getBreakpoints());
        rm.done();
    }

    public void getBreakpointsForDebugContext(ISelection debugContext, DataRequestMonitor<IBreakpoint[]> rm) {
        rm.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.NOT_SUPPORTED, "Not supported", null)); //$NON-NLS-1$
        rm.done();
    }
    
    public void handleEventInExecThread(final Object event) {
        getExecutor().execute(new DsfRunnable() {
            @Override
            public void run() {
                handleEvent(event);
            }
        });
    }

    @Override
    public void handleEvent(Object event, RequestMonitor rm) {
        if (isPresentationContextEvent(event)) {
            PropertyChangeEvent propertyEvent = (PropertyChangeEvent)event;
            if (IBreakpointUIConstants.PROP_BREAKPOINTS_ORGANIZERS.equals(propertyEvent.getProperty())) 
            {
                clearNodes(false);
                configureLayout();
            } 
        } 
        super.handleEvent(event, rm);
    }

    public static boolean isPresentationContextEvent(Object event) {
        return event instanceof PropertyChangeEvent && ((PropertyChangeEvent)event).getSource() instanceof IPresentationContext;
    }

    public static boolean isBreakpointOrganizerEvent(Object event) {
        return event instanceof PropertyChangeEvent && ((PropertyChangeEvent)event).getSource() instanceof IBreakpointOrganizerDelegate;
    }

    private static final int MODEL_DELTA_CHANGE_FLAGS = IModelDelta.STATE | IModelDelta.CONTENT | IModelDelta.ADDED | IModelDelta.REMOVED | IModelDelta.REPLACED | IModelDelta.INSERTED; 
    
    @Override
    protected void handleEvent(IVMModelProxy proxyStrategy, Object event, RequestMonitor rm) {
        // Before generating a delta, flush the caches.
        int deltaFlags = proxyStrategy.getEventDeltaFlags(event);
        if ((deltaFlags & MODEL_DELTA_CHANGE_FLAGS) != 0) {
            flushCaches();
        }
        
        super.handleEvent(proxyStrategy, event, rm);
    }
    
    private void flushCaches() {
        fFilteredBreakpointsCache.reset();
        for (DataCache<?> cache : fContainerBreakpointsCacheMap.values()) {
            cache.reset();
        }
        fContainerBreakpointsCacheMap.clear();
    }
}
