/*******************************************************************************
 * Copyright (c) 2007, 2012 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:
 *     Anton Leherbauer (Wind River Systems) - initial API and implementation
 *     IBM Corporation
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.text.c.hover;

import java.util.ArrayList;
import java.util.Collection;

import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.Splitter;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.compare.structuremergeviewer.DocumentRangeNode;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IHandler;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.layout.PixelConverter;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.text.AbstractInformationControlManager;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.contexts.IContextActivation;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.handlers.IHandlerActivation;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.menus.CommandContributionItem;
import org.eclipse.ui.menus.CommandContributionItemParameter;
import org.eclipse.ui.texteditor.ITextEditor;

import org.eclipse.cdt.core.dom.ast.IASTFileLocation;
import org.eclipse.cdt.core.dom.ast.IMacroBinding;
import org.eclipse.cdt.core.dom.rewrite.MacroExpansionExplorer.IMacroExpansionStep;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.PreferenceConstants;
import org.eclipse.cdt.ui.text.ICPartitions;

import org.eclipse.cdt.internal.ui.CPluginImages;
import org.eclipse.cdt.internal.ui.editor.CSourceViewer;
import org.eclipse.cdt.internal.ui.editor.ICEditorActionDefinitionIds;
import org.eclipse.cdt.internal.ui.text.AbstractCompareViewerInformationControl;
import org.eclipse.cdt.internal.ui.text.CTextTools;
import org.eclipse.cdt.internal.ui.text.SimpleCSourceViewerConfiguration;
import org.eclipse.cdt.internal.ui.util.EditorUtility;

/**
 * Information control for macro expansion exploration.
 *
 * @since 5.0
 */
public class CMacroExpansionExplorationControl extends AbstractCompareViewerInformationControl {

	private static final String COMMAND_ID_EXPANSION_BACK= "org.eclipse.cdt.ui.hover.backwardMacroExpansion"; //$NON-NLS-1$
	private static final String COMMAND_ID_EXPANSION_FORWARD= "org.eclipse.cdt.ui.hover.forwardMacroExpansion"; //$NON-NLS-1$
	private static final String CONTEXT_ID_MACRO_EXPANSION_HOVER= "org.eclipse.cdt.ui.macroExpansionHoverScope"; //$NON-NLS-1$

	/** Dialog settings key to persist control bounds. */
	public static final String KEY_CONTROL_BOUNDS = "org.eclipse.cdt.ui.text.hover.CMacroExpansionExploration"; //$NON-NLS-1$

	private static final String KEY_CONTROL_BOUNDS_INTERNAL = KEY_CONTROL_BOUNDS + ".internal"; //$NON-NLS-1$

	private static final int MIN_WIDTH = 320;
	private static final int MIN_HEIGHT = 180;

	private static class CDiffNode extends DocumentRangeNode implements ITypedElement {
		public CDiffNode(DocumentRangeNode parent, int type, String id, IDocument doc, int start, int length) {
			super(parent, type, id, doc, start, length);
		}
		public CDiffNode(int type, String id, IDocument doc, int start, int length) {
			super(type, id, doc, start, length);
		}
		@Override
		public String getName() {
			return getId();
		}
		@Override
		public String getType() {
			return "c2"; //$NON-NLS-1$
		}
		@Override
		public Image getImage() {
			return null;
		}
	}

	private IHandlerService fHandlerService;
	private Collection<IHandlerActivation> fHandlerActivations;
	private IContextService fContextService;
	private IContextActivation fContextActivation;
	private int fIndex;
	private CMacroExpansionInput fInput;
	private CMacroCompareViewer fMacroCompareViewer;
	private ISourceViewer fMacroViewer;
	private StyledText fMacroText;
	private boolean fRestoreSize;
	private Point fDefaultSize;
	private ScrolledComposite fTextScroller;

	/**
	 * Creates a new control for use as a "quick view" where the control immediately takes the focus.
	 * 
	 * @param parent  parent shell
	 * @param input  the input object, may be <code>null</code>
	 */
	public CMacroExpansionExplorationControl(Shell parent, CMacroExpansionInput input) {
		super(parent, new ToolBarManager(SWT.FLAT));
		setMacroExpansionInput(input);
		addFocusListener(new FocusListener() {
			@Override
			public void focusGained(FocusEvent e) {
				registerCommandHandlers();
			}
			@Override
			public void focusLost(FocusEvent e) {
				unregisterCommandHandlers();
			}
		});
		getShell().addListener(SWT.Close, new Listener() {
			@Override
			public void handleEvent(Event event) {
				widgetClosed();
			}});
		fillToolBar();
		setDefaultSize(MIN_WIDTH, MIN_HEIGHT);
	}

	/**
	 * Creates a new control for use as a "quick view" where the control immediately takes the focus.
	 * 
	 * @param parent  parent shell
	 */
	public CMacroExpansionExplorationControl(Shell parent) {
		this(parent, null);
	}

	/**
	 * Creates a new control for use as a "quick view" where the control immediately takes the focus.
	 * 
	 * @param parent  parent shell
	 * @param restoreSize  whether control size should be restored
	 */
	public CMacroExpansionExplorationControl(Shell parent, boolean restoreSize) {
		this(parent, null);
		fRestoreSize= restoreSize;
		if (restoreSize) {
			restoreSize();
		}
	}

	private void restoreSize() {
		String sectionName= KEY_CONTROL_BOUNDS_INTERNAL;
		IDialogSettings settings= CUIPlugin.getDefault().getDialogSettings().getSection(sectionName);
		if (settings == null) {
			return;
		}
		try {
			int width= settings.getInt(AbstractInformationControlManager.STORE_SIZE_WIDTH);
			int height= settings.getInt(AbstractInformationControlManager.STORE_SIZE_HEIGHT);
			setDefaultSize(width, height);
		} catch (NumberFormatException exc) {
			// Ignore
		}
	}
	
	private void setDefaultSize(int width, int height) {
		fDefaultSize= new Point(width, height);
	}

	private void storeSize() {
		final Shell shell = getShell();
		if (shell == null) {
			return;
		}
		String sectionName= KEY_CONTROL_BOUNDS_INTERNAL;
		IDialogSettings settings= CUIPlugin.getDefault().getDialogSettings().getSection(sectionName);
		if (settings == null) {
			settings= CUIPlugin.getDefault().getDialogSettings().addNewSection(sectionName);
		}
		Point size= shell.getSize();
		settings.put(AbstractInformationControlManager.STORE_SIZE_WIDTH, size.x);
		settings.put(AbstractInformationControlManager.STORE_SIZE_HEIGHT, size.y);
	}

	@Override
	protected boolean hasHeader() {
		return true;
	}

	@Override
	protected CompareViewerControl createCompareViewerControl(Composite parent, int style, CompareConfiguration compareConfig) {
		Splitter splitter= new Splitter(parent, SWT.VERTICAL);
		splitter.setLayoutData(new GridData(GridData.FILL_BOTH));
		// text viewer to show the macro definition
		fTextScroller= new ScrolledComposite(splitter, SWT.H_SCROLL | SWT.V_SCROLL);
		fMacroViewer= createSourceViewer(fTextScroller, style);
		final StyledText textWidget= fMacroViewer.getTextWidget();
		fTextScroller.setBackground(textWidget.getBackground());
		fTextScroller.setContent(textWidget);
		final Point size= textWidget.computeSize(SWT.DEFAULT, SWT.DEFAULT);
		textWidget.setSize(size);
		// compare viewer
		CompareViewerControl control= super.createCompareViewerControl(splitter, style, compareConfig);
		splitter.setWeights(new int[] { 20, 80 });
		return control;
	}

	@Override
	protected Viewer createContentViewer(Composite parent, ICompareInput input, CompareConfiguration cc) {
		fMacroCompareViewer= new CMacroCompareViewer(parent, SWT.NULL, cc);
		if (fInput != null) {
			fMacroCompareViewer.setMacroExpansionInput(fInput);
			fMacroCompareViewer.setMacroExpansionStep(fIndex);
		}
		return fMacroCompareViewer;
	}

	protected ISourceViewer createSourceViewer(Composite parent, int style) {
		IPreferenceStore store= CUIPlugin.getDefault().getCombinedPreferenceStore();
		SourceViewer sourceViewer= new CSourceViewer(parent, null, null, false, style, store);
		CTextTools tools= CUIPlugin.getDefault().getTextTools();
		sourceViewer.configure(new SimpleCSourceViewerConfiguration(tools.getColorManager(), store, null, ICPartitions.C_PARTITIONING, false));
		sourceViewer.setEditable(false);

		fMacroText= sourceViewer.getTextWidget();

		Font font= JFaceResources.getFont(PreferenceConstants.EDITOR_TEXT_FONT);
		fMacroText.setFont(font);

		GridData gd= new GridData(GridData.BEGINNING | GridData.FILL_BOTH);
		gd.heightHint= fMacroText.getLineHeight() * 2;
		fMacroText.setLayoutData(gd);

		final Document doc= new Document();
		CUIPlugin.getDefault().getTextTools().setupCDocument(doc);
		sourceViewer.setDocument(doc);
		return sourceViewer;
	}

	protected void unregisterCommandHandlers() {
		if (fHandlerService != null) {
			fHandlerService.deactivateHandlers(fHandlerActivations);
			fHandlerActivations.clear();
			fHandlerService= null;
		}
		if (fContextActivation != null) {
			fContextService.deactivateContext(fContextActivation);
			fContextActivation= null;
		}
	}

	protected void registerCommandHandlers() {
		if (fContextActivation != null) {
			return;
		}
        IHandler backwardHandler= new AbstractHandler() {
            @Override
			public Object execute(ExecutionEvent event) throws ExecutionException {
                backward();
                return null;
            }
        };
        IHandler forwardHandler= new AbstractHandler() {
            @Override
			public Object execute(ExecutionEvent event) throws ExecutionException {
                forward();
                return null;
            }
        };
        IHandler gotoDefinitionHandler= new AbstractHandler() {
            @Override
			public Object execute(ExecutionEvent event) throws ExecutionException {
                gotoMacroDefinition();
                return null;
            }
        };

        IWorkbench workbench= PlatformUI.getWorkbench();
        fHandlerService= (IHandlerService) workbench.getService(IHandlerService.class);
        fContextService= (IContextService) workbench.getService(IContextService.class);
        fContextActivation= fContextService.activateContext(CONTEXT_ID_MACRO_EXPANSION_HOVER);
        fHandlerActivations= new ArrayList<IHandlerActivation>();
        fHandlerActivations.add(fHandlerService.activateHandler(COMMAND_ID_EXPANSION_BACK, backwardHandler));
        fHandlerActivations.add(fHandlerService.activateHandler(COMMAND_ID_EXPANSION_FORWARD, forwardHandler));
        fHandlerActivations.add(fHandlerService.activateHandler(ICEditorActionDefinitionIds.OPEN_DECL, gotoDefinitionHandler));

        String infoText= getInfoText();
        if (infoText != null) {
            setStatusText(infoText);
            //bug 234952 - truncation in the info label
            PixelConverter converter = new PixelConverter(getShell());
            Point pt = getShell().getSize();
            int stringLengthInPixel = converter.convertWidthInCharsToPixels(infoText.length()+5);
            if (pt.x < stringLengthInPixel) {
            	getShell().setSize(new Point(stringLengthInPixel, pt.y));
            }
        }
	}

	private void fillToolBar() {
		ToolBarManager mgr = getToolBarManager();
		if (mgr == null) {
			return;
		}
        IWorkbench workbench= PlatformUI.getWorkbench();
        CommandContributionItemParameter param= new CommandContributionItemParameter(workbench, null, COMMAND_ID_EXPANSION_BACK, CommandContributionItem.STYLE_PUSH);
        param.icon= CPluginImages.DESC_ELCL_NAVIGATE_BACKWARD;
		mgr.add(new CommandContributionItem(param));
		param = new CommandContributionItemParameter(workbench, null, COMMAND_ID_EXPANSION_FORWARD, CommandContributionItem.STYLE_PUSH);
        param.icon= CPluginImages.DESC_ELCL_NAVIGATE_FORWARD;
        mgr.add(new CommandContributionItem(param));
        param = new CommandContributionItemParameter(workbench, null, ICEditorActionDefinitionIds.OPEN_DECL, CommandContributionItem.STYLE_PUSH);
        param.icon = CPluginImages.DESC_ELCL_OPEN_DECLARATION;
        mgr.add(new CommandContributionItem(param));
        mgr.update(true);
	}

	protected final void gotoMacroDefinition() {
		int index= fIndex < getStepCount() ? fIndex : 0;
		final IMacroExpansionStep step= fInput.fExplorer.getExpansionStep(index);
		IASTFileLocation fileLocation= step.getLocationOfExpandedMacroDefinition();
		if (fileLocation != null) {
			final IPath path= new Path(fileLocation.getFileName());
			final int offset= fileLocation.getNodeOffset();
			final int length= fileLocation.getNodeLength();
			IEditorPart editor;
			try {
				editor = EditorUtility.openInEditor(path, null);
				if (editor instanceof ITextEditor) {
					ITextEditor textEditor = (ITextEditor)editor;
					textEditor.selectAndReveal(offset, length);
				}
				dispose();
			} catch (PartInitException exc) {
				CUIPlugin.log(exc);
			}
		}
	}

	protected final void forward() {
		fIndex= fixIndex(fIndex + 1);
		if (fIndex > getStepCount()) {
			fIndex= 0;
		}
		showExpansion();
	}

	protected final void backward() {
		--fIndex;
		if (fIndex < 0) {
			fIndex= fixIndex(getStepCount());
		}
		showExpansion();
	}

	/**
	 * Returns the text to be shown in the popups's information area.
	 * May return <code>null</code>.
	 *
	 * @return The text to be shown in the popup's information area or <code>null</code>
	 */
	private String getInfoText() {
		IWorkbench workbench= PlatformUI.getWorkbench();
		IBindingService bindingService= (IBindingService) workbench.getService(IBindingService.class);
		String formattedBindingBack= bindingService.getBestActiveBindingFormattedFor(COMMAND_ID_EXPANSION_BACK);
		String formattedBindingForward= bindingService.getBestActiveBindingFormattedFor(COMMAND_ID_EXPANSION_FORWARD);

		String infoText= null;
		if (formattedBindingBack != null && formattedBindingForward != null) {
			infoText= NLS.bind(CHoverMessages.CMacroExpansionControl_statusText, formattedBindingBack, formattedBindingForward);
		}
		return infoText;
	}

	@Override
	public void dispose() {
		unregisterCommandHandlers();
		super.dispose();
	}

	@Override
	public boolean restoresLocation() {
		return true;
	}

	@Override
	public boolean restoresSize() {
		return true;
	}

	@Override
	public void setSizeConstraints(int maxWidth, int maxHeight) {
		Point constraints= getSizeConstraints();
		if (constraints != null) {
			super.setSizeConstraints(Math.max(constraints.x, maxWidth), Math.max(constraints.y, maxHeight));
		} else {
			super.setSizeConstraints(maxWidth, maxHeight);
		}
	}

	@Override
	public void setSize(int width, int height) {
		if (fDefaultSize != null) {
			width= Math.max(fDefaultSize.x, width);
			height= Math.max(fDefaultSize.y, height);
			fDefaultSize= null;
		}
		super.setSize(width, height);
	}

	@Override
	public void setInput(Object input) {
		if (input instanceof CMacroExpansionInput) {
			setMacroExpansionInput((CMacroExpansionInput) input);
		} else if (input != null) {
			if (fMacroCompareViewer != null) {
				fMacroCompareViewer.setMacroExpansionStep(fIndex);
			}
			super.setInput(input);
		} else {
			setMacroExpansionInput(null);
			super.setInput(input);
		}
	}

	private void widgetClosed() {
		if (fRestoreSize) {
			storeSize();
			fRestoreSize= false;
		}
		unregisterCommandHandlers();
	}

	@Override
	public void setVisible(boolean visible) {
		if (!visible) {
			if (fRestoreSize) {
				storeSize();
				fRestoreSize= false;
			}
		}
		super.setVisible(visible);
		if (visible) {
			setFocus();
		}
	}

	@Override
	public Rectangle computeTrim() {
		Rectangle trim= super.computeTrim();
		addInternalTrim(trim);
		return trim;
	}

	/**
	 * Adds the internal trimmings to the given trim of the shell.
	 * 
	 * @param trim the shell's trim, will be updated
	 * @since 5.0
	 */
	private void addInternalTrim(Rectangle trim) {
		Rectangle textTrim= fMacroText.computeTrim(0, 0, 0, 0);
		trim.x+= textTrim.x;
		trim.y+= textTrim.y;
		trim.width+= textTrim.width;
		trim.height+= textTrim.height;
	}

	/**
	 * Set the input for this information control.
	 * @param input
	 */
	private void setMacroExpansionInput(CMacroExpansionInput input) {
		fInput= input;
		if (fMacroCompareViewer != null) {
			fMacroCompareViewer.setMacroExpansionInput(input);
		}
		if (fInput != null) {
			fIndex= fixIndex(input.fStartWithFullExpansion ? getStepCount() : 0);
			showExpansion();
		}
	}

	private int fixIndex(int index) {
		if (getStepCount() == 1 && index == 1) {
			return 0;
		}
		return index;
	}

	private int getStepCount() {
		return fInput.fExplorer.getExpansionStepCount();
	}

	private void showExpansion() {
		final int idxLeft= fIndex == getStepCount() ? 0 : fIndex;
		final int idxRight= fIndex + 1;

		CompareConfiguration config= getCompareConfiguration();
		config.setLeftLabel(getLabelForIndex(idxLeft));
		config.setRightLabel(getLabelForIndex(idxRight));

		final ITypedElement left= getContentForIndex(fIndex, true);
		final ITypedElement right= getContentForIndex(fIndex, false);
		
		setTitleText(NLS.bind(CHoverMessages.CMacroExpansionControl_title_macroExpansionExploration, getStepCount()));
		fMacroViewer.getDocument().set(getMacroText(fIndex));
		final StyledText textWidget= fMacroViewer.getTextWidget();
		final Point size= textWidget.computeSize(SWT.DEFAULT, SWT.DEFAULT);
		textWidget.setSize(size);
		setInput(createCompareInput(null, left, right));
	}

	private String getLabelForIndex(int index) {
		if (index == 0) {
			return CHoverMessages.CMacroExpansionControl_title_original;
		} else if (index < getStepCount()) {
			return NLS.bind(CHoverMessages.CMacroExpansionControl_title_expansion,
					String.valueOf(index), String.valueOf(getStepCount()));
		} else {
			return CHoverMessages.CMacroExpansionControl_title_fullyExpanded;
		}
	}
	private Object createCompareInput(ITypedElement original, ITypedElement left, ITypedElement right) {
		Differencer d= new Differencer();
		return d.findDifferences(false, new NullProgressMonitor(), null, original, left, right);
	}

	private ITypedElement getContentForIndex(int index, boolean before) {
		final IMacroExpansionStep expansionStep;
		if (index < getStepCount()) {
			expansionStep= fInput.fExplorer.getExpansionStep(index);
		} else {
			expansionStep= fInput.fExplorer.getFullExpansion();
		}
		final String text;
		if (before) {
			text= expansionStep.getCodeBeforeStep();
		} else {
			text= expansionStep.getCodeAfterStep();
		}
		final Document doc= new Document(text);
		CUIPlugin.getDefault().getTextTools().setupCDocument(doc);
		return new CDiffNode(0, String.valueOf(index), doc, 0, text.length());
	}

	private String getMacroText(int index) {
		index= index < getStepCount() ? index : 0;
		final IMacroExpansionStep expansionStep= fInput.fExplorer.getExpansionStep(index);
		IMacroBinding binding= expansionStep.getExpandedMacro();
		StringBuffer buffer= new StringBuffer();
		buffer.append("#define ").append(binding.getName()); //$NON-NLS-1$
		char[][] params= binding.getParameterList();
		if (params != null) {
			buffer.append('(');
			for (int i= 0; i < params.length; i++) {
				if (i > 0) {
					buffer.append(',');
					buffer.append(' ');
				}
				char[] param= params[i];
				buffer.append(new String(param));
			}
			buffer.append(')');
		}
		buffer.append(' ');
		if (!binding.isDynamic()) {
			buffer.append(binding.getExpansionImage());
		}
		else {
			ReplaceEdit[] replacements= expansionStep.getReplacements();
			if (replacements.length == 1) {
				buffer.append(replacements[0].getText());
			}
		}
		return buffer.toString();
	}

}
