/*******************************************************************************
 * 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;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.cdt.dsf.concurrent.ConfinedToDsfExecutor;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.DsfExecutor;
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.concurrent.ThreadSafe;
import org.eclipse.cdt.dsf.datamodel.AbstractDMEvent;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
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.IResumedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.ISuspendedDMEvent;
import org.eclipse.cdt.dsf.debug.service.IRunControl.StateChangeReason;
import org.eclipse.cdt.dsf.debug.service.IRunControl.StepType;
import org.eclipse.cdt.dsf.debug.ui.IDsfDebugUIConstants;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;

/**
 * This class builds on top of standard run control service to provide
 * functionality for step queuing and delaying. Step queuing essentially allows
 * user to press and hold the step key and achieve maximum stepping speed. If
 * this class is used, other service implementations, such as stack and
 * expressions, can use it to avoid requesting data from debugger back end if
 * another step is about to be executed.
 * 
 * @since 1.1
 */
@ConfinedToDsfExecutor("#getExecutor()")
public final class SteppingController {
    /**
     * Default delay in milliseconds, that it takes the SteppingTimedOutEvent 
     * event to be issued after a step is started. 
     * @see SteppingTimedOutEvent
     * @see #setStepTimeout(int)
     * @see #getStepTimeout()
     */
    public final static int STEPPING_TIMEOUT = 500;
    
    /**
     * The depth of the step queue.  In other words, the maximum number of steps 
     * that are queued before the step queue manager is throwing them away. 
     */
    public final static int STEP_QUEUE_DEPTH = 2;

	/**
	 * The maximum delay (in milliseconds) between steps when synchronized
	 * stepping is enabled. This also serves as a safeguard in the case stepping
	 * control participants fail to indicate completion of event processing.
	 */
    public final static int MAX_STEP_DELAY= 5000;

    private final static boolean DEBUG = "true".equals(Platform.getDebugOption("org.eclipse.cdt.dsf.ui/debug/stepping")); //$NON-NLS-1$ //$NON-NLS-2$

    /**
     * Indicates that the given context has been stepping for some time, 
     * and the UI (views and actions) may need to be updated accordingly. 
     */
	public static final class SteppingTimedOutEvent extends AbstractDMEvent<IExecutionDMContext> {
		private SteppingTimedOutEvent(IExecutionDMContext execCtx) {
			super(execCtx);
		}
	}

	/**
	 * Interface for clients interested in stepping control. When a stepping
	 * control participant is registered with the stepping controller, it is
	 * expected to call
	 * {@link SteppingController#doneStepping(IExecutionDMContext, ISteppingControlParticipant)
	 * doneStepping} as soon as a "step", i.e. a suspended event has been
	 * processed. If synchronized stepping is enabled, further stepping is
	 * blocked until all stepping control participants have indicated completion
	 * of event processing or the maximum timeout
	 * {@link SteppingController#MAX_STEP_DELAY} has been reached.
	 * 
	 * @see SteppingController#addSteppingControlParticipant(ISteppingControlParticipant)
	 * @see SteppingController#removeSteppingControlParticipant(ISteppingControlParticipant)
	 */
	public interface ISteppingControlParticipant {
	}

	private static class StepRequest {
		IExecutionDMContext fContext;
        StepType fStepType;
        boolean inProgress = false;
        StepRequest(IExecutionDMContext execCtx, StepType type) {
        	fContext = execCtx;
            fStepType = type;
        }
    }

	private class TimeOutRunnable extends DsfRunnable{
		
		TimeOutRunnable(IExecutionDMContext dmc) {
			fDmc = dmc;
		}
		
		private final IExecutionDMContext fDmc;
		
		@Override
		public void run() {
			fTimedOutFutures.remove(fDmc);

            if (getSession().isActive()) {
                fTimedOutFlags.put(fDmc, Boolean.TRUE);
                enableStepping(fDmc);
                // Issue the stepping time-out event.
                getSession().dispatchEvent(
                    new SteppingTimedOutEvent(fDmc), 
                    null);
            }
        }
	}
	
	private final DsfSession fSession;
	private final DsfServicesTracker fServicesTracker;

    private IRunControl fRunControl;
    private int fQueueDepth = STEP_QUEUE_DEPTH;
    
    private final Map<IExecutionDMContext,List<StepRequest>> fStepQueues = new HashMap<IExecutionDMContext,List<StepRequest>>();
    private final Map<IExecutionDMContext,Boolean> fTimedOutFlags = new HashMap<IExecutionDMContext,Boolean>();
    private final Map<IExecutionDMContext,ScheduledFuture<?>> fTimedOutFutures = new HashMap<IExecutionDMContext,ScheduledFuture<?>>();

	/**
	 * Records the time of the last step for an execution context.
	 */
	private final Map<IExecutionDMContext, Long> fLastStepTimes= new HashMap<IExecutionDMContext, Long>();

	/**
	 * Minimum step interval in milliseconds.
	 */
	private volatile int fMinStepInterval;

    /**
     * Step timeout in milliseconds.
     */
    private volatile int fStepTimeout = STEPPING_TIMEOUT;

	/**
	 * Map of execution contexts for which a step is in progress.
	 */
	private final Map<IExecutionDMContext, List<ISteppingControlParticipant>> fStepInProgress = new HashMap<IExecutionDMContext, List<ISteppingControlParticipant>>();

	/**
	 * List of registered stepping control participants.
	 */
	private final List<ISteppingControlParticipant> fParticipants = Collections.synchronizedList(new ArrayList<ISteppingControlParticipant>());

	/**
	 * Property change listener.  It updates the stepping control settings.
	 */
	private IPropertyChangeListener fPreferencesListener;

    public SteppingController(DsfSession session) {
        fSession = session;
        fServicesTracker = new DsfServicesTracker(DsfUIPlugin.getBundleContext(), session.getId());
        
        final IPreferenceStore store= DsfUIPlugin.getDefault().getPreferenceStore();

        fPreferencesListener = new IPropertyChangeListener() {
            @Override
			public void propertyChange(final PropertyChangeEvent event) {
                handlePropertyChanged(store, event);
            }};
        store.addPropertyChangeListener(fPreferencesListener);
        
        setMinimumStepInterval(store.getInt(IDsfDebugUIConstants.PREF_MIN_STEP_INTERVAL));
    }

    @ThreadSafe
    public void dispose() {
        try {
            fSession.getExecutor().execute(new DsfRunnable() {
                @Override
				public void run() {
                    if (fRunControl != null) {
                        getSession().removeServiceEventListener(SteppingController.this);
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            // Session already gone.
        }
    	
        IPreferenceStore store= DsfUIPlugin.getDefault().getPreferenceStore();
        store.removePropertyChangeListener(fPreferencesListener);

        fServicesTracker.dispose();
    }

    /**
     * Configure the minimum time (in milliseconds) to wait between steps.
     * 
     * @param interval
     */
    @ThreadSafe
    public void setMinimumStepInterval(int interval) {
    	fMinStepInterval = interval;
    }

    /**
     * Configure the step timeout value. This controls the delay how long it takes
     * to send out the {@link SteppingTimedOutEvent} after a step has been issued.
     * 
     * @param timeout  The timeout value in milliseconds (must be non-negative).
     * @since 2.2
     */
    @ThreadSafe
    public void setStepTimeout(int timeout) {
        assert timeout >= 0;
        fStepTimeout = timeout;
    }
    
    /**
     * @return the currently configured step timeout value
     * @since 2.2
     */
    @ThreadSafe
    public int getStepTimeout() {
        return fStepTimeout;
    }

	/**
	 * Register given stepping control participant.
	 * <p>
	 * Participants are obliged to call
	 * {@link #doneStepping(IExecutionDMContext, ISteppingControlParticipant)}
	 * when they have received and completed processing an
	 * {@link ISuspendedDMEvent}. If synchronized stepping is enabled, further
	 * stepping is disabled until all participants have indicated completion of
	 * processing the event.
	 * </p>
	 * 
	 * @param participant
	 */
    @ThreadSafe
    public void addSteppingControlParticipant(ISteppingControlParticipant participant) {
    	fParticipants.add(participant);
    }

    /**
     * Unregister given stepping control participant.
     * 
     * @param participant
     */
    @ThreadSafe
    public void removeSteppingControlParticipant(final ISteppingControlParticipant participant) {
    	fParticipants.remove(participant);
    }

    /**
     * Indicate that participant has completed processing of the last step.
     * 
     * @param execCtx
     */
    public void doneStepping(final IExecutionDMContext execCtx, final ISteppingControlParticipant participant) {
    	if (DEBUG) System.out.println("[SteppingController] doneStepping participant=" + participant.getClass().getSimpleName()); //$NON-NLS-1$
    	List<ISteppingControlParticipant> participants = fStepInProgress.get(execCtx);
    	if (participants != null) {
    		participants.remove(participant);
    		if (participants.isEmpty()) {
    			doneStepping(execCtx);
    		}
    	} else {
    		for (IExecutionDMContext disabledCtx : fStepInProgress.keySet()) {
    			if (DMContexts.isAncestorOf(disabledCtx, execCtx)) {
    				participants = fStepInProgress.get(disabledCtx);
    		    	if (participants != null) {
    		    		participants.remove(participant);
    		    		if (participants.isEmpty()) {
    		    			doneStepping(disabledCtx);
    		    		}
    		    	}
    			}
    		}
    	}
    }

    @ThreadSafe
	public DsfSession getSession() {
		return fSession;
	}

	/**
	 * All access to this class should happen through this executor.
	 * @return the executor this class is confined to
	 */
    @ThreadSafe
	public DsfExecutor getExecutor() {
		return getSession().getExecutor();
	}
	
	private DsfServicesTracker getServicesTracker() {
		return fServicesTracker;
	}

	private IRunControl getRunControl() {
		if (fRunControl == null) {
	        fRunControl = getServicesTracker().getService(IRunControl.class);
	        getSession().addServiceEventListener(this, null);
		}
		return fRunControl;
	}

	/**
     * Checks whether a step command can be queued up for given context.
     */
    public void canEnqueueStep(IExecutionDMContext execCtx, StepType stepType, DataRequestMonitor<Boolean> rm) {
        if (doCanEnqueueStep(execCtx, stepType)) {
            rm.setData(true);
            rm.done();
        } else {
            getRunControl().canStep(execCtx, stepType, rm);
        }
    }

    private boolean doCanEnqueueStep(IExecutionDMContext execCtx, StepType stepType) {
        return getRunControl().isStepping(execCtx) && !isSteppingTimedOut(execCtx); 
    }

    /**
     * Check whether the next step on the given execution context should be delayed
     * based on the configured step delay.
     * 
	 * @param execCtx
	 * @return <code>true</code> if the step should be delayed
	 */
	private boolean shouldDelayStep(IExecutionDMContext execCtx) {
        final int stepDelay= getStepDelay(execCtx);
        if (DEBUG) System.out.println("[SteppingController] shouldDelayStep delay=" + stepDelay); //$NON-NLS-1$
		return stepDelay > 0;
	}

	/**
	 * Compute the delay in milliseconds before the next step for the given context may be executed.
	 * 
	 * @param execCtx
	 * @return  the number of milliseconds before the next possible step
	 */
	private int getStepDelay(IExecutionDMContext execCtx) {
	    int minStepInterval = fMinStepInterval;
		if (minStepInterval > 0) {
	        for (IExecutionDMContext lastStepCtx : fLastStepTimes.keySet()) {
	            if (execCtx.equals(lastStepCtx) || DMContexts.isAncestorOf(execCtx, lastStepCtx)) {
	                long now = System.currentTimeMillis();
					int delay= (int) (fLastStepTimes.get(lastStepCtx) + minStepInterval - now);
					return Math.max(delay, 0);
	            }
	        }
		}
        return 0;
	}

	private void updateLastStepTime(IExecutionDMContext execCtx) {
        long now = System.currentTimeMillis();
		fLastStepTimes.put(execCtx, now);
        for (IExecutionDMContext lastStepCtx : fLastStepTimes.keySet()) {
            if (!execCtx.equals(lastStepCtx) && DMContexts.isAncestorOf(execCtx, lastStepCtx)) {
				fLastStepTimes.put(lastStepCtx, now);
            }
        }
	}

	private long getLastStepTime(IExecutionDMContext execCtx) {
		if (fLastStepTimes.containsKey(execCtx)) {
			return fLastStepTimes.get(execCtx);
		}
		for (IExecutionDMContext lastStepCtx : fLastStepTimes.keySet()) {
			if (DMContexts.isAncestorOf(execCtx, lastStepCtx)) {
				return fLastStepTimes.get(lastStepCtx);
			}
		}
		return 0;
	}

	/**
     * Returns the number of step commands that are queued for given execution
     * context.
     */
    public int getPendingStepCount(IExecutionDMContext execCtx) {
        List<StepRequest> stepQueue = getStepQueue(execCtx);
        if (stepQueue == null) return 0;
        return stepQueue.size();
    }

    /**
     * Adds a step command to the execution queue for given context.
     * @param execCtx Execution context that should perform the step. 
     * @param stepType Type of step to execute.
     */
    public void enqueueStep(final IExecutionDMContext execCtx, final StepType stepType) {
    	if (DEBUG) System.out.println("[SteppingController] enqueueStep ctx=" + execCtx); //$NON-NLS-1$
        if (!shouldDelayStep(execCtx) || doCanEnqueueStep(execCtx, stepType)) {
            doEnqueueStep(execCtx, stepType);
            processStepQueue(execCtx);
        }
    }

	private void doStep(final IExecutionDMContext execCtx, final StepType stepType) {
		if (DEBUG) System.out.println("[SteppingController] doStep ctx="+execCtx); //$NON-NLS-1$
	    disableStepping(execCtx);
        updateLastStepTime(execCtx);
        
		getRunControl().step(execCtx, stepType, new RequestMonitor(getExecutor(), null) {
		    @Override
		    protected void handleSuccess() {
	            fTimedOutFlags.remove(execCtx);
	            ScheduledFuture<?> currentTimeOutFuture = fTimedOutFutures.get(execCtx);
	            if (currentTimeOutFuture != null) {
	            	currentTimeOutFuture.cancel(false);
	            }
	            fTimedOutFutures.put(execCtx, getExecutor().schedule(new TimeOutRunnable(execCtx), fStepTimeout, TimeUnit.MILLISECONDS));
		    }
		    
		    @Override
		    protected void handleFailure() {
		    	// in case of a failed step - enable stepping again (bug 265267)
		    	enableStepping(execCtx);
		        if (getStatus().getCode() == IDsfStatusConstants.INVALID_STATE) {
	                // Ignore errors.  During fast stepping there can be expected race
	                // conditions leading to stepping errors.
		            return;
		        } 
		        super.handleFailure();
		    }
		});
	}

	/**
	 * Enqueue the given step for later execution.
	 * 
	 * @param execCtx
	 * @param stepType
	 */
	private void doEnqueueStep(final IExecutionDMContext execCtx, final StepType stepType) {
		List<StepRequest> stepQueue = fStepQueues.get(execCtx);
		if (stepQueue == null) {
		    stepQueue = new LinkedList<StepRequest>();
		    fStepQueues.put(execCtx, stepQueue);
		}
		if (stepQueue.size() < fQueueDepth) {
		    stepQueue.add(new StepRequest(execCtx, stepType));
		}
	}

    /**
     * Returns whether the step instruction for the given context has timed out.
     */
    public boolean isSteppingTimedOut(IExecutionDMContext execCtx) {
        for (IExecutionDMContext timedOutCtx : fTimedOutFlags.keySet()) {
            if (execCtx.equals(timedOutCtx) || DMContexts.isAncestorOf(execCtx, timedOutCtx)) {
                return fTimedOutFlags.get(timedOutCtx);
            }
        }
        return false;
    }
    
	/**
	 * Process next step on queue if any.
	 * @param execCtx
	 */
	private void processStepQueue(final IExecutionDMContext execCtx) {
        final List<StepRequest> queue = getStepQueue(execCtx);
		if (queue != null) {
			final int stepDelay = getStepDelay(execCtx);
			if (stepDelay > 0) {
				getExecutor().schedule(new DsfRunnable() {
					@Override
					public void run() {
						processStepQueue(execCtx);
					}
				}, stepDelay, TimeUnit.MILLISECONDS);
				return;
			}
            final StepRequest request = queue.get(0);
    		if (DEBUG) System.out.println("[SteppingController] processStepQueue request-in-progress="+request.inProgress); //$NON-NLS-1$
            if (!request.inProgress) {
        		if (isSteppingDisabled(request.fContext)) {
        			return;
        		}
                request.inProgress = true;
                getRunControl().canStep(
                    request.fContext, request.fStepType, 
                    new DataRequestMonitor<Boolean>(getExecutor(), null) {
                        @Override
                        protected void handleCompleted() {
                            if (isSuccess() && getData()) {
                        		queue.remove(0);
                                if (queue.isEmpty()) fStepQueues.remove(request.fContext);                               
                        		doStep(request.fContext, request.fStepType);
                            } else { 
                                // For whatever reason we can't step anymore, so clear out
                                // the step queue.
                                fStepQueues.remove(request.fContext);
                            }
                        }
                    });
            } 
        }
	}

	private List<StepRequest> getStepQueue(IExecutionDMContext execCtx) {
		List<StepRequest> queue = fStepQueues.get(execCtx);
		if (queue == null) {
	        for (IExecutionDMContext stepCtx : fStepQueues.keySet()) {
	            if (DMContexts.isAncestorOf(stepCtx, execCtx)) {
	            	queue = fStepQueues.get(stepCtx);
	            	break;
	            }
	        }
		}
		return queue;
	}

	/**
	 * Disable stepping for the given execution context.
	 * 
	 * @param execCtx
	 */
	private void disableStepping(IExecutionDMContext execCtx) {
		if (!fParticipants.isEmpty()) {
			fStepInProgress.put(execCtx, new ArrayList<ISteppingControlParticipant>(fParticipants));
		}
	}

    /**
     * Indicate that processing of the last step has completed and
     * the next step can be issued.
     * 
     * @param execCtx
     */
    private void doneStepping(final IExecutionDMContext execCtx) {
    	if (DEBUG) System.out.println("[SteppingController] doneStepping ctx=" + execCtx); //$NON-NLS-1$
        enableStepping(execCtx);
        processStepQueue(execCtx);
    }

	/**
	 * Enable stepping for the given execution context.
	 * 
	 * @param execCtx
	 */
	public void enableStepping(final IExecutionDMContext execCtx) {
        fStepInProgress.remove(execCtx);
		for (IExecutionDMContext disabledCtx : fStepInProgress.keySet()) {
			if (DMContexts.isAncestorOf(disabledCtx, execCtx)) {
				fStepInProgress.remove(disabledCtx);
			}
		}
	}

	private boolean isSteppingDisabled(IExecutionDMContext execCtx) {
        boolean disabled= fStepInProgress.containsKey(execCtx);
        if (!disabled) {
	        for (IExecutionDMContext disabledCtx : fStepInProgress.keySet()) {
				if (DMContexts.isAncestorOf(execCtx, disabledCtx)) {
					disabled = true;
					break;
				}
			}
        }
        if (disabled) {
        	long now = System.currentTimeMillis();
        	long lastStepTime = getLastStepTime(execCtx);
        	if (now - lastStepTime > MAX_STEP_DELAY) {
        		if (DEBUG) System.out.println("[SteppingController] stepping control participant(s) timed out"); //$NON-NLS-1$
        		enableStepping(execCtx);
        		disabled = false;
        	}
        }
        return disabled;
	}

	protected void handlePropertyChanged(final IPreferenceStore store, final PropertyChangeEvent event) {
		String property = event.getProperty();
		if (IDsfDebugUIConstants.PREF_MIN_STEP_INTERVAL.equals(property)) {
			setMinimumStepInterval(store.getInt(property));
		}
	}


    ///////////////////////////////////////////////////////////////////////////

    @DsfServiceEventHandler 
    public void eventDispatched(final ISuspendedDMEvent e) {
        // Take care of the stepping time out
    	boolean timedOut = false;
        IExecutionDMContext dmc = e.getDMContext();
        for (Iterator<Map.Entry<IExecutionDMContext, Boolean>> itr = fTimedOutFlags.entrySet().iterator(); itr.hasNext();) {
            Map.Entry<IExecutionDMContext,Boolean> entry = itr.next();
            IExecutionDMContext nextDmc = entry.getKey();
            if (nextDmc.equals(dmc) || DMContexts.isAncestorOf(nextDmc, dmc)) {
                if (entry.getValue()) {
                    // after step timeout do not process queued steps
                    fStepQueues.remove(dmc);
                    timedOut = true;
                }
                itr.remove();
            }
        }
        
        // Cancel all time-out futures related to the event context.  I.e.
        // - If event is on a container, all child threads are suspended and 
        // should not issue a stepping time-out event.
        // - If event is on a thread, and resumed event was on a container then 
        // stepping timeout for container should be canceled as it would affect
        // suspended thread.
        for (Iterator<Map.Entry<IExecutionDMContext, ScheduledFuture<?>>> itr = fTimedOutFutures.entrySet().iterator(); itr.hasNext();) {
            Map.Entry<IExecutionDMContext, ScheduledFuture<?>> entry = itr.next();
            IExecutionDMContext nextDmc = entry.getKey();
            if (nextDmc.equals(dmc) || DMContexts.isAncestorOf(nextDmc, dmc) || DMContexts.isAncestorOf(dmc, nextDmc)) {
                entry.getValue().cancel(false);
                itr.remove();
            }            
        }
        
        if (e.getReason() != StateChangeReason.STEP) {
        	// after any non-step suspend reason do not process queued steps for given context
        	fStepQueues.remove(dmc);
        } else if (!timedOut){
        	// Check if there's a step pending, if so execute it
        	processStepQueue(dmc);
        }
    }

    @DsfServiceEventHandler 
    public void eventDispatched(final IResumedDMEvent e) {
        if (e.getReason().equals(StateChangeReason.STEP)) {
            final IExecutionDMContext dmc = e.getDMContext();
            fTimedOutFlags.remove(dmc);
            
            
            // Find any time-out futures for contexts that are children of the 
            // resumed context, and cancel them as they'll be replaced.
            if (!fTimedOutFutures.containsKey(dmc)) {
	            for (Iterator<Map.Entry<IExecutionDMContext, ScheduledFuture<?>>> itr = fTimedOutFutures.entrySet().iterator(); itr.hasNext();) {
	                Map.Entry<IExecutionDMContext, ScheduledFuture<?>> entry = itr.next();
	                if (DMContexts.isAncestorOf(entry.getKey(), dmc)) {
	                    entry.getValue().cancel(false);
	                    itr.remove();
	                }            
	            }
	            
	            fTimedOutFutures.put(dmc, getExecutor().schedule(new TimeOutRunnable(dmc), fStepTimeout, TimeUnit.MILLISECONDS));
            }
        } 
    }    
}