/*******************************************************************************
 * 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.util.Vector;

import org.eclipse.jface.action.IMenuManager;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.ole.win32.OLE;
import org.eclipse.swt.ole.win32.OleAutomation;
import org.eclipse.swt.ole.win32.OleClientSite;
import org.eclipse.swt.ole.win32.OleFrame;
import org.eclipse.swt.ole.win32.Variant;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchWindow;

/**
 * Class that manages a Word document comparison using OLE.
 */
public class WordComparison {

	private final OleFrame frame;
	private OleClientSite site;
	private boolean inplace;
	private OleAutomation document;
	
	public WordComparison(Composite composite) {
		frame = new OleFrame(composite, SWT.NONE);
	}

	// These helper methods facilitate writing the OLE apps
	private static Variant invoke(OleAutomation auto, String command) {
		return auto.invoke(property(auto, command), new Variant[0]);
	}

	private static Variant invoke(OleAutomation auto, OleAutomation reference, String command) {
		return auto.invoke(property(auto, reference, command), new Variant[0]);
	}

	private static Variant invoke(OleAutomation auto, OleAutomation reference, String command, String value) {
		return auto.invoke(property(auto, reference, command), new Variant[] { new Variant(value) });
	}

	private static Variant invoke(OleAutomation auto, String command, int value) {
		return auto.invoke(property(auto, command), new Variant[] { new Variant(value) });
	}

	private static Variant invoke(OleAutomation auto, String command, String value) {
		return auto.invoke(property(auto, command), new Variant[] { new Variant(value) });
	}

	private static Variant getVariantProperty(OleAutomation auto, String name) {
		Variant varResult = auto.getProperty(property(auto, name));
		if (varResult != null && varResult.getType() != OLE.VT_EMPTY) {
			return varResult;
		}
		throw new SWTException(NLS.bind(CompareWin32Messages.WordComparison_0, name));
	}

	private static OleAutomation getAutomationProperty(OleAutomation auto, String name) {
		Variant varResult = getVariantProperty(auto, name);
		try {
			OleAutomation automation = varResult.getAutomation();
			if (automation != null)
				return automation;
		} finally {
			varResult.dispose();
		}
		throw new SWTException(NLS.bind(CompareWin32Messages.WordComparison_1, name));
	}
	
	private static OleAutomation getAutomationResult(OleAutomation auto, String command, int value) {
		Variant varResult = invoke(auto, command, value);
		if (varResult != null) {
			try {
				OleAutomation result = varResult.getAutomation();
				if (result != null)
					return result;
			} finally {
				varResult.dispose();
			}
		}
		throw new SWTException(NLS.bind(CompareWin32Messages.WordComparison_2, command, Integer.toString(value)));
	}
	
	private static OleAutomation getAutomationResult(OleAutomation auto, String command, String value) {
		Variant varResult = invoke(auto, command, value);
		if (varResult != null) {
			try {
				OleAutomation result = varResult.getAutomation();
				if (result != null)
					return result;
			} finally {
				varResult.dispose();
			}
		}
		throw new SWTException(NLS.bind(CompareWin32Messages.WordComparison_3, command, value));
	}


	/**
	 * <p>This methods workarounds the feature in doc documents. Some properties are not accessible
	 * using names when a diff document is created. The workaround is to obtain the id of the
	 * method from an original document and use it in the newly created one.</p>
	 *
	 * <p>An exception is thrown if the id cannot be retrieved</p>
	 *
	 * Reference information for id assignment: <a href="http://msdn.microsoft.com/en-us/library/w7a36sdf%28VS.80%29.aspx">http://msdn.microsoft.com/en-us/library/w7a36sdf%28VS.80%29.aspx</a>
	 *
	 * @param auto - object from which we want to get the property, must not be <code>null</code>
	 * @param reference - an reference object from which the property will be obtained.
	 * @param name - the name of the property, must not be <code>null</code>
	 */
	private static int property(OleAutomation auto, OleAutomation reference, String name) {
		int[] ids = auto.getIDsOfNames(new String[] { name });
		if (ids != null) {
			return ids[0];
		}
		if(reference == null) throw new SWTException(NLS.bind(CompareWin32Messages.WordComparison_4, name)) ;

		// the property was not retrieved at that point, try to get it from the reference object
		ids = reference.getIDsOfNames(new String[] { name });
		if (ids == null) {
			throw new SWTException(NLS.bind(CompareWin32Messages.WordComparison_4, name));
		}
		return ids[0];
	}

	private static int property(OleAutomation auto, String name) {
		int[] ids = auto.getIDsOfNames(new String[] { name });
		if (ids == null) throw new SWTException(NLS.bind(CompareWin32Messages.WordComparison_4, name));
		return ids[0];
	}

	private static boolean setProperty(OleAutomation auto, String name, boolean value) {
		return auto.setProperty(property(auto, name), new Variant(value));
	}

	/**
	 * Open the file at the given path as a document in Word.
	 * 
	 * @param filePath
	 *            the path of the file containing the document
	 * @param inplace
	 *            whether Word is to be opened in-place or in a separate window
	 * @throws SWTException
	 *             if the document could not be opened for some reason
	 */
	public void openDocument(String filePath, boolean inplace) throws SWTException {
		resetSite(inplace ? filePath : null);
		if (inplace) {
			site.doVerb(OLE.OLEIVERB_SHOW);
		} else {
			OleAutomation application = createApplication();
			try {
				// Track the external document we just opened
				document = openDocument(application, filePath);
				setDocumentVisible(document, true);
			} finally {
				application.dispose();
			}
		}
	}

	/**
	 * Compares the base document with the revised document and saves the
	 * comparison in the working copy which can then be opened using
	 * openDocument.
	 * 
	 * @param baseDocument
	 *            the base document
	 * @param revisedDocument
	 *            the revised document
	 * @param workingCopy
	 *            the working copy (will be overwritten)
	 * @throws SWTException
	 *             if an SWT error occurs
	 */
	public void createWorkingCopy(String baseDocument, String revisedDocument, String workingCopy) throws SWTException {
		resetSite(null);
		OleAutomation application = createApplication();
		try {
			OleAutomation document = openDocument(application, revisedDocument);
			try {
				setDocumentVisible(document,false);
				compareDocument(document, baseDocument, revisedDocument);
				OleAutomation activeDocument = getActiveDocument(application);
				try {
					Variant varResult = invoke(activeDocument, document, "SaveAs", workingCopy); //$NON-NLS-1$
					if (varResult == null)
						throw new SWTException(NLS.bind(CompareWin32Messages.WordComparison_6, workingCopy));
					varResult.dispose();
				} finally {
					closeDocument(activeDocument, document);
				}
			} finally {
				closeDocument(document, null);
			}
		} finally {
			try {
				//Quit application without saving any changes
				int [] ids = application.getIDsOfNames(new String [] {"Quit", "SaveChanges"});
				final Variant wdDoNotSaveChanges = new Variant(0);
				Variant varResult = application.invoke(ids[0], new Variant[]{ wdDoNotSaveChanges }, new int[] {ids[1]});
				if (varResult != null) {
					varResult.dispose();
				}
			} catch (SWTException e) {
				// We don't want to throw the exception as we may mask another exception
				Activator.log(e);
			} finally {
				application.dispose();
			}
		}
	}
	
	private void closeDocument(OleAutomation document, OleAutomation reference) {
		// Close the first document: destination.Close()
		try {
			Variant varResult = invoke(document, reference, "Close"); //$NON-NLS-1$
			if (varResult != null) {
				varResult.dispose();
			}
		} catch (SWTException e) {
			// We don't want to throw the exception as we may mask another
			// exception
			Activator.log(e);
		} finally {
			document.dispose();
		}
	}

	private void compareDocument(OleAutomation document, String baseDocument, String revisedDocument) {
		// Compare to the second document: compare = destination.Compare(p1)
		Variant varResult = invoke(document, "Compare", baseDocument); //$NON-NLS-1$
		if (varResult == null)
			throw new SWTException(NLS.bind(CompareWin32Messages.WordComparison_9, baseDocument, revisedDocument));
		varResult.dispose();
	}
	
	private boolean getDocumentDirty(OleAutomation document) {
		// 		word.document.Saved
		if (document != null) {
			Variant variantProperty = getVariantProperty(document, "Saved"); //$NON-NLS-1$
			if (variantProperty != null) {
				try {
					return !variantProperty.getBoolean();
				} finally {
					variantProperty.dispose();
				}
			}
		}
		return false;
	}
	
	private void setDocumentVisible(OleAutomation document, boolean visible) {
		// Hide it: destination.Windows[0].Visible=0|1
		OleAutomation windows = getAutomationProperty(document, "Windows"); //$NON-NLS-1$
		try {
			OleAutomation window = getAutomationResult(windows, "Item", 1); //$NON-NLS-1$
			try {
				setProperty(window, "Visible", visible); //$NON-NLS-1$
			} finally {
				window.dispose();
			}
		} finally {
			windows.dispose();
		}
	}

	private OleAutomation openDocument(OleAutomation application, String doc) {
		// Open the first document: word.Documents.Open(p2)
		OleAutomation documents = getAutomationProperty(application, "Documents"); //$NON-NLS-1$
		try {
			OleAutomation document = getAutomationResult(documents, "Open", doc); //$NON-NLS-1$
			if (document == null) {
				throw new SWTException(NLS.bind(CompareWin32Messages.WordComparison_16, doc));
			}
			return document;
		} finally {
			documents.dispose();
		}
	}
	
	private OleAutomation getActiveDocument(OleAutomation application) {
		return getAutomationProperty(application, "ActiveDocument"); //$NON-NLS-1$
	}

	/*
	 * Create a handle to the application
	 */
	private OleAutomation createApplication() {
		return new OleAutomation(site);
	}

	/*
	 * When opening a new comparison, we want to close any existing site 
	 * and create a new one.
	 */
	private void resetSite(String filePath) {
		if (site != null && !site.isDisposed()) {
			disposeSite();
		}
		inplace = filePath != null;
		if (inplace) {
			site = new OleClientSite(frame, SWT.NONE, "Word.Document", new File(filePath)); //$NON-NLS-1$
		} else {
			site = new OleClientSite(frame, SWT.NONE, "Word.Application"); //$NON-NLS-1$
		}
	}

	private void disposeSite() {
		if (document != null) {
			closeDocument(document, null);
			document = null;
			OleAutomation application = createApplication();
			try {
				OleAutomation documents = getAutomationProperty(application, "Documents"); //$NON-NLS-1$
				try {
					Variant property = getVariantProperty(documents, "Count"); //$NON-NLS-1$
					if (property != null) {
						try {
							if (property.getLong() == 0) {
								// There are no other documents open so close the application
								Variant result = invoke(application, "Quit"); //$NON-NLS-1$
								if (result != null) {
									result.dispose();
								}
							}
						} finally {
							property.dispose();
						}
					}
				} finally {
					documents.dispose();
				}
			} finally {
				application.dispose();
			}
		}
		site.dispose();
		site = null;
	}

	public void saveAsDocument(String doc) {
		if (site == null || site.isDisposed()) return;
		if (inplace) {
			site.deactivateInPlaceClient();
			site.save(new File(doc), true);
			site.doVerb(OLE.OLEIVERB_SHOW);
		} else if (document != null) {
			try {
				Variant variant = invoke(document, "SaveAs", doc); //$NON-NLS-1$
				if (variant != null) {
					variant.dispose();
				}
			} catch (SWTException e) {
				// Ignore since this probably means the document was closed by the user
			}
		}
	}

	/**
	 * Return the OLEFrame for the comparison document.
	 * @return the OLEFrame for the comparison document
	 */
	public OleFrame getFrame() {
		return frame;
	}
	
	/**
	 * Dispose of the comparison.
	 */
	public void dispose() {
		try {
			disposeSite();
		} finally {
			if (!frame.isDisposed()) {
				frame.dispose();
			}
		}
	}

	/**
	 * Return whether the comparison document is dirty. This method handles 
	 * both an in-place document and a document opened in a separate window.
	 * @return weather the comparison document is dirty
	 */
	public boolean isDirty() {
		return (inplace && site != null && !site.isDisposed() && site.isDirty())
			|| (!inplace && getDocumentDirty(document));
	}
	
	/**
	 *	Initialize the workbench menus for proper menu merging
	 *  Copied from org.eclipse.ui.internal.editorsupport.win32OleEditor
	 */
	protected void initializeWorkbenchMenus(IWorkbenchWindow window) {
		//If there was an OLE Error or nothing has been created yet
		if (frame == null || frame.isDisposed())
			return;
		// Get the browser menu bar.  If one does not exist then
		// create it.
		Shell shell = frame.getShell();
		Menu menuBar = shell.getMenuBar();
		if (menuBar == null) {
			menuBar = new Menu(shell, SWT.BAR);
			shell.setMenuBar(menuBar);
		}

		// Swap the file and window menus.
		MenuItem[] windowMenu = new MenuItem[1];
		MenuItem[] fileMenu = new MenuItem[1];
		Vector containerItems = new Vector();

		for (int i = 0; i < menuBar.getItemCount(); i++) {
			MenuItem item = menuBar.getItem(i);
			String id = ""; //$NON-NLS-1$
			if (item.getData() instanceof IMenuManager)
				id = ((IMenuManager) item.getData()).getId();
			if (id.equals(IWorkbenchActionConstants.M_FILE))
				fileMenu[0] = item;
			else if (id.equals(IWorkbenchActionConstants.M_WINDOW))
				windowMenu[0] = item;
			else {
				if (window.isApplicationMenu(id)) {
					containerItems.addElement(item);
				}
			}
		}
		MenuItem[] containerMenu = new MenuItem[containerItems.size()];
		containerItems.copyInto(containerMenu);
		frame.setFileMenus(fileMenu);
		frame.setContainerMenus(containerMenu);
		frame.setWindowMenus(windowMenu);
	}

	/**
	 * Return whether the comparison document is being shown in-place or in
	 * a separate window.
	 * @return whether the comparison document is being shown in-place or in
	 * a separate window
	 */
	public boolean isInplace() {
		return inplace;
	}

	/**
	 * Return whether the comparison document is open.
	 * @return whether the comparison document is open
	 */
	public boolean isOpen() {
		return site != null && !site.isDisposed();
	}

	/**
	 * Close any open documents
	 */
	public void close() {
		if (isOpen()) {
			disposeSite();
		}	
	}
}
