/*
 * Copyright (C) 2008 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.ui.tree;

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;

import org.apache.xml.serialize.Method;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.xml.core.internal.document.NodeContainer;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;


/**
 * Provides Cut and Copy actions for the tree nodes.
 */
@SuppressWarnings({"restriction", "deprecation"})
public class CopyCutAction extends Action {
    private List<UiElementNode> mUiNodes;
    private boolean mPerformCut;
    private final AndroidXmlEditor mEditor;
    private final Clipboard mClipboard;
    private final ICommitXml mXmlCommit;

    /**
     * Creates a new Copy or Cut action.
     *
     * @param selected The UI node to cut or copy. It *must* have a non-null XML node.
     * @param performCut True if the operation is cut, false if it is copy.
     */
    public CopyCutAction(AndroidXmlEditor editor, Clipboard clipboard, ICommitXml xmlCommit,
            UiElementNode selected, boolean performCut) {
        this(editor, clipboard, xmlCommit, toList(selected), performCut);
    }

    /**
     * Creates a new Copy or Cut action.
     *
     * @param selected The UI nodes to cut or copy. They *must* have a non-null XML node.
     *                 The list becomes owned by the {@link CopyCutAction}.
     * @param performCut True if the operation is cut, false if it is copy.
     */
    public CopyCutAction(AndroidXmlEditor editor, Clipboard clipboard, ICommitXml xmlCommit,
            List<UiElementNode> selected, boolean performCut) {
        super(performCut ? "Cut" : "Copy");
        mEditor = editor;
        mClipboard = clipboard;
        mXmlCommit = xmlCommit;

        ISharedImages images = PlatformUI.getWorkbench().getSharedImages();
        if (performCut) {
            setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT));
            setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT_HOVER));
            setDisabledImageDescriptor(
                    images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT_DISABLED));
        } else {
            setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY));
            setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY_HOVER));
            setDisabledImageDescriptor(
                    images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY_DISABLED));
        }

        mUiNodes = selected;
        mPerformCut = performCut;
    }

    /**
     * Performs the cut or copy action.
     * First an XML serializer is used to turn the existing XML node into a valid
     * XML fragment, which is added as text to the clipboard.
     */
    @Override
    public void run() {
        super.run();
        if (mUiNodes == null || mUiNodes.size() < 1) {
            return;
        }

        // Commit the current pages first, to make sure the XML is in sync.
        // Committing may change the XML structure.
        if (mXmlCommit != null) {
            mXmlCommit.commitPendingXmlChanges();
        }

        StringBuilder allText = new StringBuilder();
        ArrayList<UiElementNode> nodesToCut = mPerformCut ? new ArrayList<UiElementNode>() : null;

        for (UiElementNode uiNode : mUiNodes) {
            try {
                Node xml_node = uiNode.getXmlNode();
                if (xml_node == null) {
                    return;
                }

                String data = getXmlTextFromEditor(xml_node);

                // In the unlikely event that IStructuredDocument failed to extract the text
                // directly from the editor, try to fall back on a direct XML serialization
                // of the XML node. This uses the generic Node interface with no SSE tricks.
                if (data == null) {
                    data = getXmlTextFromSerialization(xml_node);
                }

                if (data != null) {
                    allText.append(data);
                    if (mPerformCut) {
                        // only remove notes to cut if we actually got some XML text from them
                        nodesToCut.add(uiNode);
                    }
                }

            } catch (Exception e) {
                AdtPlugin.log(e, "CopyCutAction failed for UI node %1$s", //$NON-NLS-1$
                        uiNode.getBreadcrumbTrailDescription(true));
            }
        } // for uiNode

        if (allText != null && allText.length() > 0) {
            mClipboard.setContents(
                    new Object[] { allText.toString() },
                    new Transfer[] { TextTransfer.getInstance() });
            if (mPerformCut) {
                for (UiElementNode uiNode : nodesToCut) {
                    uiNode.deleteXmlNode();
                }
            }
        }
    }

    /** Get the data directly from the editor. */
    private String getXmlTextFromEditor(Node xml_node) {
        String data = null;
        IStructuredModel model = mEditor.getModelForRead();
        try {
            IStructuredDocument sse_doc = mEditor.getStructuredDocument();
            if (xml_node instanceof NodeContainer) {
                // The easy way to get the source of an SSE XML node.
                data = ((NodeContainer) xml_node).getSource();
            } else  if (xml_node instanceof IndexedRegion && sse_doc != null) {
                // Try harder.
                IndexedRegion region = (IndexedRegion) xml_node;
                int start = region.getStartOffset();
                int end = region.getEndOffset();

                if (end > start) {
                    data = sse_doc.get(start, end - start);
                }
            }
        } catch (BadLocationException e) {
            // the region offset was invalid. ignore.
        } finally {
            model.releaseFromRead();
        }
        return data;
    }

    /**
     * Direct XML serialization of the XML node.
     * <p/>
     * This uses the generic Node interface with no SSE tricks. It's however slower
     * and doesn't respect formatting (since serialization is involved instead of reading
     * the actual text buffer.)
     */
    private String getXmlTextFromSerialization(Node xml_node) throws IOException {
        String data;
        StringWriter sw = new StringWriter();
        XMLSerializer serializer = new XMLSerializer(sw,
                new OutputFormat(Method.XML,
                        OutputFormat.Defaults.Encoding /* utf-8 */,
                        true /* indent */));
        // Serialize will throw an IOException if it fails.
        serializer.serialize((Element) xml_node);
        data = sw.toString();
        return data;
    }

    /**
     * Static helper class to wrap on node into a list for the constructors.
     */
    private static ArrayList<UiElementNode> toList(UiElementNode selected) {
        ArrayList<UiElementNode> list = null;
        if (selected != null) {
            list = new ArrayList<UiElementNode>(1);
            list.add(selected);
        }
        return list;
    }
}

