/*******************************************************************************
 * Copyright (c) 2008, 2019 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.debug.ui;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.debug.internal.ui.SWTFactory;
import org.eclipse.debug.internal.ui.model.elements.ElementContentProvider;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IChildrenUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IPresentationContext;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdateListener;
import org.eclipse.debug.internal.ui.viewers.model.provisional.PresentationContext;
import org.eclipse.debug.internal.ui.viewers.model.provisional.TreeModelViewer;
import org.eclipse.debug.internal.ui.views.variables.details.DefaultDetailPane;
import org.eclipse.debug.internal.ui.views.variables.details.DetailPaneProxy;
import org.eclipse.debug.internal.ui.views.variables.details.IDetailPaneContainer;
import org.eclipse.debug.ui.AbstractDebugView;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.AbstractInformationControl;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IInformationControlExtension2;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeSelection;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPartSite;

/**
 * Creates an information control to display an expression in a hover control.
 *
 * @noextend This class is not intended to be subclassed by clients.
 *
 * @since 3.3
 */
public class ExpressionInformationControlCreator implements IInformationControlCreator {

	class ExpressionInformationControl extends AbstractInformationControl implements IInformationControlExtension2 {

		/**
		 * Dialog setting key for height
		 */
		private static final String HEIGHT = "HEIGHT"; //$NON-NLS-1$

		/**
		 * Dialog setting key for width.
		 */
		private static final String WIDTH = "WIDTH"; //$NON-NLS-1$

		/**
		 * Dialog setting key for tree sash weight
		 */
		private static final String SASH_WEIGHT_TREE = "SashWeightTree"; //$NON-NLS-1$

		/**
		 * Dialog setting key for details sash weight
		 */
		private static final String SASH_WEIGHT_DETAILS = "SashWeightDetails"; //$NON-NLS-1$

		/**
		 * Variable to display.
		 */
		private IVariable fVariable;

		private IPresentationContext fContext;
	    private TreeModelViewer fViewer;
	    private SashForm fSashForm;
	    private Composite fDetailPaneComposite;
	    private DetailPaneProxy fDetailPane;
	    private Tree fTree;

		/**
	     * Creates the content for the root element of the tree viewer in the hover
	     */
	    private class TreeRoot extends ElementContentProvider {
			/* (non-Javadoc)
			 * @see org.eclipse.debug.internal.ui.viewers.model.provisional.elements.ElementContentProvider#getChildCount(java.lang.Object, org.eclipse.debug.internal.ui.viewers.provisional.IPresentationContext)
			 */
			@Override
			protected int getChildCount(Object element, IPresentationContext context, IViewerUpdate monitor) throws CoreException {
				return 1;
			}
			/* (non-Javadoc)
			 * @see org.eclipse.debug.internal.ui.viewers.model.provisional.elements.ElementContentProvider#getChildren(java.lang.Object, int, int, org.eclipse.debug.internal.ui.viewers.provisional.IPresentationContext)
			 */
			@Override
			protected Object[] getChildren(Object parent, int index, int length, IPresentationContext context, IViewerUpdate monitor) throws CoreException {
				return new Object[] { fVariable };
			}

			/* (non-Javadoc)
			 * @see org.eclipse.debug.internal.ui.viewers.model.provisional.elements.ElementContentProvider#supportsContextId(java.lang.String)
			 */
			@Override
			protected boolean supportsContextId(String id) {
				return true;
			}
	    }

		/**
		 * Inner class implementing IDetailPaneContainer methods.  Handles changes to detail
		 * pane and provides limited access to the detail pane proxy.
		 */
		private class DetailPaneContainer implements IDetailPaneContainer{

			/* (non-Javadoc)
			 * @see org.eclipse.debug.internal.ui.views.variables.details.IDetailPaneContainer#getCurrentPaneID()
			 */
			@Override
			public String getCurrentPaneID() {
				return fDetailPane.getCurrentPaneID();
			}

			/* (non-Javadoc)
			 * @see org.eclipse.debug.internal.ui.views.variables.details.IDetailPaneContainer#getCurrentSelection()
			 */
			@Override
			public IStructuredSelection getCurrentSelection() {
				return (IStructuredSelection)fViewer.getSelection();
			}

			/* (non-Javadoc)
			 * @see org.eclipse.debug.internal.ui.views.variables.details.IDetailPaneContainer#refreshDetailPaneContents()
			 */
			@Override
			public void refreshDetailPaneContents() {
				fDetailPane.display(getCurrentSelection());
			}

			/* (non-Javadoc)
			 * @see org.eclipse.debug.internal.ui.views.variables.details.IDetailPaneContainer#getParentComposite()
			 */
			@Override
			public Composite getParentComposite() {
				return fDetailPaneComposite;
			}

			/* (non-Javadoc)
			 * @see org.eclipse.debug.internal.ui.views.variables.details.IDetailPaneContainer#getWorkbenchPartSite()
			 */
			@Override
			public IWorkbenchPartSite getWorkbenchPartSite() {
				return null;
			}

			/* (non-Javadoc)
			 * @see org.eclipse.debug.internal.ui.views.variables.details.IDetailPaneContainer#paneChanged(java.lang.String)
			 */
			@Override
			public void paneChanged(String newPaneID) {
				if (newPaneID.equals(DefaultDetailPane.ID)){
					fDetailPane.getCurrentControl().setForeground(getSystemForegroundColor());
					fDetailPane.getCurrentControl().setBackground(getSystemBackgroundColor());
				}
			}

		}

		/**
		 * Constructs a new control in the given shell.
		 *
		 * @param parentShell shell
		 * @param resize whether resize is supported
		 */
		ExpressionInformationControl(Shell parentShell, boolean resize) {
			super(parentShell, resize);
			create();
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.text.AbstractInformationControl#computeSizeHint()
		 */
		@Override
		public Point computeSizeHint() {
			IDialogSettings settings = getDialogSettings(false);
			if (settings != null) {
				int x = getIntSetting(settings, WIDTH);
				if (x > 0) {
					int y = getIntSetting(settings, HEIGHT);
					if (y > 0) {
						return new Point(x,y);
					}
				}
			}
			return super.computeSizeHint();
		}

		/**
		 * Returns the dialog settings for this hover or <code>null</code> if none
		 *
		 * @param create whether to create the settings
		 */
		private IDialogSettings getDialogSettings(boolean create) {
			IDialogSettings settings = JDIDebugUIPlugin.getDefault().getDialogSettings();
			IDialogSettings section = settings.getSection(this.getClass().getName());
			if (section == null && create) {
				section = settings.addNewSection(this.getClass().getName());
			}
			return section;
		}

		/**
		 * Returns an integer value in the given dialog settings or -1 if none.
		 *
		 * @param settings dialog settings
		 * @param key key
		 * @return value or -1 if not present
		 */
		private int getIntSetting(IDialogSettings settings, String key) {
			try {
				return settings.getInt(key);
			} catch (NumberFormatException e) {
				return -1;
			}
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.text.AbstractInformationControl#dispose()
		 */
		@Override
		public void dispose() {
			persistSettings(getShell());
            fContext.dispose();
			super.dispose();
		}

		/**
		 * Persists dialog settings.
		 *
		 * @param shell
		 */
		private void persistSettings(Shell shell) {
			if (shell != null && !shell.isDisposed()) {
				if (isResizable()) {
					IDialogSettings settings = getDialogSettings(true);
					Point size = shell.getSize();
					settings.put(WIDTH, size.x);
					settings.put(HEIGHT, size.y);
					int[] weights = fSashForm.getWeights();
					settings.put(SASH_WEIGHT_TREE, weights[0]);
					settings.put(SASH_WEIGHT_DETAILS, weights[1]);
				}
			}
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.text.AbstractInformationControl#setVisible(boolean)
		 */
		@Override
		public void setVisible(boolean visible) {
			if (!visible) {
				persistSettings(getShell());
			}
			super.setVisible(visible);
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.text.AbstractInformationControl#createContent(org.eclipse.swt.widgets.Composite)
		 */
		@Override
		protected void createContent(Composite parent) {

	        fSashForm = new SashForm(parent, parent.getStyle());
	        fSashForm.setOrientation(SWT.VERTICAL);

		    // update presentation context
	        AbstractDebugView view = getViewToEmulate();
	        fContext = new PresentationContext(IDebugUIConstants.ID_VARIABLE_VIEW);
	        if (view != null) {
	        	// copy over properties
	        	IPresentationContext copy = ((TreeModelViewer)view.getViewer()).getPresentationContext();
	        	String[] properties = copy.getProperties();
	        	for (int i = 0; i < properties.length; i++) {
					String key = properties[i];
					fContext.setProperty(key, copy.getProperty(key));
				}
	        }

	        fViewer = new TreeModelViewer(fSashForm, SWT.NO_TRIM | SWT.MULTI | SWT.VIRTUAL, fContext);
	        fViewer.setAutoExpandLevel(1);

	        if (view != null) {
	        	// copy over filters
	        	StructuredViewer structuredViewer = (StructuredViewer) view.getViewer();
	            if (structuredViewer != null) {
	                ViewerFilter[] filters = structuredViewer.getFilters();
	                for (int i = 0; i < filters.length; i++) {
	                    fViewer.addFilter(filters[i]);
	                }
	            }
	        }

	        fDetailPaneComposite = SWTFactory.createComposite(fSashForm, 1, 1, GridData.FILL_BOTH);
	        Layout layout = fDetailPaneComposite.getLayout();
	        if (layout instanceof GridLayout) {
				GridLayout gl = (GridLayout) layout;
				gl.marginHeight = 0;
				gl.marginWidth = 0;
			}

	        fDetailPane = new DetailPaneProxy(new DetailPaneContainer());
	        fDetailPane.display(null); // Bring up the default pane so the user doesn't see an empty composite

	        fTree = fViewer.getTree();
	        fTree.addSelectionListener(new SelectionListener() {
	            @Override
				public void widgetSelected(SelectionEvent e) {
	            	fDetailPane.display((IStructuredSelection)fViewer.getSelection());
	            }
	            @Override
				public void widgetDefaultSelected(SelectionEvent e) {}
	        });

	        initSashWeights();

	        // add update listener to auto-select and display details of root expression
	        fViewer.addViewerUpdateListener(new IViewerUpdateListener() {
				@Override
				public void viewerUpdatesComplete() {
				}
				@Override
				public void viewerUpdatesBegin() {
				}
				@Override
				public void updateStarted(IViewerUpdate update) {
				}
				@Override
				public void updateComplete(IViewerUpdate update) {
					if (update instanceof IChildrenUpdate) {
						TreeSelection selection = new TreeSelection(new TreePath(new Object[]{fVariable}));
						fViewer.setSelection(selection);
						fDetailPane.display(selection);
						fViewer.removeViewerUpdateListener(this);
					}
				}
			});

	        setForegroundColor(getSystemForegroundColor());
	        setBackgroundColor(getSystemBackgroundColor());
		}


		/**
	     * Attempts to find an appropriate view to emulate, this will either be the
	     * variables view or the expressions view.
	     * @return a view to emulate or <code>null</code>
	     */
	    private AbstractDebugView getViewToEmulate() {
	        IWorkbenchPage page = JDIDebugUIPlugin.getActiveWorkbenchWindow().getActivePage();
	        AbstractDebugView expressionsView = (AbstractDebugView) page.findView(IDebugUIConstants.ID_EXPRESSION_VIEW);
	        if (expressionsView != null && expressionsView.isVisible()) {
	            return expressionsView;
	        }
	        AbstractDebugView variablesView = (AbstractDebugView) page.findView(IDebugUIConstants.ID_VARIABLE_VIEW);
	        if (variablesView != null && variablesView.isVisible()) {
	            return variablesView;
	        }
	        if (expressionsView != null) {
	            return expressionsView;
	        }
	        return variablesView;
	    }

		/**
	     * Initializes the sash form weights from the preference store (using default values if
	     * no sash weights were stored previously).
	     */
	    protected void initSashWeights(){
	    	IDialogSettings settings = getDialogSettings(false);
	    	if (settings != null) {
		    	int tree = getIntSetting(settings, SASH_WEIGHT_TREE);
		    	if (tree > 0) {
		    		int details = getIntSetting(settings, SASH_WEIGHT_DETAILS);
		    		if (details > 0) {
		    			fSashForm.setWeights(new int[]{tree, details});
		    		}
		    	}
	    	}
	    }

	    /* (non-Javadoc)
	     * @see org.eclipse.jface.text.AbstractInformationControl#setForegroundColor(org.eclipse.swt.graphics.Color)
	     */
	    @Override
	    public void setForegroundColor(Color foreground) {
	    	super.setForegroundColor(foreground);
	    	fDetailPaneComposite.setForeground(foreground);
	    	fTree.setForeground(foreground);
	    }

		/* (non-Javadoc)
		 * @see org.eclipse.jface.text.AbstractInformationControl#setBackgroundColor(org.eclipse.swt.graphics.Color)
		 */
		@Override
		public void setBackgroundColor(Color background) {
			super.setBackgroundColor(background);
			fDetailPaneComposite.setBackground(background);
			fTree.setBackground(background);
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.text.AbstractInformationControl#setFocus()
		 */
		@Override
		public void setFocus() {
			super.setFocus();
			fTree.setFocus();
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.text.IInformationControlExtension#hasContents()
		 */
		@Override
		public boolean hasContents() {
			return fVariable != null;
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.text.IInformationControlExtension2#setInput(java.lang.Object)
		 */
		@Override
		public void setInput(Object input) {
			if (input instanceof IVariable) {
				fVariable = (IVariable) input;
		        fViewer.setInput(new TreeRoot());
			}
		}

		/* (non-Javadoc)
		 * @see org.eclipse.jface.text.AbstractInformationControl#getInformationPresenterControlCreator()
		 */
		@Override
		public IInformationControlCreator getInformationPresenterControlCreator() {
			return new ExpressionInformationControlCreator() {
				/* (non-Javadoc)
				 * @see org.eclipse.jdt.internal.debug.ui.ExpressionInformationControlCreator#createInformationControl(org.eclipse.swt.widgets.Shell)
				 */
				@Override
				public IInformationControl createInformationControl(Shell shell) {
					return new ExpressionInformationControl(shell, true);
				}
			};
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jface.text.IInformationControlCreator#createInformationControl(org.eclipse.swt.widgets.Shell)
	 */
	@Override
	public IInformationControl createInformationControl(Shell parent) {
		return new ExpressionInformationControl(parent, false);
	}

	private static Color getSystemForegroundColor() {
		ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
		Color foreground = colorRegistry.get(JFacePreferences.INFORMATION_FOREGROUND_COLOR);

		if (foreground == null) {
			return JFaceColors.getInformationViewerForegroundColor(Display.getDefault());
		}

		return foreground;
	}

	public static Color getSystemBackgroundColor() {
		ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
		Color background = colorRegistry.get(JFacePreferences.INFORMATION_BACKGROUND_COLOR);

		if (background == null) {
			return JFaceColors.getInformationViewerBackgroundColor(Display.getDefault());
		}

		return background;
	}

}
