/*******************************************************************************
 * Copyright (c) 2008, 2011 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.compare.internal.win32;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareEditorInput;
import org.eclipse.compare.CompareUI;
import org.eclipse.compare.CompareViewerPane;
import org.eclipse.compare.IEditableContent;
import org.eclipse.compare.IPropertyChangeNotifier;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.contentmergeviewer.IFlushable;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.part.PageBook;

public class WordMergeViewer extends AbstractMergeViewer implements IFlushable, IPropertyChangeNotifier  {
	
	private static final String RESOURCE_BUNDLE_NAME = "org.eclipse.compare.internal.win32.WordMergeViewer"; //$NON-NLS-1$
	
	private FormToolkit formToolkit;
	private PageBook composite;
	private Composite docArea;
	private Composite textArea;
	private Label description;
	private WordComparison wordArea;
	private boolean isDirty;
	private final ListenerList listeners = new ListenerList(ListenerList.IDENTITY);
	private Action saveAction;
	private ResourceBundle resourceBundle;
	private Action inplaceAction;
	private long resultFileTimestamp = -1;

	public WordMergeViewer(Composite parent, CompareConfiguration configuration) {
		super(configuration);
		createContentArea(parent);
		getControl().addDisposeListener(new DisposeListener() {
			@Override
			public void widgetDisposed(DisposeEvent e) {
				handleDispose();
			}
		});
		getControl().setData(CompareUI.COMPARE_VIEWER_TITLE, CompareWin32Messages.WordMergeViewer_1);
		IToolBarManager toolBarManager = CompareViewerPane.getToolBarManager(parent);
		if (toolBarManager != null) {
			toolBarManager.removeAll();
			initializeToolbar(toolBarManager);
		}
		updateEnablements();
	}

	private void createContentArea(Composite parent) {
		formToolkit = new FormToolkit(parent.getDisplay());
		composite = new PageBook(parent, SWT.NONE);
		createDocArea(composite);
		createTextArea(composite);
	}

	private void initializeToolbar(IToolBarManager toolBarManager) {
		toolBarManager.add(new Separator("modes"));	//$NON-NLS-1$
		toolBarManager.add(new Separator("file"));	//$NON-NLS-1$
		CompareConfiguration configuration = getConfiguration();
		// For now, only support saving if one side is editable
		if (configuration.isRightEditable() || configuration.isLeftEditable() 
				&& (configuration.isRightEditable() != configuration.isLeftEditable())) {
			saveAction = new Action() {
				@Override
				public void run() {
					saveDocument();
				}
			};
			initAction(saveAction, getResourceBundle(), "action.save."); //$NON-NLS-1$
			toolBarManager.appendToGroup("file", saveAction); //$NON-NLS-1$
		}
		
		inplaceAction = new Action(CompareWin32Messages.WordMergeViewer_2, Action.AS_CHECK_BOX) {
			@Override
			public void run() {
				toggleInplaceExternalState();
			}
		};
		initAction(inplaceAction, getResourceBundle(), "action.inplace."); //$NON-NLS-1$
		toolBarManager.appendToGroup("modes", inplaceAction); //$NON-NLS-1$
		
		toolBarManager.update(true);
	}
	
	/*
	 * Initialize the given Action from a ResourceBundle.
	 */
	private static void initAction(IAction a, ResourceBundle bundle, String prefix) {
		
		String labelKey= "label"; //$NON-NLS-1$
		String tooltipKey= "tooltip"; //$NON-NLS-1$
		String imageKey= "image"; //$NON-NLS-1$
		String descriptionKey= "description"; //$NON-NLS-1$
		
		if (prefix != null && prefix.length() > 0) {
			labelKey= prefix + labelKey;
			tooltipKey= prefix + tooltipKey;
			imageKey= prefix + imageKey;
			descriptionKey= prefix + descriptionKey;
		}
		
		a.setText(getString(bundle, labelKey, labelKey));
		a.setToolTipText(getString(bundle, tooltipKey, null));
		a.setDescription(getString(bundle, descriptionKey, null));
		
		String relPath= getString(bundle, imageKey, null);
		if (relPath != null && relPath.trim().length() > 0) {
			
			String dPath;
			String ePath;
			
			if (relPath.contains("/")) { //$NON-NLS-1$
				String path= relPath.substring(1);
				dPath= 'd' + path;
				ePath= 'e' + path;
			} else {
				dPath= "dlcl16/" + relPath; //$NON-NLS-1$
				ePath= "elcl16/" + relPath; //$NON-NLS-1$
			}
			
			ImageDescriptor id= getImageDescriptor(dPath);	// we set the disabled image first (see PR 1GDDE87)
			if (id != null)
				a.setDisabledImageDescriptor(id);
			id= getImageDescriptor(ePath);
			if (id != null) {
				a.setImageDescriptor(id);
				a.setHoverImageDescriptor(id);
			}
		}
	}
	
	private static ImageDescriptor getImageDescriptor(String relativePath) {
		IPath path= IPath.fromOSString("$nl$/icons/full/").append(relativePath);
		URL url= FileLocator.find(Activator.getDefault().getBundle(), path, null);
		if (url == null)
			return null;
		return ImageDescriptor.createFromURL(url);
	}
	
	private static String getString(ResourceBundle bundle, String key, String dfltValue) {
		
		if (bundle != null) {
			try {
				return bundle.getString(key);
			} catch (MissingResourceException x) {
				// fall through
			}
		}
		return dfltValue;
	}

	private ResourceBundle getResourceBundle() {
		if (resourceBundle == null) {
			resourceBundle = ResourceBundle.getBundle(RESOURCE_BUNDLE_NAME);
		}
		return resourceBundle;
	}

	private Composite createComposite(Composite parent) {
		return formToolkit.createComposite(parent);
	}

	private void updateEnablements() {
		if (saveAction != null)
			saveAction.setEnabled(isDirty());
		inplaceAction.setChecked(wordArea.isInplace());
		inplaceAction.setEnabled(wordArea.isOpen());
	}


	private void createDocArea(PageBook book) {
		docArea = createComposite(book);
		docArea.setLayout(GridLayoutFactory.fillDefaults().create());
		wordArea = new WordComparison(docArea);
		IWorkbenchPart workbenchPart = getConfiguration().getContainer().getWorkbenchPart();
		if (workbenchPart != null) {
			wordArea.initializeWorkbenchMenus(workbenchPart.getSite().getWorkbenchWindow());
		}
		wordArea.getFrame().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
		wordArea.getFrame().setBackground(formToolkit.getColors().getBackground());
		updateDirtyFlag();
	}
	
	private void createTextArea(PageBook book) {
		textArea = createComposite(book);
		textArea.setLayout(GridLayoutFactory.fillDefaults().extendedMargins(10, 10, 10, 10).create());
		description = formToolkit.createLabel(textArea, getTextDescription(), SWT.WRAP);
		description.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
	}

	protected void saveDocument() {
		try {
			File result = getResultFile();
			wordArea.saveAsDocument(result.getAbsolutePath());
			// Forward the saved content to the save target
			IEditableContent saveTarget = getSaveTarget();
			if (saveTarget == null) {
				if (MessageDialog.openQuestion(WordMergeViewer.this.composite.getShell(), "Save to File?", "The compare editor is not saveable. Would you like to save your changes to another file?")) {
					FileDialog dialog = new FileDialog(WordMergeViewer.this.composite.getShell(), SWT.SAVE);
					String filename = dialog.open();
					if (filename != null) {
						wordArea.saveAsDocument(filename);
					}
				}
			} else {
				synchronized (result) {
					if (result.exists()) {
						saveTarget.setContent(asBytes(result));
						resultFileTimestamp = result.lastModified();
					}
				}
			}
			updateEnablements();
		} catch (IOException e) {
			ErrorDialog.openError(WordMergeViewer.this.composite.getShell(), null, null, new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0, e.getMessage(), e));
		} catch (SWTException e) {
			ErrorDialog.openError(WordMergeViewer.this.composite.getShell(), null, null, new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0, e.getMessage(), e));
		}
	}
	
	@Override
	public void flush(IProgressMonitor monitor) {
		Display.getDefault().syncExec(new Runnable() {
			@Override
			public void run() {
				if (isReallyDirty())
					saveDocument();
			}
		});
	}


	protected void toggleInplaceExternalState() {
		BusyIndicator.showWhile(Display.getDefault(), new Runnable() {
			@Override
			public void run() {
				try {
					if (isReallyDirty()) {
						// If the file is dirty, save the result file before switching so our changes are not lost
						try {
							File result = getResultFile();
							wordArea.saveAsDocument(result.getAbsolutePath());
						} catch (IOException e) {
							throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0, e.getMessage(), e));
						}
					}
					openComparison(!wordArea.isInplace());
				} catch (CoreException e) {
					ErrorDialog.openError(WordMergeViewer.this.composite.getShell(), null, null, e.getStatus());
					Activator.log(e);
				}
			}
		});
	}

	private boolean isReallyDirty() {
		return isDirty() || wordArea.isDirty();
	}
	
	private void openComparison(boolean inplace) throws CoreException {
		try {
			if (isOneSided()) {
				File file = getFileForSingleSide();
				if (file != null) {
					try {
						wordArea.openDocument(file.getAbsolutePath(), inplace);
					} catch (SWTException e) {
						throw new CoreException(new Status(IStatus.ERROR,
								Activator.PLUGIN_ID, NLS.bind(
										CompareWin32Messages.WordComparison_16,
										file.getAbsolutePath()), e));
					}
				}
			} else {
				File left = getFileForLeft();
				File right = getFileForRight();
				if (left != null && right != null) {
					File result = getResultFile();
					int direction = getCompareInput().getKind() & Differencer.DIRECTION_MASK;
					File base, revised;
					if (direction == Differencer.RIGHT) {
						base = left;
						revised = right;
					} else {
						base = right;
						revised = left;
					}
					synchronized (result) {	
						if (!result.exists()) {
							wordArea.createWorkingCopy(base.getAbsolutePath(), revised.getAbsolutePath(), result.getAbsolutePath());
							resultFileTimestamp = result.lastModified();
							description.setText(getTextDescription());
						}
						try {
							wordArea.openDocument(result.getAbsolutePath(), inplace);
						} catch (SWTException e) {
							throw new CoreException(new Status(IStatus.ERROR,
									Activator.PLUGIN_ID, NLS.bind(
											CompareWin32Messages.WordComparison_16,
											result.getAbsolutePath()), e));
						}
					}
				}
			}
			if (wordArea.isInplace()) {
				composite.showPage(docArea);
			} else {
				composite.showPage(textArea);
			}
		} catch (SWTException e) {
			throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0, e.getMessage(), e));
		} catch (IOException e) {
			throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, 0, e.getMessage(), e));
		}
		updateEnablements();
	}

	private String getTextDescription() {
		if (saveAction != null &&hasResultFile()) {
			IEditableContent saveTarget = getSaveTarget();
			String name = CompareWin32Messages.WordMergeViewer_3;
			if (saveTarget instanceof ITypedElement) {
				ITypedElement te = (ITypedElement) saveTarget;
				name = te.getName();
			}
			try {
				return NLS.bind(CompareWin32Messages.WordMergeViewer_4, getResultFile().getName(), name);
			} catch (IOException e) {
				// This shouldn't happen since the result file has already been created
			}
		}
		return CompareWin32Messages.WordMergeViewer_5;
	}

	@Override
	public Control getControl() {
		return composite;
	}
	
	@Override
	public void setInput(Object input) {
		super.setInput(input);
		try {
			openComparison(true);
		} catch (CoreException e) {
			ErrorDialog.openError(WordMergeViewer.this.composite.getShell(), null, null, e.getStatus());
			Activator.log(e);
		}
	}
	
	private void updateDirtyFlag() {
		final Runnable dirtyFlagUpdater = new Runnable() {
			@Override
			public void run() {
				if (wordArea.getFrame().isDisposed())
					return;
				boolean dirty = wordArea.isDirty();
				if (hasResultFile()) {
					try {
						File resultFile = getResultFile();
						synchronized (resultFile) {
							if (resultFile.exists()) {
								long lastModified = resultFile.lastModified();
								if (lastModified != resultFileTimestamp) {
									dirty = true;
								}
							}
						}
					} catch (IOException e) {
						// Shouldn't happen since we only get the result file if it has already been created
					}
				}
				if (isDirty() != dirty) {
					setDirty(dirty);
				}
				composite.getDisplay().timerExec(1000, this);
			}
		};
		dirtyFlagUpdater.run();
	}

	protected boolean isDirty() {
		return isDirty;
	}

	protected void setDirty(boolean dirty) {
		if (isDirty != dirty) {
			isDirty = dirty;
			updateEnablements();
			firePropertyChange(CompareEditorInput.DIRTY_STATE, Boolean.valueOf(!isDirty), Boolean.valueOf(isDirty));
		}
	}

	private void firePropertyChange(String property, Object oldValue, Object newValue) {
		Object[] allListeners = listeners.getListeners();
		final PropertyChangeEvent event = new PropertyChangeEvent(this, property, oldValue, newValue);
		for (Object allListener : allListeners) {
			final IPropertyChangeListener listener = (IPropertyChangeListener) allListener;
			SafeRunner.run(new SafeRunnable() {
				@Override
				public void run() throws Exception {
					listener.propertyChange(event);
				}
			});
		}
	}

	@Override
	public void addPropertyChangeListener(IPropertyChangeListener listener) {
		listeners.add(listener);
	}

	@Override
	public void removePropertyChangeListener(IPropertyChangeListener listener) {
		listeners.remove(listener);
	}

	private void handleDispose() {
		BusyIndicator.showWhile(Display.getDefault(), new Runnable() {
			@Override
			public void run() {
				wordArea.dispose();
				formToolkit.dispose();
				reset();
			}
		});
	}
	
	@Override
	protected void reset() {
		if (wordArea.isOpen()) {
			wordArea.close();
		}
		super.reset();
	}
}
