/*******************************************************************************
 * Copyright (c) 2000, 2018 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.ui.compare;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.widgets.Composite;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ProjectScope;

import org.eclipse.jface.action.IAction;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.jface.text.source.CompositeRuler;
import org.eclipse.jface.text.source.IOverviewRuler;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.SourceViewer;

import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IPageListener;
import org.eclipse.ui.IStorageEditorInput;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.actions.PartEventAction;
import org.eclipse.ui.contexts.IContextService;

import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.eclipse.ui.texteditor.ChainedPreferenceStore;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.ITextEditorActionConstants;
import org.eclipse.ui.texteditor.ITextEditorExtension3;

import org.eclipse.ui.editors.text.EditorsUI;

import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.IResourceProvider;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.contentmergeviewer.IIgnoreWhitespaceContributor;
import org.eclipse.compare.contentmergeviewer.ITokenComparator;
import org.eclipse.compare.contentmergeviewer.TextMergeViewer;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.compare.structuremergeviewer.IDiffContainer;
import org.eclipse.compare.structuremergeviewer.IDiffElement;

import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;

import org.eclipse.jdt.ui.text.IJavaPartitions;
import org.eclipse.jdt.ui.text.JavaSourceViewerConfiguration;
import org.eclipse.jdt.ui.text.JavaTextTools;

import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor;
import org.eclipse.jdt.internal.ui.text.PreferencesAdapter;


public class JavaMergeViewer extends TextMergeViewer {

	private IPropertyChangeListener fPreferenceChangeListener;
	private IPreferenceStore fPreferenceStore;
	private Map <SourceViewer, JavaSourceViewerConfiguration> fSourceViewerConfiguration;
	private Map <SourceViewer, CompilationUnitEditorAdapter> fEditor;
	private ArrayList <SourceViewer> fSourceViewer;

	private IWorkbenchPartSite fSite;


	public JavaMergeViewer(Composite parent, int styles, CompareConfiguration mp) {
		super(parent, styles | SWT.LEFT_TO_RIGHT, mp);
	}

	private IPreferenceStore getPreferenceStore() {
		if (fPreferenceStore == null)
			setPreferenceStore(createChainedPreferenceStore(null));
		return fPreferenceStore;
	}

	@Override
	protected void handleDispose(DisposeEvent event) {
		setPreferenceStore(null);
		super.handleDispose(event);
	}

	public IJavaProject getJavaProject(ICompareInput input) {

		if (input == null)
			return null;

		IResourceProvider rp= null;
		ITypedElement te= input.getLeft();
		if (te instanceof IResourceProvider)
			rp= (IResourceProvider) te;
		if (rp == null) {
			te= input.getRight();
			if (te instanceof IResourceProvider)
				rp= (IResourceProvider) te;
		}
		if (rp == null) {
			te= input.getAncestor();
			if (te instanceof IResourceProvider)
				rp= (IResourceProvider) te;
		}
		if (rp != null) {
			IResource resource= rp.getResource();
			if (resource != null) {
				IJavaElement element= JavaCore.create(resource);
				if (element != null)
					return element.getJavaProject();
			}
		}
		return null;
	}

    @Override
	public void setInput(Object input) {
    	if (input instanceof ICompareInput) {
    		IJavaProject project= getJavaProject((ICompareInput)input);
			if (project != null) {
				setPreferenceStore(createChainedPreferenceStore(project));
			}
		}
    	super.setInput(input);
    }

    private ChainedPreferenceStore createChainedPreferenceStore(IJavaProject project) {
    	ArrayList<IPreferenceStore> stores= new ArrayList<>(4);
    	if (project != null)
    		stores.add(new EclipsePreferencesAdapter(new ProjectScope(project.getProject()), JavaCore.PLUGIN_ID));
		stores.add(JavaPlugin.getDefault().getPreferenceStore());
		stores.add(new PreferencesAdapter(JavaPlugin.getJavaCorePluginPreferences()));
		stores.add(EditorsUI.getPreferenceStore());
		return new ChainedPreferenceStore(stores.toArray(new IPreferenceStore[stores.size()]));
    }

	private void handlePropertyChange(PropertyChangeEvent event) {
		if (fSourceViewerConfiguration != null) {
			for (Entry<SourceViewer, JavaSourceViewerConfiguration> entry : fSourceViewerConfiguration.entrySet()) {
				JavaSourceViewerConfiguration configuration= entry.getValue();
				if (configuration.affectsTextPresentation(event)) {
					configuration.handlePropertyChangeEvent(event);
					ITextViewer viewer= entry.getKey();
					viewer.invalidateTextPresentation();
				}
			}
		}
	}

	@Override
	public String getTitle() {
		return CompareMessages.JavaMergeViewer_title;
	}

	@Override
	public ITokenComparator createTokenComparator(String s) {
		boolean ignoreWhitespace = getBoolean(getCompareConfiguration(), CompareConfiguration.IGNORE_WHITESPACE, false);
		return new JavaTokenComparator(s,ignoreWhitespace);
	}

	private static boolean getBoolean(CompareConfiguration cc, String key, boolean dflt) {
		if (cc != null) {
			Object value= cc.getProperty(key);
			if (value instanceof Boolean)
				return ((Boolean) value).booleanValue();
		}
		return dflt;
	}

	@Override
	protected Optional<IIgnoreWhitespaceContributor> createIgnoreWhitespaceContributor(IDocument document) {
		return Optional.of(new JavaIgnoreWhitespaceContributor(document));
	}

	@Override
	protected IDocumentPartitioner getDocumentPartitioner() {
		return JavaCompareUtilities.createJavaPartitioner();
	}

	@Override
	protected String getDocumentPartitioning() {
		return IJavaPartitions.JAVA_PARTITIONING;
	}

	@Override
	protected void configureTextViewer(TextViewer viewer) {
		if (viewer instanceof SourceViewer) {
			SourceViewer sourceViewer= (SourceViewer)viewer;
			if (fSourceViewer == null)
				fSourceViewer= new ArrayList<>();
			if (!fSourceViewer.contains(sourceViewer))
				fSourceViewer.add(sourceViewer);
			JavaTextTools tools= JavaCompareUtilities.getJavaTextTools();
			if (tools != null) {
				IEditorInput editorInput= getEditorInput(sourceViewer);
				sourceViewer.unconfigure();
				if (editorInput == null) {
					sourceViewer.configure(getSourceViewerConfiguration(sourceViewer, null));
					return;
				}
				getSourceViewerConfiguration(sourceViewer, editorInput);
			}
		}
	}

	/*
	 * @see org.eclipse.compare.contentmergeviewer.TextMergeViewer#setEditable(org.eclipse.jface.text.source.ISourceViewer, boolean)
	 * @since 3.5
	 */
	@Override
	protected void setEditable(ISourceViewer sourceViewer, boolean state) {
		super.setEditable(sourceViewer, state);
		if (fEditor != null) {
			CompilationUnitEditorAdapter editor= fEditor.get(sourceViewer);
			if (editor != null)
				editor.setEditable(state);
		}
	}

	/*
	 * @see org.eclipse.compare.contentmergeviewer.TextMergeViewer#isEditorBacked(org.eclipse.jface.text.ITextViewer)
	 * @since 3.5
	 */
	@Override
	protected boolean isEditorBacked(ITextViewer textViewer) {
		return getSite() != null;
	}


	@Override
	protected IEditorInput getEditorInput(ISourceViewer sourceViewer) {
		IEditorInput editorInput= super.getEditorInput(sourceViewer);
		if (editorInput == null)
			return null;
		if (getSite() == null)
			return null;
		if (!(editorInput instanceof IStorageEditorInput))
			return null;
		return editorInput;
	}

	private IWorkbenchPartSite getSite() {
		if (fSite == null) {
			IWorkbenchPart workbenchPart= getCompareConfiguration().getContainer().getWorkbenchPart();
			fSite= workbenchPart != null ? workbenchPart.getSite() : null;
		}
		return fSite;
	}

	private JavaSourceViewerConfiguration getSourceViewerConfiguration(SourceViewer sourceViewer, IEditorInput editorInput) {
		if (fSourceViewerConfiguration == null) {
			fSourceViewerConfiguration= new HashMap<>(3);
		}
		if (fPreferenceStore == null)
			getPreferenceStore();
		JavaTextTools tools= JavaCompareUtilities.getJavaTextTools();
		JavaSourceViewerConfiguration configuration= new JavaSourceViewerConfiguration(tools.getColorManager(), fPreferenceStore, null, getDocumentPartitioning());
		if (editorInput != null) {
			// when input available, use editor
			CompilationUnitEditorAdapter editor= fEditor.get(sourceViewer);
			try {
				editor.init((IEditorSite)editor.getSite(), editorInput);
				editor.createActions();
				configuration= new JavaSourceViewerConfiguration(tools.getColorManager(), fPreferenceStore, editor, getDocumentPartitioning());
			} catch (PartInitException e) {
				JavaPlugin.log(e);
			}
		}
		fSourceViewerConfiguration.put(sourceViewer, configuration);
		return fSourceViewerConfiguration.get(sourceViewer);
	}

	@Override
	protected int findInsertionPosition(char type, ICompareInput input) {

		int pos= super.findInsertionPosition(type, input);
		if (pos != 0)
			return pos;

		if (input instanceof IDiffElement) {

			// find the other (not deleted) element
			JavaNode otherJavaElement= null;
			ITypedElement otherElement= null;
			switch (type) {
			case 'L':
				otherElement= input.getRight();
				break;
			case 'R':
				otherElement= input.getLeft();
				break;
			}
			if (otherElement instanceof JavaNode)
				otherJavaElement= (JavaNode) otherElement;

			// find the parent of the deleted elements
			JavaNode javaContainer= null;
			IDiffElement diffElement= (IDiffElement) input;
			IDiffContainer container= diffElement.getParent();
			if (container instanceof ICompareInput) {

				ICompareInput parent= (ICompareInput) container;
				ITypedElement element= null;

				switch (type) {
				case 'L':
					element= parent.getLeft();
					break;
				case 'R':
					element= parent.getRight();
					break;
				}

				if (element instanceof JavaNode)
					javaContainer= (JavaNode) element;
			}

			if (otherJavaElement != null && javaContainer != null) {

				Object[] children;
				Position p;

				switch (otherJavaElement.getTypeCode()) {

				case JavaNode.PACKAGE:
					return 0;

				case JavaNode.IMPORT_CONTAINER:
					// we have to find the place after the package declaration
					children= javaContainer.getChildren();
					if (children.length > 0) {
						JavaNode packageDecl= null;
						for (JavaNode child : (JavaNode[])children) {
							switch (child.getTypeCode()) {
								case JavaNode.PACKAGE:
									packageDecl= child;
									break;
								case JavaNode.CLASS:
									return child.getRange().getOffset();
							}
						}
						if (packageDecl != null) {
							p= packageDecl.getRange();
							return p.getOffset() + p.getLength();
						}
					}
					return javaContainer.getRange().getOffset();

				case JavaNode.IMPORT:
					// append after last import
					p= javaContainer.getRange();
					return p.getOffset() + p.getLength();

				case JavaNode.CLASS:
					// append after last class
					children= javaContainer.getChildren();
					for (int i= children.length-1; i >= 0; i--) {
						JavaNode child= (JavaNode) children[i];
						switch (child.getTypeCode()) {
						case JavaNode.CLASS:
						case JavaNode.IMPORT_CONTAINER:
						case JavaNode.PACKAGE:
						case JavaNode.FIELD:
							p= child.getRange();
							return p.getOffset() + p.getLength();
						}
					}
					return javaContainer.getAppendPosition().getOffset();

				case JavaNode.METHOD:
					// append in next line after last child
					children= javaContainer.getChildren();
					if (children.length > 0) {
						JavaNode child= (JavaNode) children[children.length-1];
						p= child.getRange();
						return findEndOfLine(javaContainer, p.getOffset() + p.getLength());
					}
					// otherwise use position from parser
					return javaContainer.getAppendPosition().getOffset();

				case JavaNode.FIELD:
					// append after last field
					children= javaContainer.getChildren();
					if (children.length > 0) {
						JavaNode method= null;
						for (int i= children.length-1; i >= 0; i--) {
							JavaNode child= (JavaNode) children[i];
							switch (child.getTypeCode()) {
							case JavaNode.METHOD:
								method= child;
								break;
							case JavaNode.FIELD:
								p= child.getRange();
								return p.getOffset() + p.getLength();
							}
						}
						if (method != null)
							return method.getRange().getOffset();
					}
					return javaContainer.getAppendPosition().getOffset();
				}
			}

			if (javaContainer != null) {
				// return end of container
				Position p= javaContainer.getRange();
				return p.getOffset() + p.getLength();
			}
		}

		// we give up
		return 0;
	}

	private int findEndOfLine(JavaNode container, int pos) {
		int line;
		IDocument doc= container.getDocument();
		try {
			line= doc.getLineOfOffset(pos);
			pos= doc.getLineOffset(line+1);
		} catch (BadLocationException ex) {
			// silently ignored
		}

		// ensure that position is within container range
		Position containerRange= container.getRange();
		int start= containerRange.getOffset();
		int end= containerRange.getOffset() + containerRange.getLength();
		if (pos < start)
			return start;
		if (pos >= end)
			return end-1;

		return pos;
	}

	private void setPreferenceStore(IPreferenceStore ps) {
		if (fPreferenceChangeListener != null) {
			if (fPreferenceStore != null)
				fPreferenceStore.removePropertyChangeListener(fPreferenceChangeListener);
			fPreferenceChangeListener= null;
		}
		fPreferenceStore= ps;
		if (fPreferenceStore != null) {
			fPreferenceChangeListener= this::handlePropertyChange;
			fPreferenceStore.addPropertyChangeListener(fPreferenceChangeListener);
		}
	}

	/*
	 * @see org.eclipse.compare.contentmergeviewer.TextMergeViewer#createSourceViewer(org.eclipse.swt.widgets.Composite, int)
	 * @since 3.5
	 */
	@Override
	protected SourceViewer createSourceViewer(Composite parent, int textOrientation) {
		SourceViewer sourceViewer;
		if (getSite() != null) {
			CompilationUnitEditorAdapter editor= new CompilationUnitEditorAdapter(textOrientation);
			editor.createPartControl(parent);

			ISourceViewer iSourceViewer= editor.getViewer();
			Assert.isTrue(iSourceViewer instanceof SourceViewer);
			sourceViewer= (SourceViewer)iSourceViewer;
			if (fEditor == null)
				fEditor= new HashMap<>(3);
			fEditor.put(sourceViewer, editor);
		} else
			sourceViewer= super.createSourceViewer(parent, textOrientation);

		if (fSourceViewer == null)
			fSourceViewer= new ArrayList<>();
		fSourceViewer.add(sourceViewer);

		return sourceViewer;
	}

	@Override
	protected void setActionsActivated(SourceViewer sourceViewer, boolean state) {
		if (fEditor != null) {
			CompilationUnitEditorAdapter editor= fEditor.get(sourceViewer);
			if (editor != null) {
				editor.setActionsActivated(state);

				IAction saveAction= editor.getAction(ITextEditorActionConstants.SAVE);
				if (saveAction instanceof IPageListener) {
					PartEventAction partEventAction= (PartEventAction) saveAction;
					IWorkbenchPart compareEditorPart= getCompareConfiguration().getContainer().getWorkbenchPart();
					if (state)
						partEventAction.partActivated(compareEditorPart);
					else
						partEventAction.partDeactivated(compareEditorPart);
				}
			}
		}
	}

	@Override
	protected void createControls(Composite composite) {
		super.createControls(composite);
		IWorkbenchPart workbenchPart= getCompareConfiguration().getContainer().getWorkbenchPart();
		if (workbenchPart != null) {
			IContextService service= workbenchPart.getSite().getService(IContextService.class);
			if (service != null) {
				service.activateContext("org.eclipse.jdt.ui.javaEditorScope"); //$NON-NLS-1$
			}
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getAdapter(Class<T> adapter) {
		if (adapter == ITextEditorExtension3.class) {
			IEditorInput activeInput= super.getAdapter(IEditorInput.class);
			if (activeInput != null) {
				for (CompilationUnitEditorAdapter editor : fEditor.values()) {
					if (activeInput.equals(editor.getEditorInput()))
						return (T) editor;
				}
			}
			return null;
		}
		return super.getAdapter(adapter);
	}

	private class CompilationUnitEditorAdapter extends CompilationUnitEditor {
		private boolean fInputSet = false;
		private int fTextOrientation;
		private boolean fEditable;

		CompilationUnitEditorAdapter(int textOrientation) {
			super();
			fTextOrientation = textOrientation;
			// TODO: has to be set here
			setPreferenceStore(createChainedPreferenceStore(null));
		}
		private void setEditable(boolean editable) {
			fEditable= editable;
		}
		@Override
		public IWorkbenchPartSite getSite() {
			return JavaMergeViewer.this.getSite();
		}
		@Override
		public void createActions() {
			if (fInputSet) {
				super.createActions();
				// to avoid handler conflicts disable extra actions
				// we're not handling by CompareHandlerService
				getCorrectionCommands().deregisterCommands();
				getRefactorActionGroup().dispose();
				getGenerateActionGroup().dispose();
			}
			// else do nothing, we will create actions later, when input is available
		}
		@Override
		public void createPartControl(Composite composite) {
			SourceViewer sourceViewer= (SourceViewer)createJavaSourceViewer(composite, new CompositeRuler(), null, false, fTextOrientation | SWT.H_SCROLL | SWT.V_SCROLL, createChainedPreferenceStore(null));
			setSourceViewer(this, sourceViewer);
			createNavigationActions();
			getSelectionProvider().addSelectionChangedListener(getSelectionChangedListener());
		}

		@Override
		protected ISourceViewer createJavaSourceViewer(Composite parent, IVerticalRuler verticalRuler, IOverviewRuler overviewRuler, boolean isOverviewRulerVisible, int styles,
				IPreferenceStore store) {
			return new AdaptedSourceViewer(parent, verticalRuler, overviewRuler, isOverviewRulerVisible, styles, store) {
				@Override
				protected void handleDispose() {
					super.handleDispose();

					// dispose the compilation unit adapter
					dispose();

					fEditor.remove(this);
					if (fEditor.isEmpty()) {
						fEditor= null;
						fSite= null;
					}

					fSourceViewer.remove(this);
					if (fSourceViewer.isEmpty())
						fSourceViewer= null;

				}
			};
		}

		@Override
		protected void doSetInput(IEditorInput input) throws CoreException {
			super.doSetInput(input);
			// the editor input has been explicitly set
			fInputSet = true;
		}
		// called by org.eclipse.ui.texteditor.TextEditorAction.canModifyEditor()
		@Override
		public boolean isEditable() {
			return fEditable;
		}
		@Override
		public boolean isEditorInputModifiable() {
			return fEditable;
		}
		@Override
		public boolean isEditorInputReadOnly() {
			return !fEditable;
		}
		@Override
		protected boolean isWordWrapSupported() {
			return false;
		}
		@Override
		protected void setActionsActivated(boolean state) {
			super.setActionsActivated(state);
		}
		@Override
		public void close(boolean save) {
			getDocumentProvider().disconnect(getEditorInput());
		}
		@Override
		protected void installCodeMiningProviders() {
			// Don't install code minings to avoid drawing minings in the Java compare editors.
		}
	}

	// no setter to private field AbstractTextEditor.fSourceViewer
	private void setSourceViewer(ITextEditor editor, SourceViewer viewer) {
		Field field= null;
		try {
			field= AbstractTextEditor.class.getDeclaredField("fSourceViewer"); //$NON-NLS-1$
		} catch (SecurityException | NoSuchFieldException ex) {
			JavaPlugin.log(ex);
		}
		field.setAccessible(true);
		try {
			field.set(editor, viewer);
		} catch (IllegalArgumentException | IllegalAccessException ex) {
			JavaPlugin.log(ex);
		}
	}
}
