/*******************************************************************************
 * Copyright (c) 2000, 2010 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jface.text.contentassist;

import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;

import org.eclipse.jface.internal.text.InformationControlReplacer;

import org.eclipse.jface.text.AbstractInformationControlManager;
import org.eclipse.jface.text.AbstractReusableInformationControlCreator;
import org.eclipse.jface.text.DefaultInformationControl;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IInformationControlExtension3;


/**
 * Displays the additional information available for a completion proposal.
 *
 * @since 2.0
 */
class AdditionalInfoController extends AbstractInformationControlManager {

	/**
	 * A timer thread.
	 *
	 * @since 3.2
	 */
	private static abstract class Timer {
		private static final int DELAY_UNTIL_JOB_IS_SCHEDULED= 50;

		/**
		 * A <code>Task</code> is {@link Task#run() run} when {@link #delay()} milliseconds have
		 * elapsed after it was scheduled without a {@link #reset(ICompletionProposal) reset}
		 * to occur.
		 */
		private abstract class Task implements Runnable {
			/**
			 * @return the delay in milliseconds before this task should be run
			 */
	        public abstract long delay();
	        /**
	         * Runs this task.
	         */
	        @Override
			public abstract void run();
	        /**
	         * @return the task to be scheduled after this task has been run
	         */
	        public abstract Task nextTask();
		}

		/**
		 * IDLE: the initial task, and active whenever the info has been shown. It cannot be run,
		 * but specifies an infinite delay.
		 */
		private final Task IDLE= new Task() {
			@Override
			public void run() {
				Assert.isTrue(false);
			}

			@Override
			public Task nextTask() {
				Assert.isTrue(false);
			    return null;
			}

			@Override
			public long delay() {
				return Long.MAX_VALUE;
			}

			@Override
			public String toString() {
				return "IDLE"; //$NON-NLS-1$
			}
		};
		/**
		 * FIRST_WAIT: Schedules a platform {@link Job} to fetch additional info from an {@link ICompletionProposalExtension5}.
		 */
		private final Task FIRST_WAIT= new Task() {
			@Override
			public void run() {
				final ICompletionProposalExtension5 proposal= getCurrentProposalEx();
				Job job= new Job(JFaceTextMessages.getString("AdditionalInfoController.job_name")) { //$NON-NLS-1$
					@Override
					protected IStatus run(IProgressMonitor monitor) {
						Object info;
                        try {
	                        info= proposal.getAdditionalProposalInfo(monitor);
                        } catch (RuntimeException x) {
                        	/*
							 * XXX: This is the safest fix at this point so close to end of 3.2.
							 *		Will be revisited when fixing https://bugs.eclipse.org/bugs/show_bug.cgi?id=101033
                        	 */
                        	return new Status(IStatus.WARNING, "org.eclipse.jface.text", IStatus.OK, "", x); //$NON-NLS-1$ //$NON-NLS-2$
                        }
						setInfo((ICompletionProposal) proposal, info);
						return Status.OK_STATUS;
					}
				};
				job.schedule();
			}

			@Override
			public Task nextTask() {
				return SECOND_WAIT;
			}

			@Override
			public long delay() {
				return DELAY_UNTIL_JOB_IS_SCHEDULED;
			}

			@Override
			public String toString() {
				return "FIRST_WAIT"; //$NON-NLS-1$
			}
		};
		/**
		 * SECOND_WAIT: Allows display of additional info obtained from an
		 * {@link ICompletionProposalExtension5}.
		 */
		private final Task SECOND_WAIT= new Task() {
			@Override
			public void run() {
				// show the info
				allowShowing();
			}

			@Override
			public Task nextTask() {
				return IDLE;
			}

			@Override
			public long delay() {
				return fDelay - DELAY_UNTIL_JOB_IS_SCHEDULED;
			}

			@Override
			public String toString() {
				return "SECOND_WAIT"; //$NON-NLS-1$
			}
		};
		/**
		 * LEGACY_WAIT: Posts a runnable into the display thread to fetch additional info from non-{@link ICompletionProposalExtension5}s.
		 */
		private final Task LEGACY_WAIT= new Task() {
			@Override
			public void run() {
				final ICompletionProposal proposal= getCurrentProposal();
				if (!fDisplay.isDisposed()) {
					fDisplay.asyncExec(new Runnable() {
						@Override
						public void run() {
							synchronized (Timer.this) {
								if (proposal == getCurrentProposal()) {
									Object info= proposal.getAdditionalProposalInfo();
									showInformation(proposal, info);
								}
							}
						}
					});
				}
			}

			@Override
			public Task nextTask() {
				return IDLE;
			}

			@Override
			public long delay() {
				return fDelay;
			}

			@Override
			public String toString() {
			    return "LEGACY_WAIT"; //$NON-NLS-1$
			}
		};
		/**
		 * EXIT: The task that triggers termination of the timer thread.
		 */
		private final Task EXIT= new Task() {
			@Override
			public long delay() {
		        return 1;
	        }

			@Override
			public Task nextTask() {
				Assert.isTrue(false);
		        return EXIT;
	        }

			@Override
			public void run() {
				Assert.isTrue(false);
	        }

			@Override
			public String toString() {
				return "EXIT"; //$NON-NLS-1$
			}
		};

		/** The timer thread. */
		private final Thread fThread;

		/** The currently waiting / active task. */
		private Task fTask;
		/** The next wake up time. */
		private long fNextWakeup;

		private ICompletionProposal fCurrentProposal= null;
		private Object fCurrentInfo= null;
		private boolean fAllowShowing= false;

		private final Display fDisplay;
		private final int fDelay;

		/**
		 * Creates a new timer.
		 *
		 * @param display the display to use for display thread posting.
		 * @param delay the delay until to show additional info
		 */
		public Timer(Display display, int delay) {
    		fDisplay= display;
			fDelay= delay;
			long current= System.currentTimeMillis();
    		schedule(IDLE, current);

    		fThread= new Thread(new Runnable() {
    			@Override
				public void run() {
    				try {
	                    loop();
                    } catch (InterruptedException x) {
                    }
    			}
    		}, JFaceTextMessages.getString("InfoPopup.info_delay_timer_name")); //$NON-NLS-1$
			fThread.start();
		}

		/**
		 * Terminates the timer thread.
		 */
		public synchronized final void terminate() {
			schedule(EXIT, System.currentTimeMillis());
			notifyAll();
		}

		/**
		 * Resets the timer thread as the selection has changed to a new proposal.
		 *
		 * @param p the new proposal
		 */
		public synchronized final void reset(ICompletionProposal p) {
			if (fCurrentProposal != p) {
				fCurrentProposal= p;
				fCurrentInfo= null;
				fAllowShowing= false;

				long oldWakeup= fNextWakeup;
				Task task= taskOnReset(p);
				schedule(task, System.currentTimeMillis());
				if (fNextWakeup < oldWakeup)
					notifyAll();
			}
		}

        private Task taskOnReset(ICompletionProposal p) {
			if (p == null)
				return IDLE;
			if (isExt5(p))
				return FIRST_WAIT;
			return LEGACY_WAIT;
        }

        private synchronized void loop() throws InterruptedException {
        	long current= System.currentTimeMillis();
        	Task task= currentTask();

        	while (task != EXIT) {
        		long delay= fNextWakeup - current;
        		if (delay <= 0) {
        			task.run();
        			task= task.nextTask();
        			schedule(task, current);
        		} else {
        			wait(delay);
        			current= System.currentTimeMillis();
        			task= currentTask();
        		}
        	}
        }

        private Task currentTask() {
        	return fTask;
        }

        private void schedule(Task task, long current) {
        	fTask= task;
        	long nextWakeup= current + task.delay();
        	if (nextWakeup <= current)
        		fNextWakeup= Long.MAX_VALUE;
        	else
        		fNextWakeup= nextWakeup;
        }

		private boolean isExt5(ICompletionProposal p) {
			return p instanceof ICompletionProposalExtension5;
		}

        ICompletionProposal getCurrentProposal() {
	        return fCurrentProposal;
        }

        ICompletionProposalExtension5 getCurrentProposalEx() {
        	Assert.isTrue(fCurrentProposal instanceof ICompletionProposalExtension5);
        	return (ICompletionProposalExtension5) fCurrentProposal;
        }

        synchronized void setInfo(ICompletionProposal proposal, Object info) {
        	if (proposal == fCurrentProposal) {
        		fCurrentInfo= info;
        		if (fAllowShowing) {
        			triggerShowing();
        		}
        	}
        }

        private void triggerShowing() {
			final Object info= fCurrentInfo;
			if (!fDisplay.isDisposed()) {
				fDisplay.asyncExec(new Runnable() {
					@Override
					public void run() {
						synchronized (Timer.this) {
							if (info == fCurrentInfo) {
								showInformation(fCurrentProposal, info);
							}
						}
					}
				});
			}
        }

        /**
         * Called in the display thread to show additional info.
         *
         * @param proposal the proposal to show information about
         * @param info the information about <code>proposal</code>
         */
        protected abstract void showInformation(ICompletionProposal proposal, Object info);

        void allowShowing() {
        	fAllowShowing= true;
       		triggerShowing();
        }
	}
	/**
	 * Internal table selection listener.
	 */
	private class TableSelectionListener implements SelectionListener {

		@Override
		public void widgetSelected(SelectionEvent e) {
			handleTableSelectionChanged();
		}

		@Override
		public void widgetDefaultSelected(SelectionEvent e) {
		}
	}

	/**
	 * Default control creator for the information control replacer.
	 * @since 3.4
	 */
	private static class DefaultPresenterControlCreator extends AbstractReusableInformationControlCreator {
		@Override
		public IInformationControl doCreateInformationControl(Shell shell) {
			return new DefaultInformationControl(shell, true);
		}
	}

	/** The proposal table. */
	private Table fProposalTable;
	/** The table selection listener */
	private SelectionListener fSelectionListener= new TableSelectionListener();
	/** The delay after which additional information is displayed */
	private final int fDelay;
	/**
	 * The timer thread.
	 * @since 3.2
	 */
	private Timer fTimer;
	/**
	 * The proposal most recently set by {@link #showInformation(ICompletionProposal, Object)},
	 * possibly <code>null</code>.
	 * @since 3.2
	 */
	private ICompletionProposal fProposal;
	/**
	 * The information most recently set by {@link #showInformation(ICompletionProposal, Object)},
	 * possibly <code>null</code>.
	 * @since 3.2
	 */
	private Object fInformation;

	/**
	 * Creates a new additional information controller.
	 *
	 * @param creator the information control creator to be used by this controller
	 * @param delay time in milliseconds after which additional info should be displayed
	 */
	AdditionalInfoController(IInformationControlCreator creator, int delay) {
		super(creator);
		fDelay= delay;
		setAnchor(ANCHOR_RIGHT);
		setFallbackAnchors(new Anchor[] { ANCHOR_RIGHT, ANCHOR_LEFT, ANCHOR_BOTTOM });

	    /*
		 * Adjust the location by one pixel towards the proposal popup, so that the single pixel
		 * border of the additional info popup overlays with the border of the popup. This avoids
		 * having a double black line.
		 */
	    int spacing= -1;
		setMargins(spacing, spacing); // see also adjustment in #computeLocation

		InformationControlReplacer replacer= new InformationControlReplacer(new DefaultPresenterControlCreator());
		getInternalAccessor().setInformationControlReplacer(replacer);
	}

	@Override
	public void install(Control control) {

		if (fProposalTable == control) {
			// already installed
			return;
		}

		super.install(control.getShell());

		Assert.isTrue(control instanceof Table);
		fProposalTable= (Table) control;
		fProposalTable.addSelectionListener(fSelectionListener);
		getInternalAccessor().getInformationControlReplacer().install(fProposalTable);

		fTimer= new Timer(fProposalTable.getDisplay(), fDelay) {
			@Override
			protected void showInformation(ICompletionProposal proposal, Object info) {
				InformationControlReplacer replacer= getInternalAccessor().getInformationControlReplacer();
				if (replacer != null)
					replacer.hideInformationControl();
				AdditionalInfoController.this.showInformation(proposal, info);
			}
		};
	}

	@Override
	public void disposeInformationControl() {

		if (fTimer !=null) {
			fTimer.terminate();
			fTimer= null;
		}

		fProposal= null;
		fInformation= null;

		if (fProposalTable != null && !fProposalTable.isDisposed()) {
			fProposalTable.removeSelectionListener(fSelectionListener);
			fProposalTable= null;
		}

		super.disposeInformationControl();
	}

	/**
	 *Handles a change of the line selected in the associated selector.
	 */
	public void handleTableSelectionChanged() {

		if (fProposalTable != null && !fProposalTable.isDisposed() && fProposalTable.isVisible()) {
			TableItem[] selection= fProposalTable.getSelection();
			if (selection != null && selection.length > 0) {

				TableItem item= selection[0];

				Object d= item.getData();
				if (d instanceof ICompletionProposal) {
					ICompletionProposal p= (ICompletionProposal) d;
					fTimer.reset(p);
				}
			}
		}
	}

	void showInformation(ICompletionProposal proposal, Object info) {
		if (fProposalTable == null || fProposalTable.isDisposed())
			return;

		if (fProposal == proposal && ((info == null && fInformation == null) || (info != null && info.equals(fInformation))))
			return;

        fInformation= info;
        fProposal= proposal;
        showInformation();
    }

	@Override
	protected void computeInformation() {
		if (fProposal instanceof ICompletionProposalExtension3)
			setCustomInformationControlCreator(((ICompletionProposalExtension3) fProposal).getInformationControlCreator());
		else
			setCustomInformationControlCreator(null);

		// compute subject area
		Point size= fProposalTable.getShell().getSize();

		// set information & subject area
		setInformation(fInformation, new Rectangle(0, 0, size.x, size.y));
	}

	@Override
	protected Point computeLocation(Rectangle subjectArea, Point controlSize, Anchor anchor) {
	    Point location= super.computeLocation(subjectArea, controlSize, anchor);

	    /*
		 * The location is computed using subjectControl.toDisplay(), which does not include the
		 * trim of the subject control. As we want the additional info popup aligned with the outer
		 * coordinates of the proposal popup, adjust this here
		 */
	    Rectangle trim= fProposalTable.getShell().computeTrim(0, 0, 0, 0);
	    location.x += trim.x;
	    location.y += trim.y;

		return location;
	}

	@Override
	protected Point computeSizeConstraints(Control subjectControl, IInformationControl informationControl) {
		// at least as big as the proposal table
		Point sizeConstraint= super.computeSizeConstraints(subjectControl, informationControl);
		Point size= subjectControl.getShell().getSize();

	    // AbstractInformationControlManager#internalShowInformationControl(Rectangle, Object) adds trims
		// to the computed constraints. Need to remove them here, to make the outer bounds of the additional
		// info shell fit the bounds of the proposal shell:
	    if (fInformationControl instanceof IInformationControlExtension3) {
	    	Rectangle shellTrim= ((IInformationControlExtension3) fInformationControl).computeTrim();
	    	size.x -= shellTrim.width;
	    	size.y -= shellTrim.height;
	    }

		if (sizeConstraint.x < size.x)
			sizeConstraint.x= size.x;
		if (sizeConstraint.y < size.y)
			sizeConstraint.y= size.y;
		return sizeConstraint;
	}

	@Override
	protected void hideInformationControl() {
		super.hideInformationControl();
		if (fTimer != null)
			fTimer.reset(null);
	}

	@Override
	protected boolean canClearDataOnHide() {
		return false; // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=293176
	}

	/**
	 * @return the current information control, or <code>null</code> if none available
	 */
	public IInformationControl getCurrentInformationControl2() {
		return getInternalAccessor().getCurrentInformationControl();
	}
}


