/*******************************************************************************
 * Copyright (c) 2008, 2013 Wind River Systems, Inc. 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
 *     Patrick Chuong (Texas Instruments) - Add support for icon overlay in the debug view (Bug 334566)
 *     Dobrin Alexiev (Texas Instruments) - user groups support  (bug 240208)   
 *******************************************************************************/
package org.eclipse.cdt.dsf.debug.ui.viewmodel.launch;

import java.util.List;
import java.util.concurrent.RejectedExecutionException;

import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
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.DataModelInitializedEvent;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.datamodel.IDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerResumedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerSuspendedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExecutionDMContext;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExecutionDMData;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExecutionDMData2;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IExitedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IStartedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.StateChangeReason;
import org.eclipse.cdt.dsf.debug.ui.viewmodel.SteppingController.SteppingTimedOutEvent;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.dsf.ui.concurrent.ViewerDataRequestMonitor;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMContext;
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.cdt.dsf.ui.viewmodel.datamodel.AbstractDMVMProvider;
import org.eclipse.cdt.dsf.ui.viewmodel.datamodel.IDMVMContext;
import org.eclipse.cdt.dsf.ui.viewmodel.properties.IElementPropertiesProvider;
import org.eclipse.cdt.dsf.ui.viewmodel.properties.IPropertiesUpdate;
import org.eclipse.cdt.dsf.ui.viewmodel.properties.LabelAttribute;
import org.eclipse.cdt.dsf.ui.viewmodel.properties.LabelColumnInfo;
import org.eclipse.cdt.dsf.ui.viewmodel.properties.LabelImage;
import org.eclipse.cdt.dsf.ui.viewmodel.properties.LabelText;
import org.eclipse.cdt.dsf.ui.viewmodel.properties.PropertiesBasedLabelProvider;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IElementLabelProvider;
import org.eclipse.debug.internal.ui.viewers.model.provisional.ILabelUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelDelta;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.debug.ui.IDebugUIConstants;

/**
 * Abstract implementation of a container view model node.
 * Clients need to implement {@link #updateLabelInSessionThread(ILabelUpdate[])}.
 * 
 * @since 1.1
 */
public abstract class AbstractContainerVMNode extends AbstractExecutionContextVMNode 
    implements IElementLabelProvider, IElementPropertiesProvider 
 {
    /**
     * The label provider delegate.  This VM node will delegate label updates to this provider
     * which can be created by sub-classes. 
     *  
     * @since 2.0
     */    
    private IElementLabelProvider fLabelProvider;
    
	public AbstractContainerVMNode(AbstractDMVMProvider provider, DsfSession session) {
		super(provider, session, IRunControl.IContainerDMContext.class);
		fLabelProvider = createLabelProvider();
	}

    /**
     * Creates the label provider delegate.  This VM node will delegate label 
     * updates to this provider which can be created by sub-classes.   
     *  
     * @return Returns the label provider for this node. 
     *  
     * @since 2.0
     */    
    protected IElementLabelProvider createLabelProvider() {
        PropertiesBasedLabelProvider provider = new PropertiesBasedLabelProvider();
        
        provider.setColumnInfo(
            PropertiesBasedLabelProvider.ID_COLUMN_NO_COLUMNS, 
            new LabelColumnInfo(new LabelAttribute[] { 
                new ExecutionContextLabelText(
                    MessagesForLaunchVM.AbstractContainerVMNode_No_columns__text_format,
                    new String[] { 
                        ExecutionContextLabelText.PROP_NAME_KNOWN, 
                        PROP_NAME,  
                        ExecutionContextLabelText.PROP_ID_KNOWN, 
                        ILaunchVMConstants.PROP_ID }), 
                new LabelText(MessagesForLaunchVM.AbstractContainerVMNode_No_columns__Error__label, new String[0]),
                new LabelImage(DebugUITools.getImageDescriptor(IDebugUIConstants.IMG_OBJS_DEBUG_TARGET_SUSPENDED)) {
                    { setPropertyNames(new String[] { ILaunchVMConstants.PROP_IS_SUSPENDED }); }
                    
                    @Override
                    public boolean isEnabled(IStatus status, java.util.Map<String,Object> properties) {
                        return Boolean.TRUE.equals(properties.get(ILaunchVMConstants.PROP_IS_SUSPENDED));
                    };
                },
                new LabelImage(DebugUITools.getImageDescriptor(IDebugUIConstants.IMG_OBJS_DEBUG_TARGET)),
            }));
        
        return provider;
    }
    
	
	@Override
	public void update(final ILabelUpdate[] updates) {
        fLabelProvider.update(updates);
    }

    /**
     * @see IElementPropertiesProvider#update(IPropertiesUpdate[])
     * 
     * @since 2.0
     */    
    @Override
	public void update(final IPropertiesUpdate[] updates) {
        try {
            getSession().getExecutor().execute(new DsfRunnable() {
                @Override
				public void run() {
                    updatePropertiesInSessionThread(updates);
                }});
        } catch (RejectedExecutionException e) {
            for (IPropertiesUpdate update : updates) {
                handleFailedUpdate(update);
            }
        }
    }

    /**
     * @since 2.0
     */
    @ConfinedToDsfExecutor("getSession().getExecutor()")
    protected void updatePropertiesInSessionThread(final IPropertiesUpdate[] updates) {
        IRunControl service = getServicesTracker().getService(IRunControl.class);
        
        for (final IPropertiesUpdate update : updates) {
            if (service == null) {
                handleFailedUpdate(update);
                continue;
            }

            IExecutionDMContext dmc = findDmcInPath(update.getViewerInput(), update.getElementPath(), IExecutionDMContext.class);
            if (dmc == null) {
                handleFailedUpdate(update);
                continue;
            }

            update.setProperty(ILaunchVMConstants.PROP_IS_SUSPENDED, service.isSuspended(dmc));
            update.setProperty(ILaunchVMConstants.PROP_IS_STEPPING, service.isStepping(dmc));
            
            service.getExecutionData(
                dmc, 
                new ViewerDataRequestMonitor<IExecutionDMData>(getSession().getExecutor(), update) { 
                    @Override
                    protected void handleSuccess() {
                        fillExecutionDataProperties(update, getData());
                        update.done();
                    }
                });
        }        
    }
    
    protected void fillExecutionDataProperties(IPropertiesUpdate update, IExecutionDMData data) {
        StateChangeReason reason = data.getStateChangeReason();
        if (reason != null) {
        	update.setProperty(ILaunchVMConstants.PROP_STATE_CHANGE_REASON, data.getStateChangeReason().name());
        }
        
        if (data instanceof IExecutionDMData2) {
        	String details = ((IExecutionDMData2)data).getDetails();
        	if (details != null) {
            	update.setProperty(ILaunchVMConstants.PROP_STATE_CHANGE_DETAILS, details);
        	}
        }
    }

    @Override
    public void getContextsForEvent(VMDelta parentDelta, Object e, final DataRequestMonitor<IVMContext[]> rm) {
    	if (e instanceof ModelProxyInstalledEvent || e instanceof DataModelInitializedEvent) {
    		getContainerVMCForModelProxyInstallEvent(
    				parentDelta,
    				new DataRequestMonitor<VMContextInfo>(getExecutor(), rm) {
    					@Override
    					protected void handleCompleted() {
    						if (isSuccess()) {
    							rm.setData(new IVMContext[] { getData().fVMContext });
    						} else {
    							rm.setData(new IVMContext[0]);
    						}
    						rm.done();
    					}
    				});
    	} else {
    		super.getContextsForEvent(parentDelta, e, rm);
    	}
    }

    private static class VMContextInfo {
        final IVMContext fVMContext;
        final int fIndex;
        final boolean fIsSuspended;
        VMContextInfo(IVMContext vmContext, int index, boolean isSuspended) {
            fVMContext = vmContext;
            fIndex = index;
            fIsSuspended = isSuspended;
        }
    }
    
    private void getContainerVMCForModelProxyInstallEvent(VMDelta parentDelta, final DataRequestMonitor<VMContextInfo> rm) {
        getVMProvider().updateNode(this, new VMChildrenUpdate(
            parentDelta, getVMProvider().getPresentationContext(), -1, -1,
            new DataRequestMonitor<List<Object>>(getExecutor(), rm) {
                @Override
                protected void handleSuccess() {
                    try {
                        getSession().getExecutor().execute(new DsfRunnable() {
                            @Override
							public void run() {
                                final IRunControl runControl = getServicesTracker().getService(IRunControl.class);
                                if (runControl != null) {
                                    int vmcIdx = -1;
                                    int suspendedVmcIdx = -1;
                                    
                                    for (int i = 0; i < getData().size(); i++) {
                                        if (getData().get(i) instanceof IDMVMContext) {
                                            IDMVMContext vmc = (IDMVMContext)getData().get(i);
                                            IContainerDMContext containerDmc = DMContexts.getAncestorOfType(
                                                vmc.getDMContext(), IContainerDMContext.class);
                                            if (containerDmc != null) {
                                                vmcIdx = vmcIdx < 0 ? i : vmcIdx;
                                                if (runControl.isSuspended(containerDmc)) {
                                                    suspendedVmcIdx = suspendedVmcIdx < 0 ? i : suspendedVmcIdx;
                                                }
                                            }
                                        }
                                    }
                                    if (suspendedVmcIdx >= 0) {
                                        rm.setData(new VMContextInfo(
                                            (IVMContext)getData().get(suspendedVmcIdx), suspendedVmcIdx, true));
                                    } else if (vmcIdx >= 0) {
                                        rm.setData(new VMContextInfo((IVMContext)getData().get(vmcIdx), vmcIdx, false));
                                    } else {
                                        rm.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.REQUEST_FAILED, "No container available", null)); //$NON-NLS-1$
                                    }
                                    rm.done();
                                } else {
                                    rm.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.REQUEST_FAILED, "No container available", null)); //$NON-NLS-1$
                                    rm.done();
                                }
                            }
                        });
                    } catch (RejectedExecutionException e) {
                        rm.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.NOT_SUPPORTED, "", null)); //$NON-NLS-1$
                        rm.done();
                    }
                }
            }));
    }

    @Override
	public int getDeltaFlags(Object e) {
        IDMContext dmc = e instanceof IDMEvent<?> ? ((IDMEvent<?>)e).getDMContext() : null;

	    if (e instanceof IContainerResumedDMEvent) {
            if (((IContainerResumedDMEvent)e).getReason() != IRunControl.StateChangeReason.STEP) 
            {
                return IModelDelta.CONTENT;
            }
        } else if (e instanceof IContainerSuspendedDMEvent) {
            return IModelDelta.NO_CHANGE;
        } else if (e instanceof SteppingTimedOutEvent) {
	        if (dmc instanceof IContainerDMContext) 
	        {
	            return IModelDelta.CONTENT;
	        }
	    } else if (e instanceof IExitedDMEvent) {
	        return IModelDelta.CONTENT;
	    } else if (e instanceof IStartedDMEvent) {
	    	if (dmc instanceof IContainerDMContext) {
	    		return IModelDelta.EXPAND | IModelDelta.SELECT;
	    	} else {
		        return IModelDelta.CONTENT;
	    	}
        } else if (e instanceof ModelProxyInstalledEvent || e instanceof DataModelInitializedEvent) {
            return IModelDelta.SELECT | IModelDelta.EXPAND;
	    } else if (e instanceof StateChangedEvent) {
	    	return IModelDelta.STATE;
	    } else if (e instanceof FullStackRefreshEvent &&
            (((FullStackRefreshEvent)e).getTriggeringEvent() instanceof IContainerSuspendedDMEvent)) {
            return IModelDelta.CONTENT;
        }	    
	    return IModelDelta.NO_CHANGE;
	}

	@Override
	public void buildDelta(Object e, final VMDelta parentDelta, final int nodeOffset, final RequestMonitor requestMonitor) {
	    IDMContext dmc = e instanceof IDMEvent<?> ? ((IDMEvent<?>)e).getDMContext() : null;
	    
		if(e instanceof IContainerResumedDMEvent) {
            // Container resumed: 
		    // - If not stepping, update the container and the execution 
		    // contexts under it.  
		    // - If stepping, do nothing to avoid too many updates.  If a 
		    // time-out is reached before the step completes, the 
		    // ISteppingTimedOutEvent will trigger a full refresh.
		    if (((IContainerResumedDMEvent)e).getReason() != IRunControl.StateChangeReason.STEP) 
		    {
    	        parentDelta.addNode(createVMContext(((IDMEvent<?>)e).getDMContext()), IModelDelta.CONTENT);
		    } 
		} else if (e instanceof IContainerSuspendedDMEvent) {
            // Container suspended.  Do nothing here to give the stack the 
		    // priority in updating. The container and threads will update as 
		    // a result of FullStackRefreshEvent. 
		} else if (e instanceof SteppingTimedOutEvent) {
		    // Stepping time-out indicates that a step operation is taking 
		    // a long time, and the view needs to be refreshed to show 
		    // the user that the program is running.
		    // If the step was issued for the whole container refresh
		    // the whole container.
		    if (dmc instanceof IContainerDMContext) {
	            parentDelta.addNode(createVMContext(dmc), IModelDelta.CONTENT);
		    }
		} else if (e instanceof IExitedDMEvent) {
		    // An exited event could either be for a thread within a container
		    // or for the container itself.  
		    // If a container exited, refresh the parent element so that the 
		    // container may be removed.
		    // If a thread exited within a container, refresh that container.
			if (dmc instanceof IContainerDMContext) {
	    		parentDelta.setFlags(parentDelta.getFlags() |  IModelDelta.CONTENT);
	    	} else {
		        IContainerDMContext containerCtx = DMContexts.getAncestorOfType(dmc, IContainerDMContext.class);
		        if (containerCtx != null) {
		            parentDelta.addNode(createVMContext(containerCtx), IModelDelta.CONTENT);
		        }
	    	}
	    } else if (e instanceof IStartedDMEvent) {
            // A started event could either be for a thread within a container
            // or for the container itself.  
            // If a container started, issue an expand and select event to 
	        // show the threads in the new container. 
	        // Note: the EXPAND flag implies refreshing the parent element.
			if (dmc instanceof IContainerDMContext) {
		        parentDelta.addNode(createVMContext(dmc), IModelDelta.EXPAND | IModelDelta.SELECT);
			} else {
				IContainerDMContext containerCtx = DMContexts.getAncestorOfType(dmc, IContainerDMContext.class);
				if (containerCtx != null) {
					parentDelta.addNode(createVMContext(containerCtx), IModelDelta.CONTENT);
				}
			}
        } else if (e instanceof ModelProxyInstalledEvent || e instanceof DataModelInitializedEvent) {
            // Model Proxy install event is generated when the model is first 
            // populated into the view.  This happens when a new debug session
            // is started or when the view is first opened.  
            // In both cases, if there are already thread containers in the debug model, 
            // the desired user behavior is to show the containers and to select
            // the first thread.  
            // If the container is suspended, do not select it, instead, 
            // one of its threads will be selected.
            getContainerVMCForModelProxyInstallEvent(
                parentDelta,
                new DataRequestMonitor<VMContextInfo>(getExecutor(), requestMonitor) {
                    @Override
                    protected void handleCompleted() {
                        if (isSuccess()) {
                            parentDelta.addNode(
                                getData().fVMContext, nodeOffset + getData().fIndex,
                                IModelDelta.EXPAND | (getData().fIsSuspended ? 0 : IModelDelta.SELECT));
                        }
                        requestMonitor.done();
                    }
                });
            return;
	    } else if (e instanceof StateChangedEvent) {	    	
	    	// If there is a state change needed on the container, update the container
	    	if (dmc instanceof IContainerDMContext)
	    		parentDelta.addNode(createVMContext(dmc), IModelDelta.STATE);
        } else if (e instanceof FullStackRefreshEvent) {
            FullStackRefreshEvent refreshEvent = (FullStackRefreshEvent)e;
            if (refreshEvent.getTriggeringEvent() instanceof IContainerSuspendedDMEvent) {
            	// For a full container suspended event, issue a single change when we get
            	// a FullStackRefreshEvent.  This avoids refreshing all threads, even those
            	// there are not visible
            	// bug 386175
                IContainerSuspendedDMEvent containerTriggerEvent = 
                    (IContainerSuspendedDMEvent)refreshEvent.getTriggeringEvent();
                buildDeltaForFullStackRefreshEvent((IContainerDMContext)refreshEvent.getDMContext(), 
                    containerTriggerEvent.getTriggeringContexts(), parentDelta, nodeOffset, requestMonitor);
                return;
            }
	    }
	
		requestMonitor.done();
	 }

    /**
     * Builds a delta in response to automatic refresh event generated after 
     * every suspend event.  
     * <p>
     * As opposed to the StackFrameVMNode handling of the refresh event, the 
     * container handles only the refresh events for container suspended events, 
     * and it refreshes the entire container.
     * <p>
     * The default behavior is to check if the thread is still stepping or 
     * suspended and refresh the stack trace.
     */
    protected void buildDeltaForFullStackRefreshEvent(final IContainerDMContext containerCtx, 
        final IExecutionDMContext[] triggeringCtxs, final VMDelta parentDelta, final int nodeOffset, 
        final RequestMonitor rm) 
    {
        try {
            getSession().getExecutor().execute(new DsfRunnable() {
                @Override
                public void run() {
                    IRunControl runControlService = getServicesTracker().getService(IRunControl.class); 
                    if (runControlService == null) {
                        // Required services have not initialized yet.  Ignore the event.
                        rm.done();
                        return;
                    }         
                    
                    // Refresh the whole list of stack frames unless the target is already stepping the next command.  In 
                    // which case, the refresh will occur when the stepping sequence slows down or stops.  Trying to
                    // refresh the whole stack trace with every step would slow down stepping too much.
                    boolean isStepping = false;
                    for (IExecutionDMContext triggeringCtx : triggeringCtxs) {
                        if (runControlService.isStepping(triggeringCtx)) {
                            isStepping = true;
                            break;
                        }
                    }
                    if (!isStepping) {
                        parentDelta.addNode(createVMContext(containerCtx), IModelDelta.CONTENT);
                    }
                    
                    rm.done();
                }
            });
        } catch (RejectedExecutionException e) {
            // Session shut down, no delta to build.
            rm.done();
        }
    }


}
