/**
 *  Copyright (c) 2015-2017 Angelo ZERR.
 *  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:
 *  Angelo Zerr <[email protected]> - initial API and implementation
 */
package ts.eclipse.ide.jsdt.internal.ui.compare;

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

import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.IResourceProvider;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.contentmergeviewer.TextMergeViewer;
import org.eclipse.compare.structuremergeviewer.ICompareInput;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.TextViewer;
import org.eclipse.jface.text.formatter.IContentFormatter;
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.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.widgets.Composite;
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.editors.text.EditorsUI;
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.wst.jsdt.core.JavaScriptCore;
import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
import org.eclipse.wst.jsdt.internal.ui.text.PreferencesAdapter;

import ts.eclipse.ide.core.TypeScriptCorePlugin;
import ts.eclipse.ide.core.resources.IIDETypeScriptProject;
import ts.eclipse.ide.core.utils.TypeScriptResourceUtil;
import ts.eclipse.ide.jsdt.internal.ui.JSDTTypeScriptUIPlugin;
import ts.eclipse.ide.jsdt.internal.ui.editor.EclipsePreferencesAdapter;
import ts.eclipse.ide.jsdt.internal.ui.editor.TypeScriptEditor;
import ts.eclipse.ide.jsdt.internal.ui.editor.TypeScriptSourceViewerConfiguration;
import ts.eclipse.ide.jsdt.internal.ui.editor.format.TypeScriptContentFormatter;
import ts.eclipse.ide.jsdt.internal.ui.text.TypeScriptTextTools;
import ts.eclipse.ide.ui.TypeScriptUIPlugin;

/**
 * TypeScript merge viewer used to compare ts files.
 *
 */
public class TypeScriptMergeViewer extends TextMergeViewer {

	private IPropertyChangeListener fPreferenceChangeListener;
	private IPreferenceStore fPreferenceStore;
	private Map<SourceViewer, TypeScriptSourceViewerConfiguration> fSourceViewerConfiguration;
	private Map<SourceViewer, TypeScriptEditorAdapter> fEditor;
	private ArrayList<SourceViewer> fSourceViewer;
	private IWorkbenchPartSite fSite;
	private IIDETypeScriptProject project;

	public TypeScriptMergeViewer(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 IIDETypeScriptProject getTypeScriptProject(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) {
				try {
					return TypeScriptResourceUtil.getTypeScriptProject(resource.getProject());
				} catch (CoreException e) {
					return null;
				}
			}
		}
		return null;
	}

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

	private ChainedPreferenceStore createChainedPreferenceStore(IIDETypeScriptProject project) {
		ArrayList<IPreferenceStore> stores = new ArrayList<>(4);
		if (project != null) {
			stores.add(new EclipsePreferencesAdapter(new ProjectScope(project.getProject()),
					TypeScriptCorePlugin.PLUGIN_ID));
			stores.add(new EclipsePreferencesAdapter(new ProjectScope(project.getProject()),
					TypeScriptUIPlugin.PLUGIN_ID));
		}
		stores.add(JavaScriptPlugin.getDefault().getPreferenceStore());
		stores.add(new PreferencesAdapter(JavaScriptCore.getPlugin().getPluginPreferences()));
		stores.add(new PreferencesAdapter(JSDTTypeScriptUIPlugin.getDefault().getPluginPreferences()));
		stores.add(EditorsUI.getPreferenceStore());
		return new ChainedPreferenceStore(stores.toArray(new IPreferenceStore[stores.size()]));
	}

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

	@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);
			TypeScriptTextTools tools = JSDTTypeScriptUIPlugin.getDefault().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) {
			Object editor = fEditor.get(sourceViewer);
			if (editor instanceof TypeScriptEditorAdapter)
				((TypeScriptEditorAdapter) 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 TypeScriptSourceViewerConfiguration getSourceViewerConfiguration(SourceViewer sourceViewer,
			IEditorInput editorInput) {
		if (fSourceViewerConfiguration == null) {
			fSourceViewerConfiguration = new HashMap<>(3);
		}
		if (fPreferenceStore == null)
			getPreferenceStore();
		TypeScriptTextTools tools = JSDTTypeScriptUIPlugin.getDefault().getJavaTextTools();
		TypeScriptSourceViewerConfiguration configuration = new TypeScriptSourceViewerConfiguration(
				tools.getColorManager(), fPreferenceStore, null, getDocumentPartitioning()) {
			@Override
			public IContentFormatter getContentFormatter(ISourceViewer sourceViewer) {
				return project != null ? new TypeScriptContentFormatter(project.getProject()) : null;
			}
		};
		if (editorInput != null) {
			// when input available, use editor
			TypeScriptEditorAdapter editor = fEditor.get(sourceViewer);
			try {
				editor.init((IEditorSite) editor.getSite(), editorInput);
				editor.createActions();
				configuration = new TypeScriptSourceViewerConfiguration(tools.getColorManager(), fPreferenceStore,
						editor, getDocumentPartitioning());
			} catch (PartInitException e) {
				JSDTTypeScriptUIPlugin.log(e);
			}
		}
		fSourceViewerConfiguration.put(sourceViewer, configuration);
		return fSourceViewerConfiguration.get(sourceViewer);
	}

	private void setPreferenceStore(IPreferenceStore ps) {
		if (fPreferenceChangeListener != null) {
			if (fPreferenceStore != null)
				fPreferenceStore.removePropertyChangeListener(fPreferenceChangeListener);
			fPreferenceChangeListener = null;
		}
		fPreferenceStore = ps;
		if (fPreferenceStore != null) {
			fPreferenceChangeListener = new IPropertyChangeListener() {
				@Override
				public void propertyChange(PropertyChangeEvent event) {
					handlePropertyChange(event);
				}
			};
			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) {
			TypeScriptEditorAdapter editor = new TypeScriptEditorAdapter(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) {
			Object editor = fEditor.get(sourceViewer);
			if (editor instanceof TypeScriptEditorAdapter) {
				TypeScriptEditorAdapter cuea = (TypeScriptEditorAdapter) editor;
				cuea.setActionsActivated(state);

				IAction saveAction = cuea.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("ts.eclipse.ide.jsdt.ui.typeScriptEditorScope"); //$NON-NLS-1$
			}
		}
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object getAdapter(Class adapter) {
		if (adapter == ITextEditorExtension3.class) {
			IEditorInput activeInput = (IEditorInput) super.getAdapter(IEditorInput.class);
			if (activeInput != null) {
				for (Iterator<TypeScriptEditorAdapter> iterator = fEditor.values().iterator(); iterator.hasNext();) {
					TypeScriptEditorAdapter editor = iterator.next();
					if (activeInput.equals(editor.getEditorInput()))
						return editor;
				}
			}
			return null;
		}
		return super.getAdapter(adapter);
	}

	private class TypeScriptEditorAdapter extends TypeScriptEditor {
		private boolean fInputSet = false;
		private int fTextOrientation;
		private boolean fEditable;

		TypeScriptEditorAdapter(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 TypeScriptMergeViewer.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) createTypeScriptSourceViewer(composite, new CompositeRuler(),
					null, false, fTextOrientation | SWT.H_SCROLL | SWT.V_SCROLL, createChainedPreferenceStore(null));
			setSourceViewer(this, sourceViewer);
			createNavigationActions();
			getSelectionProvider().addSelectionChangedListener(getSelectionChangedListener());
		}

		@Override
		protected ISourceViewer createTypeScriptSourceViewer(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());
		}
	}

	// 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) {
			JSDTTypeScriptUIPlugin.log(ex);
		}
		field.setAccessible(true);
		try {
			field.set(editor, viewer);
		} catch (IllegalArgumentException | IllegalAccessException ex) {
			JSDTTypeScriptUIPlugin.log(ex);
		}
	}
}