/*
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.guvnor.tools.views;

import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.ViewerDropAdapter;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.part.DrillDownAdapter;
import org.eclipse.ui.part.ViewPart;
import org.guvnor.tools.Activator;
import org.guvnor.tools.GuvnorRepository;
import org.guvnor.tools.Messages;
import org.guvnor.tools.GuvnorLocationManager.IRepositorySetListener;
import org.guvnor.tools.utils.GuvnorMetadataProps;
import org.guvnor.tools.utils.GuvnorMetadataUtils;
import org.guvnor.tools.utils.PlatformUtils;
import org.guvnor.tools.utils.webdav.IWebDavClient;
import org.guvnor.tools.utils.webdav.ResourceProperties;
import org.guvnor.tools.utils.webdav.WebDavClientFactory;
import org.guvnor.tools.utils.webdav.WebDavServerCache;
import org.guvnor.tools.views.model.TreeObject;
import org.guvnor.tools.views.model.TreeParent;
import org.guvnor.tools.wizards.NewRepLocationWizard;

/**
 * A view showing Guvnor structure in a tree.
 */
public class RepositoryView extends ViewPart {

    private TreeViewer viewer;
    private DrillDownAdapter drillDownAdapter;
    private Action deleteRepositoryLocAction;
    private Action addRepositoryLocAction;
    private Action doubleClickAction;
    private Action refreshAction;

    class NameSorter extends ViewerSorter {
    }

    public RepositoryView() {
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.part.WorkbenchPart#createPartControl(org.eclipse.swt.widgets.Composite)
     */
    public void createPartControl(Composite parent) {
        viewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
        drillDownAdapter = new DrillDownAdapter(viewer);
        viewer.setContentProvider(new RepositoryContentProvider());
        viewer.setLabelProvider(new RepositoryLabelProvider());
        viewer.setSorter(new NameSorter());
        viewer.setInput(viewer);
        makeActions();
        hookContextMenu();
        hookDoubleClickAction();
        contributeToActionBars();

        Activator.getLocationManager().addRepositorySetListener(new IRepositorySetListener() {
            public void repositorySetChanged(int type, List<GuvnorRepository> repList) {
                // TODO: Just creating an entirely new content provider.
                //       Someday might update this to have incremental changes
                //       to existing content provider.
                viewer.setContentProvider(new RepositoryContentProvider());
            }
        });

        super.getSite().setSelectionProvider(viewer);

        addDragDropSupport();
    }

    public void refresh() {
        viewer.refresh();
    }

    private void addDragDropSupport() {
        // TODO: Support drag and drop of directories
        Transfer[] transfers = new Transfer[] { FileTransfer.getInstance()};
        viewer.addDragSupport(DND.DROP_COPY, transfers, new DragSourceListener() {
            private TreeObject[] target;

            public void dragFinished(DragSourceEvent event) {
                target = null;
                event.doit = true;
            }
            public void dragSetData(DragSourceEvent event) {
                if (target == null) {
                    return;
                }
                try {
                    List<String> files = prepareFileTransfer(target);
                    String[] res = new String[files.size()];
                    files.toArray(res);
                    event.data = res;
                } catch (Exception e) {
                    // Note: we could catch a 401 (not authorized) WebDav error and retry,
                    // like we do when listing directories. But since we have to first list
                    // directories to get to files, and the act of listing directories authenticates
                    // for the server, currently we do not have a situation requiring authentication
                    // for specific files. This might be different in the future if the Guvnor security
                    // model changes, or users can directly connect to specific files.
                    Activator.getDefault().displayError(IStatus.ERROR, e.getMessage(), e, true);
                }
            }

            public void dragStart(DragSourceEvent event) {
                List<TreeObject> transferNodes = new ArrayList<TreeObject>();
                ISelection selection = viewer.getSelection();
                Object[] objs = ((IStructuredSelection)selection).toArray();
                for (int i = 0; i < objs.length; i++) {
                    if (objs[i] instanceof TreeObject
                        && ((TreeObject)objs[i]).getNodeType() == TreeObject.Type.RESOURCE) {
                        transferNodes.add((TreeObject)objs[i]);
                    }
                }
                if (transferNodes.size() > 0) {
                    event.doit = true;
                    target = new TreeObject[transferNodes.size()];
                    transferNodes.toArray(target);
                } else {
                    event.doit = false;
                }
            }
        });

        viewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE, transfers, new ViewerDropAdapter(viewer) {

            private TreeParent targetNode;

            @Override
            public boolean performDrop(Object data) {
                if (targetNode == null) {
                    return false;
                }
                String[] items = (String[])data;

                boolean isvalid = validateInputResource(items);

                if(!isvalid) return false;

                String[] errors = processDrop(targetNode, items);
                if (errors.length != 0) {
                    StringBuilder msg = new StringBuilder();
                    for (int i = 0; i < errors.length; i++) {
                        msg.append(errors[i]);
                        msg.append("\r\n"); //$NON-NLS-1$
                    }
                    Activator.getDefault().
                        displayError(IStatus.ERROR, msg.toString(), new Exception(), true);
                }
                if (items.length != errors.length) {
                    // At least one item did not have an error,
                    // so refresh the views
                    PlatformUtils.updateDecoration();
                    PlatformUtils.refreshRepositoryView();
                }
                return items.length != errors.length;
            }

            @Override
            public boolean validateDrop(Object target,
                                       int operation,
                                       TransferData transferType) {
                // The drop target needs to be a directory
                if (target == null
                    || !(target instanceof TreeParent)) {
                    targetNode = null;
                    return false;
                }
                targetNode = (TreeParent)target;
                return true;
            }

        });
    }

    private boolean validateInputResource(String[] filenames){

        for(String filename : filenames){
            File file = new File(filename);
            String msg = "";
            if(!file.isFile()){
                msg = MessageFormat.format(Messages.getString("error.message.isfolder"), filename);
                MessageDialog.openError(null, "Problem Occurred", msg);
                return false;
            }

            String name = file.getName();
            if(!(name.indexOf(".") > 0)){
                msg = MessageFormat.format(Messages.getString("error.message.invalid.filename"), filename);
                MessageDialog.openError(null, "Problem Occurred", msg);
                return false;
            }
        }

        return true;
    }
    private String[] processDrop(TreeParent target, String[] items) {
        List<String> errors = new ArrayList<String>();

        for (int i = 0; i < items.length; i++) {
            IFile sourceFile = PlatformUtils.getResourceFromFSPath(items[i]);
            if (sourceFile != null) {
                try {
                    GuvnorMetadataProps md = GuvnorMetadataUtils.getGuvnorMetadata(sourceFile);
                    if (md == null) {
                        // The file is not already associated with Guvnor, so just add it
                        boolean res = GuvnorMetadataUtils.
                                        addResourceToGuvnor(target.getGuvnorRepository().getLocation(),
                                                           target.getFullPath(), sourceFile);
                        if (!res) {
                            errors.add(MessageFormat.format(Messages.getString("add.failure.exist"), //$NON-NLS-1$
                                                            new Object[] {sourceFile.getName(), target.getFullPath()}));
                        }
                    } else {
                        // Need to check if the drop location is the same as the Guvnor
                        // associated location. If so, then perform commit.
                        // If not, disallow the drop.
                        String itemPath = target.getFullPath() + sourceFile.getName();
                        if (itemPath.equals(md.getFullpath())) {
                            // If there are pending changes
                            if (!GuvnorMetadataUtils.isGuvnorResourceCurrent(sourceFile)) {
                                GuvnorMetadataUtils.commitFileChanges(sourceFile);
                            }
                        } else {
                            errors.add(MessageFormat.format(Messages.getString("already.guvnor.as"), //$NON-NLS-1$
                                                           new Object[] { items[i],
                                                                          md.getFullpath() }));
                        }
                    }
                } catch (Exception e) {
                    Activator.getDefault().writeLog(IStatus.ERROR, e.getMessage(), e);
                }
            } else {
                Activator.getDefault().
                    writeLog(IStatus.WARNING,
                            "Could not resolve: " + items[i],  //$NON-NLS-1$
                            new Exception());
            }
        }
        String[] res = new String[errors.size()];
        errors.toArray(res);
        return res;
    }

    private List<String> prepareFileTransfer(TreeObject[] nodes) throws Exception {
        List<String> res = new ArrayList<String>();
        for (int i = 0; i < nodes.length; i++) {
            String contents = getResourceContents(nodes[i]);
            IPath path = new Path(Activator.getDefault().getStateLocation().toOSString() +
                                  File.separator + "t" + //$NON-NLS-1$
                                  String.valueOf(System.currentTimeMillis()));
            if (!path.toFile().mkdir()) {
                throw new Exception("Could not create directory " + path.toOSString()); //$NON-NLS-1$
            }
            path.toFile().deleteOnExit();
            File transfer = new File(path + File.separator + nodes[i].getName());
            transfer.deleteOnExit();
            FileOutputStream fos = new FileOutputStream(transfer);
            PrintWriter writer = new PrintWriter(fos);
            writer.write(contents);
            writer.flush();
            writer.close();
            res.add(transfer.getAbsolutePath());

            IPath metaPath = GuvnorMetadataUtils.
                                createGuvnorMetadataLocation(path.toOSString());
            metaPath.toFile().deleteOnExit();
            File metaFile = GuvnorMetadataUtils.
                                getGuvnorMetadataFile(metaPath.toOSString(), nodes[i].getName());
            metaFile.deleteOnExit();
            GuvnorMetadataUtils.writeGuvnorMetadataProps(metaFile, getGuvnorMetadataProps(nodes[i]));
            res.add(metaFile.getAbsolutePath());
            // Put at least a 1ms gap in generated dir names above
            Thread.sleep(1, 0);
        }
        return res;
    }

    private GuvnorMetadataProps getGuvnorMetadataProps(TreeObject node) throws Exception {
        GuvnorRepository rep = node.getGuvnorRepository();
        IWebDavClient webdav = WebDavServerCache.getWebDavClient(rep.getLocation());
        if (webdav == null) {
            webdav = WebDavClientFactory.createClient(new URL(rep.getLocation()));
            WebDavServerCache.cacheWebDavClient(rep.getLocation(), webdav);
        }
        ResourceProperties props  = webdav.queryProperties(node.getFullPath());
        return new GuvnorMetadataProps(node.getName(),
                                      node.getGuvnorRepository().getLocation(),
                                      node.getFullPath(), 
                                      props.getLastModifiedDate(),
                                      props.getRevision());
    }

    private void hookContextMenu() {
        MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$
        menuMgr.setRemoveAllWhenShown(true);
        menuMgr.addMenuListener(new IMenuListener() {
            public void menuAboutToShow(IMenuManager manager) {
                RepositoryView.this.fillContextMenu(manager);
            }
        });
        Menu menu = menuMgr.createContextMenu(viewer.getControl());
        viewer.getControl().setMenu(menu);
        getSite().registerContextMenu(menuMgr, viewer);
    }

    private void contributeToActionBars() {
        IActionBars bars = getViewSite().getActionBars();
        fillLocalPullDown(bars.getMenuManager());
        fillLocalToolBar(bars.getToolBarManager());
    }

    private void fillLocalPullDown(IMenuManager manager) {
        manager.add(deleteRepositoryLocAction);
        manager.add(new Separator());
        manager.add(addRepositoryLocAction);
        manager.add(new Separator());
        manager.add(refreshAction);
    }

    private boolean shouldAddDeleteAction() {
        ISelection selection = viewer.getSelection();
        if (selection == null) {
            return false;
        }
        Object obj = ((IStructuredSelection)selection).getFirstElement();
        if (obj instanceof TreeParent) {
            return ((TreeParent)obj).getNodeType() == TreeObject.Type.REPOSITORY;
        } else {
            return false;
        }
    }

    private void fillContextMenu(IMenuManager manager) {
        if (shouldAddDeleteAction()) {
            manager.add(deleteRepositoryLocAction);
        }
        manager.add(refreshAction);
        drillDownAdapter.addNavigationActions(manager);
        // Other plug-ins can contribute there actions here
        manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
    }

    private void fillLocalToolBar(IToolBarManager manager) {
        manager.add(deleteRepositoryLocAction);
        manager.add(addRepositoryLocAction);
        manager.add(new Separator());
        drillDownAdapter.addNavigationActions(manager);
    }

    private void makeActions() {
        deleteRepositoryLocAction = new Action() {
            public void run() {
                ISelection selection = viewer.getSelection();
                Object obj = ((IStructuredSelection)selection).getFirstElement();
                if (obj instanceof TreeParent) {
                    GuvnorRepository rep = ((TreeParent)obj).getGuvnorRepository();
                    if (MessageDialog.openConfirm(RepositoryView.this.getSite().getShell(),
                                                 Messages.getString("remove.rep.dialog.caption"), //$NON-NLS-1$
                                                 MessageFormat.format(Messages.getString("remove.rep.dialog.msg"), //$NON-NLS-1$
                                                                     new Object[] { rep.getLocation() }))) {
                        Activator.getLocationManager().removeRepository(rep.getLocation());
                    }
                }
            }
        };
        deleteRepositoryLocAction.setText(Messages.getString("action.delete.rep")); //$NON-NLS-1$
        deleteRepositoryLocAction.setToolTipText(Messages.getString("action.delete.rep.desc")); //$NON-NLS-1$
        deleteRepositoryLocAction.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages().
            getImageDescriptor(ISharedImages.IMG_TOOL_DELETE));

        addRepositoryLocAction = new Action() {
            public void run() {
                NewRepLocationWizard wiz = new NewRepLocationWizard();
                wiz.init(Activator.getDefault().getWorkbench(), null);
                WizardDialog dialog =
                    new WizardDialog(RepositoryView.this.getSite().getShell(), wiz);
                dialog.create();
                dialog.open();
            }
        };
        addRepositoryLocAction.setText(Messages.getString("action.add.rep")); //$NON-NLS-1$
        addRepositoryLocAction.setToolTipText(Messages.getString("action.add.rep.desc")); //$NON-NLS-1$
        addRepositoryLocAction.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages().
                getImageDescriptor(ISharedImages.IMG_TOOL_NEW_WIZARD));

        doubleClickAction = new Action() {
            public void run() {
                ISelection selection = viewer.getSelection();
                Object obj = ((IStructuredSelection)selection).getFirstElement();
                if (obj instanceof TreeObject) {
                    doubleClick((TreeObject)obj);
                }
            }
        };

        refreshAction = new Action() {
            public void run() {
                ISelection selection = viewer.getSelection();
                if (selection instanceof IStructuredSelection) {
                    IStructuredSelection strucSel = (IStructuredSelection)selection;
                    if (!strucSel.isEmpty()) {
                        viewer.refresh(strucSel.getFirstElement());
                    }
                }
            }
        };
        refreshAction.setText(Messages.getString("action.refresh.rep")); //$NON-NLS-1$
        refreshAction.setToolTipText(Messages.getString("action.refresh.rep.desc")); //$NON-NLS-1$
        refreshAction.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages().
                getImageDescriptor(ISharedImages.IMG_TOOL_REDO));
    }

    private void hookDoubleClickAction() {
        viewer.addDoubleClickListener(new IDoubleClickListener() {
            public void doubleClick(DoubleClickEvent event) {
                doubleClickAction.run();
            }
        });
    }

    private void doubleClick(TreeObject node) {
        if (node instanceof TreeParent) {
            if (viewer.getExpandedState(node)) {
                viewer.collapseToLevel(node, 1);
            } else {
                viewer.expandToLevel(node, 1);
            }
        } else {

            try {
                String contents = getResourceContents(node);
                PlatformUtils.openEditor(contents, node.getName());
            } catch (Exception e) {
                // Note: we could catch a 401 (not authorized) WebDav error and retry,
                // like we do when listing directories. But since we have to first list
                // directories to get to files, and the act of listing directories authenticates
                // for the server, currently we do not have a situation requiring authentication
                // for specific files. This might be different in the future if the Guvnor security
                // model changes, or users can directly connect to specific files.
                Activator.getDefault().displayError(IStatus.ERROR, e.getMessage(), e, true);
            }
        }
    }

    private String getResourceContents(TreeObject node) throws Exception {
        GuvnorRepository rep = node.getGuvnorRepository();
        IWebDavClient webdav = WebDavServerCache.getWebDavClient(rep.getLocation());
        if (webdav == null) {
            webdav = WebDavClientFactory.createClient(new URL(rep.getLocation()));
            WebDavServerCache.cacheWebDavClient(rep.getLocation(), webdav);
        }
        String res = webdav.getResourceContents(node.getFullPath());
        return res;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.ui.part.WorkbenchPart#setFocus()
     */
    public void setFocus() {
        viewer.getControl().setFocus();
    }
}
