/*******************************************************************************
 * Copyright (c) 2010 Neil Bartlett.
 * 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:
 *     Neil Bartlett - initial API and implementation
 *******************************************************************************/
package bndtools.views.resolution;

import java.io.File;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.bndtools.core.ui.icons.Icons;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.util.LocalSelectionTransfer;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.ide.ResourceUtil;
import org.eclipse.ui.part.ViewPart;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.namespace.HostNamespace;
import org.osgi.framework.namespace.IdentityNamespace;
import org.osgi.resource.Capability;
import org.osgi.resource.Resource;

import aQute.bnd.osgi.Clazz;
import aQute.lib.io.IO;
import bndtools.Plugin;
import bndtools.model.repo.RepositoryResourceElement;
import bndtools.model.resolution.CapReqMapContentProvider;
import bndtools.model.resolution.CapabilityLabelProvider;
import bndtools.model.resolution.RequirementWrapper;
import bndtools.model.resolution.RequirementWrapperLabelProvider;
import bndtools.tasks.AnalyseBundleResolutionJob;
import bndtools.tasks.BndBuilderCapReqLoader;
import bndtools.tasks.BndFileCapReqLoader;
import bndtools.tasks.CapReqLoader;
import bndtools.tasks.JarFileCapReqLoader;
import bndtools.tasks.ResourceCapReqLoader;
import bndtools.utils.PartAdapter;
import bndtools.utils.SelectionUtils;

public class ResolutionView extends ViewPart implements ISelectionListener, IResourceChangeListener {

    private static String[] FILTERED_CAPABILITY_NAMESPACES = {
        IdentityNamespace.IDENTITY_NAMESPACE, HostNamespace.HOST_NAMESPACE
    };

    private Display display = null;

    private Tree reqsTree = null;
    private Table capsTable = null;

    private TreeViewer reqsViewer;
    private TableViewer capsViewer;

    private ViewerFilter hideSelfImportsFilter;

    private boolean inputLocked = false;
    private boolean outOfDate = false;
    Set<CapReqLoader> loaders;
    private Job analysisJob;

    private final Set<String> filteredCapabilityNamespaces;

    public ResolutionView() {
        filteredCapabilityNamespaces = new HashSet<String>(Arrays.asList(FILTERED_CAPABILITY_NAMESPACES));
        loaders = Collections.emptySet();
    }

    private final IPartListener partAdapter = new PartAdapter() {
        @Override
        public void partActivated(IWorkbenchPart part) {
            if (part == ResolutionView.this) {
                if (outOfDate) {
                    executeAnalysis();
                }
            } else if (part instanceof IEditorPart) {
                IEditorInput editorInput = ((IEditorPart) part).getEditorInput();
                IFile file = ResourceUtil.getFile(editorInput);
                if (file != null) {
                    CapReqLoader loader = getLoaderForFile(file.getLocation()
                        .toFile());
                    if (loader != null) {
                        setLoaders(Collections.singleton(loader));
                        if (getSite().getPage()
                            .isPartVisible(ResolutionView.this)) {
                            executeAnalysis();
                        } else {
                            outOfDate = true;
                        }
                    }
                }
            }
        }
    };

    private boolean setLoaders(Set<CapReqLoader> newLoaders) {
        Set<CapReqLoader> oldLoaders = loaders;
        boolean swap = !oldLoaders.equals(newLoaders);
        if (swap) {
            loaders = newLoaders;
        }
        for (CapReqLoader l : swap ? oldLoaders : newLoaders) {
            IO.close(l);
        }
        return swap;
    }

    private CapReqLoader getLoaderForFile(File file) {
        CapReqLoader loader;
        if (file.getName()
            .toLowerCase()
            .endsWith(".bnd")) {
            loader = new BndFileCapReqLoader(file);
        } else if (file.getName()
            .toLowerCase()
            .endsWith(".jar")) {
            loader = new JarFileCapReqLoader(file);
        } else {
            loader = null;
        }
        return loader;
    }

    @Override
    public void createPartControl(Composite parent) {
        this.display = parent.getDisplay();

        SashForm splitPanel = new SashForm(parent, SWT.HORIZONTAL);
        splitPanel.setLayout(new FillLayout());

        Composite reqsPanel = new Composite(splitPanel, SWT.NONE);
        reqsPanel.setBackground(parent.getBackground());

        GridLayout reqsLayout = new GridLayout(1, false);
        reqsLayout.marginWidth = 0;
        reqsLayout.marginHeight = 0;
        reqsLayout.verticalSpacing = 2;
        reqsPanel.setLayout(reqsLayout);
        new Label(reqsPanel, SWT.NONE).setText("Requirements:");
        reqsTree = new Tree(reqsPanel, SWT.FULL_SELECTION | SWT.MULTI | SWT.BORDER);
        reqsTree.setHeaderVisible(false);
        reqsTree.setLinesVisible(false);
        reqsTree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

        reqsViewer = new TreeViewer(reqsTree);
        ColumnViewerToolTipSupport.enableFor(reqsViewer);
        reqsViewer.setLabelProvider(new RequirementWrapperLabelProvider(true));
        reqsViewer.setContentProvider(new CapReqMapContentProvider());

        Composite capsPanel = new Composite(splitPanel, SWT.NONE);
        capsPanel.setBackground(parent.getBackground());

        GridLayout capsLayout = new GridLayout(1, false);
        capsLayout.marginWidth = 0;
        capsLayout.marginHeight = 0;
        capsLayout.verticalSpacing = 2;
        capsPanel.setLayout(capsLayout);
        new Label(capsPanel, SWT.NONE).setText("Capabilities:");
        capsTable = new Table(capsPanel, SWT.FULL_SELECTION | SWT.MULTI | SWT.BORDER);
        capsTable.setHeaderVisible(false);
        capsTable.setLinesVisible(false);
        capsTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

        capsViewer = new TableViewer(capsTable);
        ColumnViewerToolTipSupport.enableFor(capsViewer);
        capsViewer.setLabelProvider(new CapabilityLabelProvider(true));
        capsViewer.setContentProvider(new CapReqMapContentProvider());
        capsViewer.setFilters(new ViewerFilter[] {
            new ViewerFilter() {
                @Override
                public boolean select(Viewer viewer, Object parent, Object element) {
                    return !filteredCapabilityNamespaces.contains(((Capability) element).getNamespace());
                }
            }
        });

        hideSelfImportsFilter = new ViewerFilter() {

            @Override
            public boolean select(Viewer viewer, Object parentElement, Object element) {
                if (element instanceof RequirementWrapper) {
                    RequirementWrapper rw = (RequirementWrapper) element;
                    return !rw.resolved;
                }
                return true;
            }
        };
        reqsViewer.setFilters(new ViewerFilter[] {
            hideSelfImportsFilter
        });

        reqsViewer.addDragSupport(DND.DROP_MOVE | DND.DROP_COPY, new Transfer[] {
            LocalSelectionTransfer.getTransfer()
        }, new LocalTransferDragListener(reqsViewer));

        capsViewer.addDragSupport(DND.DROP_MOVE | DND.DROP_COPY, new Transfer[] {
            LocalSelectionTransfer.getTransfer()
        }, new LocalTransferDragListener(capsViewer));

        reqsViewer.addOpenListener(new IOpenListener() {
            @Override
            public void open(OpenEvent event) {
                IStructuredSelection selection = (IStructuredSelection) event.getSelection();
                for (Iterator<?> iter = selection.iterator(); iter.hasNext();) {
                    Object item = iter.next();
                    if (item instanceof Clazz) {
                        Clazz clazz = (Clazz) item;
                        String className = clazz.getFQN();
                        IType type = null;
                        if (!loaders.isEmpty()) {
                            IWorkspaceRoot wsroot = ResourcesPlugin.getWorkspace()
                                .getRoot();
                            for (CapReqLoader loader : loaders) {
                                if (loader instanceof BndBuilderCapReqLoader) {
                                    File loaderFile = ((BndBuilderCapReqLoader) loader).getFile();
                                    IFile[] wsfiles = wsroot.findFilesForLocationURI(loaderFile.toURI());
                                    for (IFile wsfile : wsfiles) {
                                        IJavaProject javaProject = JavaCore.create(wsfile.getProject());
                                        try {
                                            type = javaProject.findType(className);
                                            if (type != null)
                                                break;
                                        } catch (JavaModelException e) {
                                            ErrorDialog.openError(getSite().getShell(), "Error", "", new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, MessageFormat.format("Error opening Java class '{0}'.", className), e));
                                        }
                                    }
                                }

                            }
                        }
                        try {
                            if (type != null)
                                JavaUI.openInEditor(type, true, true);
                        } catch (PartInitException e) {
                            ErrorDialog.openError(getSite().getShell(), "Error", "", new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, MessageFormat.format("Error opening Java editor for class '{0}'.", className), e));
                        } catch (JavaModelException e) {
                            ErrorDialog.openError(getSite().getShell(), "Error", "", new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, MessageFormat.format("Error opening Java class '{0}'.", className), e));
                        }
                    }
                }
            }
        });

        fillActionBars();

        getSite().getPage()
            .addPostSelectionListener(this);
        getSite().getPage()
            .addPartListener(partAdapter);
        ResourcesPlugin.getWorkspace()
            .addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);

        // Current selection & part
        IWorkbenchPart activePart = getSite().getPage()
            .getActivePart();
        ISelection activeSelection = getSite().getWorkbenchWindow()
            .getSelectionService()
            .getSelection();
        selectionChanged(activePart, activeSelection);
    }

    void fillActionBars() {
        IAction toggleShowSelfImports = new Action("showSelfImports", IAction.AS_CHECK_BOX) {
            @Override
            public void runWithEvent(Event event) {
                if (isChecked()) {
                    reqsViewer.removeFilter(hideSelfImportsFilter);
                } else {
                    reqsViewer.addFilter(hideSelfImportsFilter);
                }
            }
        };
        toggleShowSelfImports.setChecked(false);
        toggleShowSelfImports.setImageDescriptor(AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "/icons/package_folder_impexp.gif"));
        toggleShowSelfImports.setToolTipText("Show resolved requirements.\n\nInclude requirements that are resolved within the set of selected bundles.");

        IAction toggleLockInput = new Action("lockInput", IAction.AS_CHECK_BOX) {
            @Override
            public void runWithEvent(Event event) {
                inputLocked = isChecked();
                if (!inputLocked) {
                    executeAnalysis();
                }
            }
        };
        toggleLockInput.setChecked(false);
        toggleLockInput.setImageDescriptor(Icons.desc("lock"));
        toggleLockInput.setToolTipText("Lock to current selection");

        IToolBarManager toolBarManager = getViewSite().getActionBars()
            .getToolBarManager();
        toolBarManager.add(toggleShowSelfImports);
        toolBarManager.add(toggleLockInput);
    }

    @Override
    public void setFocus() {}

    @Override
    public void dispose() {
        getSite().getPage()
            .removeSelectionListener(this);
        ResourcesPlugin.getWorkspace()
            .removeResourceChangeListener(this);
        getSite().getPage()
            .removePartListener(partAdapter);
        setLoaders(Collections.<CapReqLoader> emptySet());
        super.dispose();
    }

    public void setInput(Set<CapReqLoader> sourceLoaders, Map<String, List<Capability>> capabilities, Map<String, List<RequirementWrapper>> requirements) {
        setLoaders(sourceLoaders);
        sourceLoaders = loaders;
        if (reqsTree != null && !reqsTree.isDisposed() && capsTable != null && !capsTable.isDisposed()) {
            reqsViewer.setInput(requirements);
            capsViewer.setInput(capabilities);

            String label;
            if (!sourceLoaders.isEmpty()) {
                StringBuilder builder = new StringBuilder();
                String delim = "";
                boolean shortLabel = sourceLoaders.size() > 1;
                for (CapReqLoader l : sourceLoaders) {
                    builder.append(delim);
                    builder.append(shortLabel ? l.getShortLabel() : l.getLongLabel());
                    delim = ", ";
                }
                label = builder.toString();
            } else {
                label = "<no input>";
            }
            setContentDescription(label);
        }
    }

    @Override
    public void selectionChanged(IWorkbenchPart part, ISelection selection) {
        if (selection == null || !(selection instanceof IStructuredSelection))
            return;

        Set<CapReqLoader> loaders = getLoadersFromSelection((IStructuredSelection) selection);
        if (setLoaders(loaders)) {
            if (getSite().getPage()
                .isPartVisible(this)) {
                executeAnalysis();
            } else {
                outOfDate = true;
            }
        }
    }

    private Set<CapReqLoader> getLoadersFromSelection(IStructuredSelection structSel) {
        Set<CapReqLoader> result = new LinkedHashSet<CapReqLoader>();
        Iterator<?> iter = structSel.iterator();
        while (iter.hasNext()) {

            Object element = iter.next();
            CapReqLoader loader = null;

            File file = SelectionUtils.adaptObject(element, File.class);
            if (file != null) {
                loader = getLoaderForFile(file);
            } else {
                IResource eresource = SelectionUtils.adaptObject(element, IResource.class);
                if (eresource != null) {
                    IPath location = eresource.getLocation();
                    if (location != null) {
                        loader = getLoaderForFile(location.toFile());
                    }
                } else if (element instanceof RepositoryResourceElement) {
                    Resource resource = ((RepositoryResourceElement) element).getResource();
                    loader = new ResourceCapReqLoader(resource);
                }
            }

            if (loader != null)
                result.add(loader);
        }

        return result;
    }

    void executeAnalysis() {
        if (inputLocked)
            return;

        outOfDate = false;
        synchronized (this) {
            Job oldJob = analysisJob;
            if (oldJob != null && oldJob.getState() != Job.NONE)
                oldJob.cancel();

            if (!loaders.isEmpty()) {
                final AnalyseBundleResolutionJob job = new AnalyseBundleResolutionJob("importExportAnalysis", loaders);
                job.setSystem(true);

                job.addJobChangeListener(new JobChangeAdapter() {
                    @Override
                    public void aboutToRun(IJobChangeEvent event) {
                        if (display != null && !display.isDisposed()) {
                            Runnable update = new Runnable() {
                                @Override
                                public void run() {
                                    setContentDescription("Working...");
                                }
                            };
                            if (display.getThread() == Thread.currentThread())
                                update.run();
                            else
                                display.asyncExec(update);
                        }
                    }

                    @Override
                    public void done(IJobChangeEvent event) {
                        IStatus result = job.getResult();
                        if (result != null && result.isOK()) {
                            if (display != null && !display.isDisposed())
                                display.asyncExec(new Runnable() {
                                    @Override
                                    public void run() {
                                        setInput(loaders, job.getCapabilities(), job.getRequirements());
                                    }
                                });
                        }
                    }
                });

                analysisJob = job;
                analysisJob.schedule(500);
            } else {
                analysisJob = null;
            }
        }
    }

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        if (!loaders.isEmpty()) {
            IWorkspaceRoot wsroot = ResourcesPlugin.getWorkspace()
                .getRoot();
            for (CapReqLoader loader : loaders) {
                if (loader instanceof BndBuilderCapReqLoader) {
                    File file = ((BndBuilderCapReqLoader) loader).getFile();
                    IFile[] wsfiles = wsroot.findFilesForLocationURI(file.toURI());
                    for (IFile wsfile : wsfiles) {
                        if (event.getDelta()
                            .findMember(wsfile.getFullPath()) != null) {
                            executeAnalysis();
                            break;
                        }
                    }
                }
            }
        }
    }

    static class LocalTransferDragListener implements DragSourceListener {

        private final Viewer viewer;

        public LocalTransferDragListener(Viewer viewer) {
            this.viewer = viewer;
        }

        @Override
        public void dragStart(DragSourceEvent event) {}

        @Override
        public void dragSetData(DragSourceEvent event) {
            LocalSelectionTransfer transfer = LocalSelectionTransfer.getTransfer();
            if (transfer.isSupportedType(event.dataType))
                transfer.setSelection(viewer.getSelection());
        }

        @Override
        public void dragFinished(DragSourceEvent event) {}
    }

}