/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * 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 com.android.ide.eclipse.adt.internal.editors.layout.gle2;

import com.android.ide.common.api.IDragElement;
import com.android.ide.common.api.IDragElement.IDragAttribute;
import com.android.ide.common.api.INode;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.sdklib.SdkConstants;

import org.eclipse.jface.action.Action;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.widgets.Composite;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The {@link ClipboardSupport} class manages the native clipboard, providing operations
 * to copy, cut and paste view items, and can answer whether the clipboard contains
 * a transferable we care about.
 */
public class ClipboardSupport {
    private static final boolean DEBUG = false;

    /** SWT clipboard instance. */
    private Clipboard mClipboard;
    private LayoutCanvas mCanvas;

    /**
     * Constructs a new {@link ClipboardSupport} tied to the given
     * {@link LayoutCanvas}.
     *
     * @param canvas The {@link LayoutCanvas} to provide clipboard support for.
     * @param parent The parent widget in the SWT hierarchy of the canvas.
     */
    public ClipboardSupport(LayoutCanvas canvas, Composite parent) {
        this.mCanvas = canvas;

        mClipboard = new Clipboard(parent.getDisplay());
    }

    /**
     * Frees up any resources held by the {@link ClipboardSupport}.
     */
    public void dispose() {
        if (mClipboard != null) {
            mClipboard.dispose();
            mClipboard = null;
        }
    }

    /**
     * Perform the "Copy" action, either from the Edit menu or from the context
     * menu.
     * <p/>
     * This sanitizes the selection, so it must be a copy. It then inserts the
     * selection both as text and as {@link SimpleElement}s in the clipboard.
     * (If there is selected text in the error label, then the error is used
     * as the text portion of the transferable.)
     *
     * @param selection A list of selection items to add to the clipboard;
     *            <b>this should be a copy already - this method will not make a
     *            copy</b>
     */
    public void copySelectionToClipboard(List<SelectionItem> selection) {
        SelectionManager.sanitize(selection);

        // The error message area shares the copy action with the canvas. Invoking the
        // copy action when there are errors visible *AND* the user has selected text there,
        // should include the error message as the text transferable.
        String message = null;
        GraphicalEditorPart graphicalEditor = mCanvas.getLayoutEditor().getGraphicalEditor();
        StyledText errorLabel = graphicalEditor.getErrorLabel();
        if (errorLabel.getSelectionCount() > 0) {
            message = errorLabel.getSelectionText();
        }

        if (selection.isEmpty()) {
            if (message != null) {
                mClipboard.setContents(
                        new Object[] { message },
                        new Transfer[] { TextTransfer.getInstance() }
                );
            }
            return;
        }

        Object[] data = new Object[] {
                SelectionItem.getAsElements(selection),
                message != null ? message : SelectionItem.getAsText(mCanvas, selection)
        };

        Transfer[] types = new Transfer[] {
                SimpleXmlTransfer.getInstance(),
                TextTransfer.getInstance()
        };

        mClipboard.setContents(data, types);
    }

    /**
     * Perform the "Cut" action, either from the Edit menu or from the context
     * menu.
     * <p/>
     * This sanitizes the selection, so it must be a copy. It uses the
     * {@link #copySelectionToClipboard(List)} method to copy the selection to
     * the clipboard. Finally it uses {@link #deleteSelection(String, List)} to
     * delete the selection with a "Cut" verb for the title.
     *
     * @param selection A list of selection items to add to the clipboard;
     *            <b>this should be a copy already - this method will not make a
     *            copy</b>
     */
    public void cutSelectionToClipboard(List<SelectionItem> selection) {
        copySelectionToClipboard(selection);
        deleteSelection(
                mCanvas.getCutLabel(),
                selection);
    }

    /**
     * Deletes the given selection.
     *
     * @param verb A translated verb for the action. Will be used for the
     *            undo/redo title. Typically this should be
     *            {@link Action#getText()} for either the cut or the delete
     *            actions in the canvas.
     * @param selection The selection. Must not be null. Can be empty, in which
     *            case nothing happens. The selection list will be sanitized so
     *            the caller should pass in a copy.
     */
    public void deleteSelection(String verb, final List<SelectionItem> selection) {
        SelectionManager.sanitize(selection);

        if (selection.isEmpty()) {
            return;
        }

        // If all selected items have the same *kind* of parent, display that in the undo title.
        String title = null;
        for (SelectionItem cs : selection) {
            CanvasViewInfo vi = cs.getViewInfo();
            if (vi != null && vi.getParent() != null) {
                if (title == null) {
                    title = vi.getParent().getName();
                } else if (!title.equals(vi.getParent().getName())) {
                    // More than one kind of parent selected.
                    title = null;
                    break;
                }
            }
        }

        if (title != null) {
            // Typically the name is an FQCN. Just get the last segment.
            int pos = title.lastIndexOf('.');
            if (pos > 0 && pos < title.length() - 1) {
                title = title.substring(pos + 1);
            }
        }
        boolean multiple = mCanvas.getSelectionManager().hasMultiSelection();
        if (title == null) {
            title = String.format(
                        multiple ? "%1$s elements" : "%1$s element",
                        verb);
        } else {
            title = String.format(
                        multiple ? "%1$s elements from %2$s" : "%1$s element from %2$s",
                        verb, title);
        }

        // Implementation note: we don't clear the internal selection after removing
        // the elements. An update XML model event should happen when the model gets released
        // which will trigger a recompute of the layout, thus reloading the model thus
        // resetting the selection.
        mCanvas.getLayoutEditor().wrapUndoEditXmlModel(title, new Runnable() {
            public void run() {
                // Segment the deleted nodes into clusters of siblings
                Map<NodeProxy, List<INode>> clusters =
                        new HashMap<NodeProxy, List<INode>>();
                for (SelectionItem cs : selection) {
                    NodeProxy node = cs.getNode();
                    INode parent = node.getParent();
                    if (parent != null) {
                        List<INode> children = clusters.get(parent);
                        if (children == null) {
                            children = new ArrayList<INode>();
                            clusters.put((NodeProxy) parent, children);
                        }
                        children.add(node);
                    }
                }

                // Notify parent views about children getting deleted
                RulesEngine rulesEngine = mCanvas.getRulesEngine();
                LayoutEditor editor = mCanvas.getLayoutEditor();
                for (Map.Entry<NodeProxy, List<INode>> entry : clusters.entrySet()) {
                    NodeProxy parent = entry.getKey();
                    List<INode> children = entry.getValue();
                    assert children != null && children.size() > 0;
                    rulesEngine.callOnRemovingChildren(editor, parent, children);
                    parent.applyPendingChanges();
                }

                for (SelectionItem cs : selection) {
                    CanvasViewInfo vi = cs.getViewInfo();
                    // You can't delete the root element
                    if (vi != null && !vi.isRoot()) {
                        UiViewElementNode ui = vi.getUiViewNode();
                        if (ui != null) {
                            ui.deleteXmlNode();
                        }
                    }
                }
            }
        });
    }

    /**
     * Perform the "Paste" action, either from the Edit menu or from the context
     * menu.
     *
     * @param selection A list of selection items to add to the clipboard;
     *            <b>this should be a copy already - this method will not make a
     *            copy</b>
     */
    public void pasteSelection(List<SelectionItem> selection) {

        SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
        final SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt);

        if (pasted == null || pasted.length == 0) {
            return;
        }

        CanvasViewInfo lastRoot = mCanvas.getViewHierarchy().getRoot();
        if (lastRoot == null) {
            // Pasting in an empty document. Only paste the first element.
            pasteInEmptyDocument(pasted[0]);
            return;
        }

        // Otherwise use the current selection, if any, as a guide where to paste
        // using the first selected element only. If there's no selection use
        // the root as the insertion point.
        SelectionManager.sanitize(selection);
        final CanvasViewInfo target;
        if (selection.size() > 0) {
            SelectionItem cs = selection.get(0);
            target = cs.getViewInfo();
        } else {
            target = lastRoot;
        }

        final NodeProxy targetNode = mCanvas.getNodeFactory().create(target);
        mCanvas.getLayoutEditor().wrapUndoEditXmlModel("Paste", new Runnable() {
            public void run() {
                mCanvas.getRulesEngine().callOnPaste(targetNode, target.getViewObject(), pasted);
                targetNode.applyPendingChanges();
            }
        });
    }

    /**
     * Paste a new root into an empty XML layout.
     * <p/>
     * In case of error (unknown FQCN, document not empty), silently do nothing.
     * In case of success, the new element will have some default attributes set (xmlns:android,
     * layout_width and height). The edit is wrapped in a proper undo.
     * <p/>
     * Implementation is similar to {@link #createDocumentRoot(String)} except we also
     * copy all the attributes and inner elements recursively.
     */
    private void pasteInEmptyDocument(final IDragElement pastedElement) {
        String rootFqcn = pastedElement.getFqcn();

        // Need a valid empty document to create the new root
        final LayoutEditor layoutEditor = mCanvas.getLayoutEditor();
        final UiDocumentNode uiDoc = layoutEditor.getUiRootNode();
        if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
            debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn);
            return;
        }

        // Find the view descriptor matching our FQCN
        final ViewElementDescriptor viewDesc = layoutEditor.getFqcnViewDescriptor(rootFqcn);
        if (viewDesc == null) {
            // TODO this could happen if pasting a custom view not known in this project
            debugPrintf("Failed to paste document root, unknown FQCN %1$s", rootFqcn);
            return;
        }

        // Get the last segment of the FQCN for the undo title
        String title = rootFqcn;
        int pos = title.lastIndexOf('.');
        if (pos > 0 && pos < title.length() - 1) {
            title = title.substring(pos + 1);
        }
        title = String.format("Paste root %1$s in document", title);

        layoutEditor.wrapUndoEditXmlModel(title, new Runnable() {
            public void run() {
                UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);

                // A root node requires the Android XMLNS
                uiNew.setAttributeValue(
                        "android", //$NON-NLS-1$
                        XmlnsAttributeDescriptor.XMLNS_URI,
                        SdkConstants.NS_RESOURCES,
                        true /*override*/);

                // Copy all the attributes from the pasted element
                for (IDragAttribute attr : pastedElement.getAttributes()) {
                    uiNew.setAttributeValue(
                            attr.getName(),
                            attr.getUri(),
                            attr.getValue(),
                            true /*override*/);
                }

                // Adjust the attributes, adding the default layout_width/height
                // only if they are not present (the original element should have
                // them though.)
                DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);

                uiNew.createXmlNode();

                // Now process all children
                for (IDragElement childElement : pastedElement.getInnerElements()) {
                    addChild(uiNew, childElement);
                }
            }

            private void addChild(UiElementNode uiParent, IDragElement childElement) {
                String childFqcn = childElement.getFqcn();
                final ViewElementDescriptor childDesc =
                    layoutEditor.getFqcnViewDescriptor(childFqcn);
                if (childDesc == null) {
                    // TODO this could happen if pasting a custom view
                    debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn);
                    return;
                }

                UiElementNode uiChild = uiParent.appendNewUiChild(childDesc);

                // Copy all the attributes from the pasted element
                for (IDragAttribute attr : childElement.getAttributes()) {
                    uiChild.setAttributeValue(
                            attr.getName(),
                            attr.getUri(),
                            attr.getValue(),
                            true /*override*/);
                }

                // Adjust the attributes, adding the default layout_width/height
                // only if they are not present (the original element should have
                // them though.)
                DescriptorsUtils.setDefaultLayoutAttributes(
                        uiChild, false /*updateLayout*/);

                uiChild.createXmlNode();

                // Now process all grand children
                for (IDragElement grandChildElement : childElement.getInnerElements()) {
                    addChild(uiChild, grandChildElement);
                }
            }
        });
    }

    /**
     * Returns true if we have a a simple xml transfer data object on the
     * clipboard.
     *
     * @return True if and only if the clipboard contains one of XML element
     *         objects.
     */
    public boolean hasSxtOnClipboard() {
        // The paste operation is only available if we can paste our custom type.
        // We do not currently support pasting random text (e.g. XML). Maybe later.
        SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
        for (TransferData td : mClipboard.getAvailableTypes()) {
            if (sxt.isSupportedType(td)) {
                return true;
            }
        }

        return false;
    }

    private void debugPrintf(String message, Object... params) {
        if (DEBUG) AdtPlugin.printToConsole("Clipboard", String.format(message, params));
    }

}
