package bndtools.editor.contents;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.bndtools.api.ILogger;
import org.bndtools.api.Logger;
import org.bndtools.utils.collections.CollectionUtils;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
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.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.viewers.ViewerDropAdapter;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.forms.IManagedForm;
import org.eclipse.ui.forms.SectionPart;
import org.eclipse.ui.forms.editor.IFormPage;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Section;
import org.eclipse.ui.ide.ResourceUtil;
import org.eclipse.ui.part.ResourceTransfer;
import org.eclipse.ui.plugin.AbstractUIPlugin;

import aQute.bnd.build.model.BndEditModel;
import aQute.bnd.osgi.Constants;
import bndtools.Plugin;
import bndtools.internal.testcaseselection.ITestCaseFilter;
import bndtools.internal.testcaseselection.JavaSearchScopeTestCaseLister;
import bndtools.internal.testcaseselection.TestCaseSelectionDialog;

public class TestSuitesPart extends SectionPart implements PropertyChangeListener {
    private static final ILogger logger = Logger.getLogger(TestSuitesPart.class);

    private BndEditModel model;
    private List<String> testSuites;

    private TableViewer viewer;

    private final Image imgUp = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "/icons/arrow_up.png")
        .createImage();
    private final Image imgDown = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "/icons/arrow_down.png")
        .createImage();

    public TestSuitesPart(Composite parent, FormToolkit toolkit, int style) {
        super(parent, toolkit, style);

        createSection(getSection(), toolkit);
    }

    private void createSection(Section section, FormToolkit toolkit) {
        section.setText(Messages.TestSuitesPart_section_junit_tests);

        Composite composite = toolkit.createComposite(section);
        section.setClient(composite);

        // Section toolbar buttons
        ToolBar toolbar = new ToolBar(section, SWT.FLAT);
        section.setTextClient(toolbar);

        final ToolItem addItem = new ToolItem(toolbar, SWT.PUSH);
        addItem.setImage(PlatformUI.getWorkbench()
            .getSharedImages()
            .getImage(ISharedImages.IMG_OBJ_ADD));
        addItem.setToolTipText(Messages.TestSuitesPart_add);

        final ToolItem removeItem = new ToolItem(toolbar, SWT.PUSH);
        removeItem.setImage(PlatformUI.getWorkbench()
            .getSharedImages()
            .getImage(ISharedImages.IMG_TOOL_DELETE));
        removeItem.setDisabledImage(PlatformUI.getWorkbench()
            .getSharedImages()
            .getImage(ISharedImages.IMG_TOOL_DELETE_DISABLED));
        removeItem.setToolTipText(Messages.TestSuitesPart_remove);
        removeItem.setEnabled(false);

        Table table = toolkit.createTable(composite, SWT.FULL_SELECTION | SWT.MULTI | SWT.BORDER);

        viewer = new TableViewer(table);
        viewer.setContentProvider(new ArrayContentProvider());
        viewer.setLabelProvider(new TestSuiteLabelProvider());

        toolbar = new ToolBar(composite, SWT.FLAT | SWT.HORIZONTAL | SWT.RIGHT);

        final ToolItem btnMoveUp = new ToolItem(toolbar, SWT.PUSH);
        btnMoveUp.setText("Up");
        btnMoveUp.setImage(imgUp);
        btnMoveUp.setEnabled(false);

        final ToolItem btnMoveDown = new ToolItem(toolbar, SWT.PUSH);
        btnMoveDown.setText("Down");
        btnMoveDown.setImage(imgDown);
        btnMoveDown.setEnabled(false);

        // LISTENERS
        viewer.addSelectionChangedListener(new ISelectionChangedListener() {
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                ISelection selection = event.getSelection();
                boolean enabled = selection != null && !selection.isEmpty();
                removeItem.setEnabled(enabled);
                btnMoveUp.setEnabled(enabled);
                btnMoveDown.setEnabled(enabled);
                getManagedForm().fireSelectionChanged(TestSuitesPart.this, selection);
            }
        });
        viewer.addOpenListener(new IOpenListener() {
            @Override
            public void open(OpenEvent event) {
                String name = (String) ((IStructuredSelection) event.getSelection()).getFirstElement();
                if (name != null)
                    doOpenSource(name);
            }
        });
        viewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE, new Transfer[] {
            ResourceTransfer.getInstance()
        }, new TestSuiteListDropAdapter());
        addItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doAdd();
            }
        });
        removeItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doRemove();
            }
        });
        table.addKeyListener(new KeyAdapter() {
            @Override
            public void keyReleased(KeyEvent e) {
                if (e.character == SWT.DEL) {
                    doRemove();
                } else if (e.character == '+') {
                    doAdd();
                }
            }
        });

        btnMoveUp.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doMoveUp();
            }
        });
        btnMoveDown.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doMoveDown();
            }
        });

        // Layout
        GridLayout layout;

        layout = new GridLayout(1, false);
        layout.marginHeight = 0;
        layout.marginWidth = 0;
        layout.verticalSpacing = 0;
        layout.horizontalSpacing = 0;
        composite.setLayout(layout);

        GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true);
        // gd.widthHint = 75;
        gd.heightHint = 75;
        table.setLayoutData(gd);
        toolbar.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));

    }

    void doOpenSource(String name) {
        IJavaProject javaProj = getJavaProject();
        if (javaProj != null) {
            try {
                IType type = javaProj.findType(name);
                if (type != null)
                    JavaUI.openInEditor(type, true, true);
            } catch (PartInitException e) {
                e.printStackTrace();
            } catch (JavaModelException e) {
                e.printStackTrace();
            }
        }
    }

    void doAdd() {

        // Prepare the exclusion list based on existing test cases
        final Set<String> testSuitesSet = new HashSet<String>(testSuites);

        // Create a filter from the exclusion list
        ITestCaseFilter filter = new ITestCaseFilter() {
            @Override
            public boolean select(String testCaseName) {
                return !testSuitesSet.contains(testCaseName);
            }
        };
        IFormPage page = (IFormPage) getManagedForm().getContainer();
        IWorkbenchWindow window = page.getEditorSite()
            .getWorkbenchWindow();

        // Prepare the package lister from the Java project
        IJavaProject javaProject = getJavaProject();
        if (javaProject == null) {
            MessageDialog.openError(getSection().getShell(), "Error", "Cannot add test cases: unable to find a Java project associated with the editor input.");
            return;
        }

        IJavaSearchScope searchScope = SearchEngine.createJavaSearchScope(new IJavaElement[] {
            javaProject
        });
        JavaSearchScopeTestCaseLister testCaseLister = new JavaSearchScopeTestCaseLister(searchScope, window);

        // Create and open the dialog
        TestCaseSelectionDialog dialog = new TestCaseSelectionDialog(getSection().getShell(), testCaseLister, filter, Messages.TestSuitesPart_title);
        dialog.setSourceOnly(true);
        dialog.setMultipleSelection(true);
        if (dialog.open() == Window.OK) {
            Object[] results = dialog.getResult();
            List<String> added = new LinkedList<String>();

            // Select the results
            for (Object result : results) {
                String newTestSuites = (String) result;
                if (testSuites.add(newTestSuites)) {
                    added.add(newTestSuites);
                }
            }

            // Update the model and view
            if (!added.isEmpty()) {
                viewer.add(added.toArray());
                markDirty();
            }
        }
    }

    void doRemove() {
        IStructuredSelection sel = (IStructuredSelection) viewer.getSelection();
        if (!sel.isEmpty()) {
            testSuites.removeAll(sel.toList());
            viewer.remove(sel.toArray());
            markDirty();
            validate();
        }
    }

    void doMoveUp() {
        int[] selectedIndexes = findSelectedIndexes();
        if (CollectionUtils.moveUp(testSuites, selectedIndexes)) {
            viewer.refresh();
            validate();
            markDirty();
        }
    }

    void doMoveDown() {
        int[] selectedIndexes = findSelectedIndexes();
        if (CollectionUtils.moveDown(testSuites, selectedIndexes)) {
            viewer.refresh();
            validate();
            markDirty();
        }
    }

    int[] findSelectedIndexes() {
        Object[] selection = ((IStructuredSelection) viewer.getSelection()).toArray();
        int[] selectionIndexes = new int[selection.length];

        for (int i = 0; i < selection.length; i++) {
            selectionIndexes[i] = testSuites.indexOf(selection[i]);
        }
        return selectionIndexes;
    }

    @Override
    public void initialize(IManagedForm form) {
        super.initialize(form);

        this.model = (BndEditModel) form.getInput();
        this.model.addPropertyChangeListener(Constants.TESTCASES, this);
    }

    @Override
    public void refresh() {
        List<String> modelList = model.getTestSuites();
        testSuites = new ArrayList<String>(modelList);
        viewer.setInput(testSuites);
        validate();
    }

    private void validate() {}

    @Override
    public void commit(boolean onSave) {
        try {
            model.removePropertyChangeListener(Constants.TESTCASES, this);
            model.setTestSuites(testSuites.isEmpty() ? null : testSuites);
        } finally {
            model.addPropertyChangeListener(Constants.TESTCASES, this);
            super.commit(onSave);
        }
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        String propertyName = evt.getPropertyName();
        if (Constants.TESTCASES.equals(propertyName)) {
            IFormPage page = (IFormPage) getManagedForm().getContainer();
            if (page.isActive()) {
                refresh();
            } else {
                markStale();
            }
        } else if (Constants.PRIVATE_PACKAGE.equals(propertyName) || Constants.EXPORT_PACKAGE.equals(propertyName)) {
            validate();
        }
    }

    IJavaProject getJavaProject() {
        IFormPage page = (IFormPage) getManagedForm().getContainer();
        IEditorInput input = page.getEditorInput();

        IFile file = ResourceUtil.getFile(input);
        if (file != null) {
            return JavaCore.create(file.getProject());
        }
        return null;
    }

    private class TestSuiteListDropAdapter extends ViewerDropAdapter {

        protected TestSuiteListDropAdapter() {
            super(viewer);
        }

        @Override
        public void dragEnter(DropTargetEvent event) {
            event.detail = DND.DROP_COPY;
            super.dragEnter(event);
        }

        @Override
        public boolean validateDrop(Object target, int operation, TransferData transferType) {
            return ResourceTransfer.getInstance()
                .isSupportedType(transferType);
        }

        @Override
        public boolean performDrop(Object data) {
            Object target = getCurrentTarget();
            int loc = getCurrentLocation();

            int insertionIndex = -1;
            if (target != null) {
                insertionIndex = testSuites.indexOf(target);
                if (insertionIndex > -1 && loc == LOCATION_ON || loc == LOCATION_AFTER)
                    insertionIndex++;
            }

            List<String> addedNames = new ArrayList<String>();
            if (data instanceof IResource[]) {
                IResource[] resources = (IResource[]) data;
                for (IResource resource : resources) {
                    IJavaElement javaElement = JavaCore.create(resource);
                    if (javaElement != null) {
                        try {
                            if (javaElement instanceof IType) {
                                IType type = (IType) javaElement;
                                if (type.isClass() && Flags.isPublic(type.getFlags())) {
                                    String typeName = type.getPackageFragment()
                                        .getElementName() + "." + type.getElementName(); //$NON-NLS-1$
                                    addedNames.add(typeName);
                                }
                            } else if (javaElement instanceof ICompilationUnit) {
                                IType[] allTypes = ((ICompilationUnit) javaElement).getAllTypes();
                                for (IType type : allTypes) {
                                    if (type.isClass() && Flags.isPublic(type.getFlags())) {
                                        String typeName = type.getPackageFragment()
                                            .getElementName() + "." + type.getElementName(); //$NON-NLS-1$
                                        addedNames.add(typeName);
                                    }
                                }
                            }
                        } catch (JavaModelException e) {
                            logger.logError(Messages.TestSuitesPart_errorJavaType, e);
                        }
                    }
                }
            }

            if (!addedNames.isEmpty()) {
                if (insertionIndex == -1 || insertionIndex == testSuites.size()) {
                    testSuites.addAll(addedNames);
                    viewer.add(addedNames.toArray());
                } else {
                    testSuites.addAll(insertionIndex, addedNames);
                    viewer.refresh();
                }
                viewer.setSelection(new StructuredSelection(addedNames), true);
                validate();
                markDirty();
            }
            return true;
        }
    }
}

class TestSuiteLabelProvider extends StyledCellLabelProvider {
    private final Image suiteImg = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "/icons/tsuite.gif")
        .createImage();

    // private Image testImg =
    // AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID,
    // "/icons/test.gif").createImage();

    @Override
    public void update(ViewerCell cell) {
        String fqName = (String) cell.getElement();
        cell.setText(fqName);
        cell.setImage(suiteImg);
    }

    @Override
    public void dispose() {
        super.dispose();
        suiteImg.dispose();
    }

}
