/*******************************************************************************
 * Copyright (c) 2000, 2017 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.ui.internal.editorsupport.win32;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.util.Vector;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.graphics.Image;
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.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IFileEditorInput;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.WorkspaceModifyOperation;
import org.eclipse.ui.dialogs.SaveAsDialog;
import org.eclipse.ui.ide.ResourceUtil;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.part.EditorPart;
import org.eclipse.ui.part.FileEditorInput;

/**
 */
public class OleEditor extends EditorPart {

    /**
     * The resource listener updates the receiver when
     * a change has occurred.
     */
    private IResourceChangeListener resourceListener = new IResourceChangeListener() {

        @Override
		public void resourceChanged(IResourceChangeEvent event) {
            IResourceDelta mainDelta = event.getDelta();
            if (mainDelta == null)
                return;
            IResourceDelta affectedElement = mainDelta.findMember(resource
                    .getFullPath());
            if (affectedElement != null)
                processDelta(affectedElement);
        }

        /*
         * Process the delta for the receiver
         */
        private boolean processDelta(final IResourceDelta delta) {

            Runnable changeRunnable = null;

            switch (delta.getKind()) {
            case IResourceDelta.REMOVED:
                if ((IResourceDelta.MOVED_TO & delta.getFlags()) != 0) {
					changeRunnable = () -> {
						IPath path = delta.getMovedToPath();
						IFile newFile = delta.getResource().getWorkspace().getRoot().getFile(path);
						if (newFile != null) {
							sourceChanged(newFile);
						}
					};
                } else {
					changeRunnable = () -> {
						sourceDeleted = true;
						getSite().getPage().closeEditor(OleEditor.this, true);
					};

                }

                break;
            }

            if (changeRunnable != null)
                update(changeRunnable);

            return true; // because we are sitting on files anyway
        }

    };

    private OleFrame clientFrame;

    private OleClientSite clientSite;

    private File source;

    private IFile resource;

    private Image oleTitleImage;

    //The sourceDeleted flag makes sure that the receiver is not
    //dirty when shutting down
    boolean sourceDeleted = false;

    //The sourceChanged flag indicates whether or not the save from the ole component
    //can be used or if the input changed
    boolean sourceChanged = false;

    private static final String RENAME_ERROR_TITLE = OleMessages
            .getString("OleEditor.errorSaving"); //$NON-NLS-1$

    private static final String OLE_EXCEPTION_TITLE = OleMessages
            .getString("OleEditor.oleExceptionTitle"); //$NON-NLS-1$

    private static final String OLE_EXCEPTION_MESSAGE = OleMessages
            .getString("OleEditor.oleExceptionMessage"); //$NON-NLS-1$

    private static final String OLE_CREATE_EXCEPTION_MESSAGE = OleMessages
            .getString("OleEditor.oleCreationExceptionMessage"); //$NON-NLS-1$

    private static final String OLE_CREATE_EXCEPTION_REASON = OleMessages
            .getString("OleEditor.oleCreationExceptionReason"); //$NON-NLS-1$

    private static final String SAVE_ERROR_TITLE = OleMessages
            .getString("OleEditor.savingTitle"); //$NON-NLS-1$

    private static final String SAVE_ERROR_MESSAGE = OleMessages
            .getString("OleEditor.savingMessage"); //$NON-NLS-1$

    /**
     * Return a new ole editor.
     */
    public OleEditor() {
        //Do nothing
    }

    @Override
	public void createPartControl(Composite parent) {

        // Create a frame.
        clientFrame = new OleFrame(parent, SWT.CLIP_CHILDREN);
        clientFrame.setBackground(JFaceColors.getBannerBackground(clientFrame
                .getDisplay()));

        initializeWorkbenchMenus();

        createClientSite();
        updateDirtyFlag();

		oleActivate();
    }

    /**
     * Create the client site for the receiver
     */

    private void createClientSite() {
        //If there was an OLE Error or nothing has been created yet
        if (clientFrame == null || clientFrame.isDisposed())
            return;
        // Create a OLE client site.
        try {
            clientSite = new OleClientSite(clientFrame, SWT.NONE, source);
        } catch (SWTException exception) {

            IStatus errorStatus = new Status(IStatus.ERROR,
                    WorkbenchPlugin.PI_WORKBENCH, IStatus.ERROR,
                    OLE_CREATE_EXCEPTION_REASON, exception);
            //ErrorDialog.openError(null, OLE_EXCEPTION_TITLE, OLE_CREATE_EXCEPTION_REASON, errorStatus);
            ErrorDialog.openError(null, OLE_EXCEPTION_TITLE, OLE_CREATE_EXCEPTION_MESSAGE, errorStatus);
            return;
        }
        clientSite.setBackground(JFaceColors.getBannerBackground(clientFrame
                .getDisplay()));

    }

    /**
     * Display an error dialog with the supplied title and message.
     * @param title
     * @param message
     */
    private void displayErrorDialog(String title, String message) {
        Shell parent = null;
        if (getClientSite() != null)
            parent = getClientSite().getShell();
        MessageDialog.openError(parent, title, message);
    }

    /**
     * @see IWorkbenchPart#dispose
     */
    @Override
	public void dispose() {
        if (resource != null) {
            ResourcesPlugin.getWorkspace().removeResourceChangeListener(
                    resourceListener);
            resource = null;
        }

        //can dispose the title image because it was created in init
        if (oleTitleImage != null) {
            oleTitleImage.dispose();
            oleTitleImage = null;
        }
    }

    /**
     *	Print this object's contents
     */
    public void doPrint() {
        if (clientSite == null)
            return;
		BusyIndicator.showWhile(clientSite.getDisplay(),
				() -> clientSite.exec(OLE.OLECMDID_PRINT, OLE.OLECMDEXECOPT_PROMPTUSER, null, null));
    }

    /**
     *	Save the viewer's contents to the source file system file
     */
    @Override
	public void doSave(final IProgressMonitor monitor) {
        if (clientSite == null)
            return;
		BusyIndicator.showWhile(clientSite.getDisplay(), () -> {

			// Do not try and use the component provided save if the source has
			// changed in Eclipse
			if (!sourceChanged) {
				int result = clientSite.queryStatus(OLE.OLECMDID_SAVE);
				if ((result & OLE.OLECMDF_ENABLED) != 0) {
					result = clientSite.exec(OLE.OLECMDID_SAVE, OLE.OLECMDEXECOPT_PROMPTUSER, null, null);
					if (result == OLE.S_OK) {
						try {
							resource.refreshLocal(IResource.DEPTH_ZERO, monitor);
						} catch (CoreException ex1) {
							// Do nothing on a failed refresh
						}
						return;
					}
					displayErrorDialog(OLE_EXCEPTION_TITLE, OLE_EXCEPTION_MESSAGE + String.valueOf(result));
					return;
				}
			}
			if (saveFile(source)) {
				try {
					if (resource != null)
						resource.refreshLocal(IResource.DEPTH_ZERO, monitor);
				} catch (CoreException ex2) {
					// Do nothing on a failed refresh
				}
			} else
				displayErrorDialog(SAVE_ERROR_TITLE, SAVE_ERROR_MESSAGE + source.getName());
		});
    }

    /**
     *	Save the viewer's contents into the provided resource.
     */
    @Override
	public void doSaveAs() {
        if (clientSite == null)
            return;
        WorkspaceModifyOperation op = saveNewFileOperation();
        Shell shell = clientSite.getShell();
        try {
            new ProgressMonitorDialog(shell).run(false, true, op);
        } catch (InterruptedException interrupt) {
            //Nothing to reset so do nothing
        } catch (InvocationTargetException invocationException) {
            MessageDialog.openError(shell, RENAME_ERROR_TITLE,
                    invocationException.getTargetException().getMessage());
        }

    }

    /**
     *	Answer self's client site
     *
     *	@return org.eclipse.swt.ole.win32.OleClientSite
     */
    public OleClientSite getClientSite() {
        return clientSite;
    }

    /**
     *	Answer the file system representation of self's input element
     *
     *	@return java.io.File
     */
    public File getSourceFile() {
        return source;
    }

    private void handleWord() {
        OleAutomation dispInterface = new OleAutomation(clientSite);
        // Get Application
        int[] appId = dispInterface
                .getIDsOfNames(new String[] { "Application" }); //$NON-NLS-1$
        if (appId != null) {
            Variant pVarResult = dispInterface.getProperty(appId[0]);
            if (pVarResult != null) {
                OleAutomation application = pVarResult.getAutomation();
                int[] dispid = application
                        .getIDsOfNames(new String[] { "DisplayScrollBars" }); //$NON-NLS-1$
                if (dispid != null) {
                    Variant rgvarg = new Variant(true);
                    application.setProperty(dispid[0], rgvarg);
                }
                application.dispose();
            }
        }
        dispInterface.dispose();
    }

    @Override
	public void init(IEditorSite site, IEditorInput input)
            throws PartInitException {

    	validatePathEditorInput(input);

        // Save input.
        setSite(site);
        setInputWithNotify(input);

        // Update titles.
        setPartName(input.getName());
        setTitleToolTip(input.getToolTipText());
        ImageDescriptor desc = input.getImageDescriptor();
        if (desc != null) {
            oleTitleImage = desc.createImage();
            setTitleImage(oleTitleImage);
        }
    }

    /**
     * Validates the given input
     *
     * @param input the editor input to validate
     * @throws PartInitException if the editor input is not OK
     */
    private boolean validatePathEditorInput(IEditorInput input) throws PartInitException {
        // Check input type.
		IPathEditorInput pathEditorInput = Adapters.adapt(input, IPathEditorInput.class);
        if (pathEditorInput == null)
            throw new PartInitException(OleMessages.format(
                    "OleEditor.invalidInput", new Object[] { input })); //$NON-NLS-1$

        IPath path = pathEditorInput.getPath();

        //Cannot create this with a file and no physical location
        if (!(new File(path.toOSString()).exists()))
            throw new PartInitException(
                    OleMessages
                            .format(
                                    "OleEditor.noFileInput", new Object[] { path.toOSString() })); //$NON-NLS-1$
        return true;
    }

    /**
     *	Initialize the workbench menus for proper merging
     */
    protected void initializeWorkbenchMenus() {
        //If there was an OLE Error or nothing has been created yet
        if (clientFrame == null || clientFrame.isDisposed())
            return;
        // Get the browser menu bar.  If one does not exist then
        // create it.
        Shell shell = clientFrame.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<MenuItem> containerItems = new Vector<>();

        IWorkbenchWindow window = getSite().getWorkbenchWindow();

        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);
        clientFrame.setFileMenus(fileMenu);
        clientFrame.setContainerMenus(containerMenu);
        clientFrame.setWindowMenus(windowMenu);
    }

    @Override
	public boolean isDirty() {
        /*Return only if we have a clientSite which is dirty
         as this can be asked before anything is opened*/
        return clientSite != null && clientSite.isDirty();
    }

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

    /**
     *Since we don't know when a change has been made, always answer true
     * @return <code>false</code> if it was not opened and <code>true</code>
     * only if it is dirty
     */
    public boolean isSaveNeeded() {
        return isDirty();
    }

    /**
     * Save the supplied file using the SWT API.
     * @param file java.io.File
     * @return <code>true</code> if the save was successful
     */
    private boolean saveFile(File file) {

        File tempFile = new File(file.getAbsolutePath() + ".tmp"); //$NON-NLS-1$
        file.renameTo(tempFile);
        boolean saved = false;
        if (OLE.isOleFile(file) || usesStorageFiles(clientSite.getProgramID())) {
            saved = clientSite.save(file, true);
        } else {
            saved = clientSite.save(file, false);
        }

        if (saved) {
            // save was successful so discard the backup
            tempFile.delete();
            return true;
        }
        // save failed so restore the backup
        tempFile.renameTo(file);
        return false;
    }

    /**
     * Save the new File using the client site.
     * @return WorkspaceModifyOperation
     */
    private WorkspaceModifyOperation saveNewFileOperation() {

        return new WorkspaceModifyOperation() {
            @Override
			public void execute(final IProgressMonitor monitor)
                    throws CoreException {
                SaveAsDialog dialog = new SaveAsDialog(clientFrame.getShell());
                IFile sFile = ResourceUtil.getFile(getEditorInput());
                if (sFile != null) {
                    dialog.setOriginalFile(sFile);
                    dialog.open();

                    IPath newPath = dialog.getResult();
                    if (newPath == null)
                        return;

                    if (dialog.getReturnCode() == Window.OK) {
                        String projectName = newPath.segment(0);
                        newPath = newPath.removeFirstSegments(1);
                        IProject project = resource.getWorkspace().getRoot()
                                .getProject(projectName);
                        newPath = project.getLocation().append(newPath);
                        File newFile = newPath.toFile();
                        if (saveFile(newFile)) {
                            IFile newResource = resource.getWorkspace().getRoot()
                                    .getFileForLocation(newPath);
                            if (newResource != null) {
                                sourceChanged(newResource);
                                newResource.refreshLocal(IResource.DEPTH_ZERO,
                                        monitor);
                            }
                        } else {
                            displayErrorDialog(SAVE_ERROR_TITLE, SAVE_ERROR_MESSAGE
                                    + newFile.getName());
                            return;
                        }
                    }
                }
            }
        };

    }

    @Override
	public void setFocus() {
		if (clientSite != null)
			clientSite.setFocus();
    }

    /**
     * Make ole active so that the controls are rendered.
     */
    private void oleActivate() {
        //If there was an OLE Error or nothing has been created yet
        if (clientSite == null || clientFrame == null
                || clientFrame.isDisposed())
            return;

		clientSite.doVerb(OLE.OLEIVERB_SHOW);
		String progId = clientSite.getProgramID();
		if (progId != null && progId.startsWith("Word.Document")) { //$NON-NLS-1$
			handleWord();
		}
    }

    @Override
	protected void setInputWithNotify(IEditorInput input) {
		IPathEditorInput pathEditorInput = Adapters.adapt(input, IPathEditorInput.class);
    	if (pathEditorInput != null)
    		source = new File(pathEditorInput.getPath().toOSString());

        if (input instanceof IFileEditorInput) {
        	if (resource == null)
        		ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceListener);
        	resource = ((IFileEditorInput)input).getFile();
        } else if (resource != null) {
        	ResourcesPlugin.getWorkspace().removeResourceChangeListener(resourceListener);
        	resource = null;
        }

        super.setInputWithNotify(input);
    }

    /**
     * See if it is one of the known types that use OLE Storage.
     * @param progID the type to test
     * @return <code>true</code> if it is one of the known types
     */
    private static boolean usesStorageFiles(String progID) {
        return (progID != null && (progID.startsWith("Word.", 0) //$NON-NLS-1$
                || progID.startsWith("MSGraph", 0) //$NON-NLS-1$
                || progID.startsWith("PowerPoint", 0) //$NON-NLS-1$
        || progID.startsWith("Excel", 0))); //$NON-NLS-1$
    }

    /**
     * The source has changed to the newFile. Update
     * editors and set any required flags
     * @param newFile The file to get the new contents from.
     */
    private void sourceChanged(IFile newFile) {

        FileEditorInput newInput = new FileEditorInput(newFile);
        setInputWithNotify(newInput);
        sourceChanged = true;
        setPartName(newInput.getName());

    }

    @Override
	public boolean isSaveOnCloseNeeded() {
        return !sourceDeleted && super.isSaveOnCloseNeeded();
    }

    /**
     * Posts the update code "behind" the running operation.
     *
     * @param runnable the update code
     */
    private void update(Runnable runnable) {
        IWorkbench workbench = PlatformUI.getWorkbench();
        IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
        if (windows != null && windows.length > 0) {
            Display display = windows[0].getShell().getDisplay();
            display.asyncExec(runnable);
        } else
            runnable.run();
    }

    private boolean isDirty = false;
    private void updateDirtyFlag() {
    	final Runnable dirtyFlagUpdater = new Runnable() {
			@Override
			public void run() {
				if (clientSite == null || resource == null) return;
				boolean dirty = isDirty();
				if (isDirty != dirty) {
					isDirty = dirty;
					firePropertyChange(PROP_DIRTY);
				}
				clientSite.getDisplay().timerExec(1000, this);
			}
    	};
    	dirtyFlagUpdater.run();
    }
}
