/*******************************************************************************
 * Copyright (c) 2006, 2013 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
 *     Ericsson			  - Modified for new functionality	
 *******************************************************************************/
package org.eclipse.cdt.dsf.debug.ui.viewmodel.launch;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.eclipse.cdt.debug.ui.CDebugUIPlugin;
import org.eclipse.cdt.dsf.concurrent.DsfRunnable;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ThreadSafe;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
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.IExecutionDMContext;
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.ISuspendedDMEvent;
import org.eclipse.cdt.dsf.debug.ui.IDsfDebugUIConstants;
import org.eclipse.cdt.dsf.debug.ui.viewmodel.launch.LaunchRootVMNode.LaunchesEvent;
import org.eclipse.cdt.dsf.debug.ui.viewmodel.launch.StackFramesVMNode.IncompleteStackVMContext;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.dsf.ui.viewmodel.AbstractVMAdapter;
import org.eclipse.cdt.dsf.ui.viewmodel.IRootVMNode;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMNode;
import org.eclipse.cdt.dsf.ui.viewmodel.datamodel.AbstractDMVMProvider;
import org.eclipse.cdt.dsf.ui.viewmodel.update.AutomaticUpdatePolicy;
import org.eclipse.cdt.dsf.ui.viewmodel.update.IVMUpdatePolicy;
import org.eclipse.cdt.dsf.ui.viewmodel.update.ManualUpdatePolicy;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IDebugEventSetListener;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchesListener2;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelDelta;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;


/**
 * @since 1.1
 */
public class AbstractLaunchVMProvider extends AbstractDMVMProvider 
    implements IDebugEventSetListener, ILaunchesListener2
{
    /**
	 * Delay (in milliseconds) before a full stack trace will be requested.
	 */
	private static final int FRAME_UPDATE_DELAY= 200;
	
    private final Map<IExecutionDMContext,ScheduledFuture<?>> fRefreshStackFramesFutures = new HashMap<IExecutionDMContext,ScheduledFuture<?>>();

	private IPropertyChangeListener fPreferencesListener;

	@ThreadSafe
    public AbstractLaunchVMProvider(AbstractVMAdapter adapter, IPresentationContext presentationContext, DsfSession session)
    {
        super(adapter, presentationContext, session);
        
        final IPreferenceStore store= DsfUIPlugin.getDefault().getPreferenceStore();
        if (store.getBoolean(IDsfDebugUIConstants.PREF_STACK_FRAME_LIMIT_ENABLE)) {
        	getPresentationContext().setProperty(IDsfDebugUIConstants.PREF_STACK_FRAME_LIMIT, store.getInt(IDsfDebugUIConstants.PREF_STACK_FRAME_LIMIT));
        }
        
        fPreferencesListener = new IPropertyChangeListener() {
			@Override
			public void propertyChange(final PropertyChangeEvent event) {
				handlePropertyChanged(store, event);
			}};
        store.addPropertyChangeListener(fPreferencesListener);

        final IPreferenceStore cStore= CDebugUIPlugin.getDefault().getPreferenceStore();
       	getPresentationContext().setProperty(IDsfDebugUIConstants.DEBUG_VIEW_SHOW_FULL_PATH_PROPERTY, cStore.getBoolean(IDsfDebugUIConstants.DEBUG_VIEW_SHOW_FULL_PATH_PROPERTY));
		cStore.addPropertyChangeListener(fPreferencesListener);
 
        // Register the LaunchVM provider as a listener to debug and launch 
        // events.  These events are used by the launch and processes nodes.
        DebugPlugin.getDefault().addDebugEventListener(this);
        DebugPlugin.getDefault().getLaunchManager().addLaunchListener(this);
    }
    
    @Override
	protected IVMUpdatePolicy[] createUpdateModes() {
		return new IVMUpdatePolicy[] {
				new DelayedStackRefreshUpdatePolicy(new AutomaticUpdatePolicy()),
				new DelayedStackRefreshUpdatePolicy(new ManualUpdatePolicy())
		};
    }
    
    @Override
	public void handleDebugEvents(final DebugEvent[] events) {
        if (isDisposed()) return;
        
		// We're in session's executor thread. Re-dispatch to our executor thread
		// and then call root layout node.
        try {
            getExecutor().execute(new Runnable() {
                @Override
				public void run() {
                    if (isDisposed()) return;
    
                    for (final DebugEvent event : events) {
                        handleEvent(event);
                    }
                }});
        } catch (RejectedExecutionException e) {
            // Ignore.  This exception could be thrown if the provider is being 
            // shut down.  
        }
    }

    @Override
    public void handleEvent(Object event, final RequestMonitor rm) {
        if (event instanceof DoubleClickEvent && !isDisposed()) {
            final ISelection selection= ((DoubleClickEvent) event).getSelection();
            if (selection instanceof IStructuredSelection) {
                Object element= ((IStructuredSelection) selection).getFirstElement();
                if (element instanceof IncompleteStackVMContext) {
                    IncompleteStackVMContext incStackVmc = ((IncompleteStackVMContext) element); 
                    IVMNode node = incStackVmc.getVMNode();
                    if (node instanceof StackFramesVMNode && node.getVMProvider() == this) {
                        IExecutionDMContext exeCtx= incStackVmc.getExecutionDMContext();
						((StackFramesVMNode) node).incrementStackFrameLimit(exeCtx);
						// replace double click event with expand stack event
						final ExpandStackEvent expandStackEvent = new ExpandStackEvent(exeCtx);
						getExecutor().execute(new DsfRunnable() {
						    @Override
							public void run() {
						        handleEvent(expandStackEvent, null);
						    }
						});
                    }
                    if (rm != null) {
                    	rm.done();
                    }
                    return;
                }
            }
        } else if (event instanceof IRunControl.ISuspendedDMEvent) {
            final IRunControl.ISuspendedDMEvent suspendEvent = (IRunControl.ISuspendedDMEvent)event;
    		final IExecutionDMContext exeContext= suspendEvent.getDMContext();
    		ScheduledFuture<?> refreshStackFramesFuture = getRefreshFuture(exeContext);
    		// trigger delayed full stack frame update
    		if (refreshStackFramesFuture != null) {
    			// cancel previously scheduled frame update
    			refreshStackFramesFuture.cancel(false);
    		}

    		try {
        		refreshStackFramesFuture = getSession().getExecutor().schedule(
    	            new DsfRunnable() { 
    	                @Override
						public void run() {
    	                    if (getSession().isActive()) {
    	                        getExecutor().execute(new Runnable() {
    	                            @Override
									public void run() {
    	                                // trigger full stack frame update
    	                                ScheduledFuture<?> future= fRefreshStackFramesFutures.get(exeContext);
    	                                if (future != null && !isDisposed()) {
    	                                    fRefreshStackFramesFutures.remove(exeContext);
    	                                    handleEvent(new FullStackRefreshEvent(exeContext, suspendEvent), null);
    	                                }
    	                            }});
    	                    }
    	                }
    	            },
    			    FRAME_UPDATE_DELAY, TimeUnit.MILLISECONDS);
        		fRefreshStackFramesFutures.put(exeContext, refreshStackFramesFuture);
    		} catch (RejectedExecutionException e) {}
    	} else if (event instanceof IRunControl.IResumedDMEvent) {
    		IExecutionDMContext exeContext= ((IRunControl.IResumedDMEvent) event).getDMContext();
    		ScheduledFuture<?> refreshStackFramesFuture= fRefreshStackFramesFutures.get(exeContext);
    		if (refreshStackFramesFuture != null) {
    			// cancel previously scheduled frame update
    			refreshStackFramesFuture.cancel(false);
    			fRefreshStackFramesFutures.remove(exeContext);
    		}
    	}

    	super.handleEvent(event, rm);
    }

    /**
     * Returns the future for the given execution context or for any child of the 
     * given execution context.
     */
    private ScheduledFuture<?> getRefreshFuture(IExecutionDMContext execCtx) {
        for (IExecutionDMContext refreshCtx : fRefreshStackFramesFutures.keySet()) {
            if (refreshCtx.equals(execCtx) || DMContexts.isAncestorOf(refreshCtx, execCtx)) {
                return fRefreshStackFramesFutures.remove(refreshCtx);
            }
        }
        return null;
    }
    
    @Override
    public void dispose() {
        DebugPlugin.getDefault().removeDebugEventListener(this);
        DebugPlugin.getDefault().getLaunchManager().removeLaunchListener(this);

        final IPreferenceStore store= DsfUIPlugin.getDefault().getPreferenceStore();
        store.removePropertyChangeListener(fPreferencesListener);

        final IPreferenceStore cStore= CDebugUIPlugin.getDefault().getPreferenceStore();
        cStore.removePropertyChangeListener(fPreferencesListener);

        super.dispose();
    }
    
    @Override
	public void launchesAdded(ILaunch[] launches) {
        handleLaunchesEvent(new LaunchesEvent(launches, LaunchesEvent.Type.ADDED)); 
    }
    
    @Override
	public void launchesRemoved(ILaunch[] launches) {
        handleLaunchesEvent(new LaunchesEvent(launches, LaunchesEvent.Type.REMOVED)); 
    }
    
    @Override
	public void launchesChanged(ILaunch[] launches) {
        handleLaunchesEvent(new LaunchesEvent(launches, LaunchesEvent.Type.CHANGED)); 
    }
    
    @Override
	public void launchesTerminated(ILaunch[] launches) {
        handleLaunchesEvent(new LaunchesEvent(launches, LaunchesEvent.Type.TERMINATED)); 
    }
    
    private void handleLaunchesEvent(final LaunchesEvent event) {
        if (isDisposed()) return;
        
		// We're in session's executor thread. Re-dispach to our executor thread
		// and then call root layout node.
        try {
            getExecutor().execute(new Runnable() {
                @Override
				public void run() {
                    if (isDisposed()) return;
    
                    IRootVMNode rootLayoutNode = getRootVMNode();
                    if (rootLayoutNode != null && rootLayoutNode.getDeltaFlags(event) != IModelDelta.NO_CHANGE) {
                        handleEvent(event);
                    }
                }});
        } catch (RejectedExecutionException e) {
            // Ignore.  This exception could be thrown if the provider is being 
            // shut down.  
        }
    }
    
    @Override
    protected boolean canSkipHandlingEvent(Object newEvent, Object eventToSkip) {
        // To optimize view performance when stepping rapidly, skip events that came 
        // before the last suspended events.  However, the debug view can get suspended
        // events for different threads, so make sure to skip only the events if they
        // were in the same hierarchy as the last suspended event.
        // Note: Avoid skipping thread started/exited events which require a larger
        // scope refresh than some suspended events.
        if (newEvent instanceof IStartedDMEvent || newEvent instanceof IExitedDMEvent) {
            return false;
        }
        
        if (newEvent instanceof ISuspendedDMEvent && eventToSkip instanceof IDMEvent<?>) {
            IDMContext newEventDmc = ((IDMEvent<?>)newEvent).getDMContext();
            IDMContext eventToSkipDmc = ((IDMEvent<?>)eventToSkip).getDMContext();
            
            if (newEventDmc.equals(eventToSkipDmc) || DMContexts.isAncestorOf(eventToSkipDmc, newEventDmc)) {
                return true;
            }
        }
        
        return false;
    }

	protected void handlePropertyChanged(final IPreferenceStore store, final PropertyChangeEvent event) {
		String property = event.getProperty();
		boolean processEvent = false;
		
		if (IDsfDebugUIConstants.PREF_STACK_FRAME_LIMIT_ENABLE.equals(property)
				|| IDsfDebugUIConstants.PREF_STACK_FRAME_LIMIT.equals(property)) {
			if (store.getBoolean(IDsfDebugUIConstants.PREF_STACK_FRAME_LIMIT_ENABLE)) {
		    	getPresentationContext().setProperty(IDsfDebugUIConstants.PREF_STACK_FRAME_LIMIT, store.getInt(IDsfDebugUIConstants.PREF_STACK_FRAME_LIMIT));
			} else {
		    	getPresentationContext().setProperty(IDsfDebugUIConstants.PREF_STACK_FRAME_LIMIT, null);
			}
			processEvent = true;
		} else if (IDsfDebugUIConstants.DEBUG_VIEW_SHOW_FULL_PATH_PROPERTY.equals(property)) {
			getPresentationContext().setProperty(IDsfDebugUIConstants.DEBUG_VIEW_SHOW_FULL_PATH_PROPERTY, event.getNewValue());
			processEvent = true;
		}
		
		if (processEvent) {
			getExecutor().execute(new DsfRunnable() {
			    @Override
				public void run() {
			        handleEvent(event);
			    }
			});
		}
	}

}
