/*
 * Copyright (C) 2011 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.refactoring;

import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML;

import com.android.ide.common.rendering.api.ViewInfo;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.CanvasViewInfo;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
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.provisional.document.IDOMModel;
import org.w3c.dom.Element;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@SuppressWarnings("restriction")
public class RefactoringTest extends AdtProjectTest {

    protected boolean autoFormat() {
        return true;
    }

    @Override
    protected void setUp() throws Exception {

        // Ensure that the defaults are initialized so for example formatting options are
        // initialized properly
        IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
        AdtPrefs.init(store);
        AdtPrefs prefs = AdtPrefs.getPrefs();
        prefs.initializeStoreWithDefaults(store);

        store.setValue(AdtPrefs.PREFS_FORMAT_GUI_XML, autoFormat());

        prefs.loadValues(null);

        super.setUp();
    }

    protected static Element findElementById(Element root, String id) {
        if (id.equals(VisualRefactoring.getId(root))) {
            return root;
        }

        for (Element child : DomUtilities.getChildren(root)) {
            Element result = findElementById(child, id);
            if (result != null) {
                return result;
            }
        }

        return null;
    }

    protected static List<Element> getElements(Element root, String... ids) {
        List<Element> selectedElements = new ArrayList<Element>();
        for (String id : ids) {
            Element element = findElementById(root, id);
            assertNotNull(element);
            selectedElements.add(element);
        }
        return selectedElements;
    }

    protected void checkEdits(String basename, List<Change> changes) throws BadLocationException,
            IOException {
        IDocument document = new Document();

        String xml = readTestFile(basename, false);
        if (xml == null) { // New file
            xml = ""; //$NON-NLS-1$
        }
        document.set(xml);

        for (Change change : changes) {
            if (change instanceof TextFileChange) {
                TextFileChange tf = (TextFileChange) change;
                TextEdit edit = tf.getEdit();
                if (edit instanceof MultiTextEdit) {
                    MultiTextEdit edits = (MultiTextEdit) edit;
                    edits.apply(document);
                } else {
                    edit.apply(document);
                }
            } else {
                System.out.println("Ignoring non-textfilechange in refactoring result");
            }
        }

        String actual = document.get();

        // Ensure that the document is still valid to make sure the edits don't
        // mangle it:
        org.w3c.dom.Document doc = DomUtilities.parseDocument(actual, true);
        assertNotNull(actual, doc);

        assertEqualsGolden(basename, actual);
    }

    protected void checkEdits(List<Change> changes,
            Map<IPath, String> fileToGoldenName) throws BadLocationException {
        checkEdits(changes, fileToGoldenName, false);
    }

    protected void checkEdits(List<Change> changes,
            Map<IPath, String> fileToGoldenName, boolean createDiffs) throws BadLocationException {
        for (Change change : changes) {
            if (change instanceof TextFileChange) {
                TextFileChange tf = (TextFileChange) change;
                IFile file = tf.getFile();
                assertNotNull(file);
                IPath path = file.getProjectRelativePath();
                String goldenName = fileToGoldenName.get(path);
                assertNotNull("Not found: " + path.toString(), goldenName);

                String xml = readTestFile(goldenName, false);
                if (xml == null) { // New file
                    xml = ""; //$NON-NLS-1$
                }
                IDocument document = new Document();
                document.set(xml);

                String before = document.get();

                TextEdit edit = tf.getEdit();
                if (edit instanceof MultiTextEdit) {
                    MultiTextEdit edits = (MultiTextEdit) edit;
                    edits.apply(document);
                } else {
                    edit.apply(document);
                }

                String actual = document.get();

                if (createDiffs) {
                    // Use a diff as the golden file instead of the after
                    actual = getDiff(before, actual);
                    if (goldenName.endsWith(DOT_XML)) {
                        goldenName = goldenName.substring(0,
                                goldenName.length() - DOT_XML.length())
                                + ".diff";
                    }
                }

                assertEqualsGolden(goldenName, actual);
            } else {
                System.out.println("Ignoring non-textfilechange in refactoring result");
                assertNull(change.getAffectedObjects());
            }
        }
    }

    protected UiViewElementNode createModel(UiViewElementNode parent, Element element) {
        List<Element> children = DomUtilities.getChildren(element);
        String fqcn = ANDROID_WIDGET_PREFIX + element.getTagName();
        boolean hasChildren = children.size() > 0;
        UiViewElementNode node = createNode(parent, fqcn, hasChildren);
        node.setXmlNode(element);
        for (Element child : children) {
            createModel(node, child);
        }

        return node;
    }

    /**
     * Builds up a ViewInfo hierarchy for the given model. This is done by
     * reading .info dump files which record the exact pixel sizes of each
     * ViewInfo object. These files are assumed to match up exactly with the
     * model objects. This is done rather than rendering an actual layout
     * hierarchy to insulate the test from pixel difference (in say font size)
     * among platforms, as well as tying the test to particulars about relative
     * sizes of things which may change with theme adjustments etc.
     * <p>
     * Each file can be generated by the dump method in the ViewHierarchy.
     */
    protected ViewInfo createInfos(UiElementNode model, String relativePath) {
        String basename = relativePath.substring(0, relativePath.lastIndexOf('.') + 1);
        String relative = basename + "info"; //$NON-NLS-1$
        String info = readTestFile(relative, true);
        // Parse the info file and build up a model from it
        // Each line contains a new info.
        // If indented it is a child of the parent.
        String[] lines = info.split("\n"); //$NON-NLS-1$

        // Iteration order for the info file should match exactly the UI model so
        // we can just advance the line index sequentially as we traverse

        return create(model, Arrays.asList(lines).iterator());
    }

    protected ViewInfo create(UiElementNode node, Iterator<String> lineIterator) {
        // android.widget.LinearLayout [0,36,240,320]
        Pattern pattern = Pattern.compile("(\\s*)(\\S+) \\[(\\d+),(\\d+),(\\d+),(\\d+)\\].*");
        assertTrue(lineIterator.hasNext());
        String description = lineIterator.next();
        Matcher matcher = pattern.matcher(description);
        assertTrue(matcher.matches());
        //String indent = matcher.group(1);
        //String fqcn = matcher.group(2);
        String left = matcher.group(3);
        String top = matcher.group(4);
        String right = matcher.group(5);
        String bottom = matcher.group(6);

        ViewInfo view = new ViewInfo(node.getXmlNode().getLocalName(), node,
                Integer.parseInt(left), Integer.parseInt(top),
                Integer.parseInt(right), Integer.parseInt(bottom));

        List<UiElementNode> childNodes = node.getUiChildren();
        if (childNodes.size() > 0) {
            List<ViewInfo> children = new ArrayList<ViewInfo>();
            for (UiElementNode child : childNodes) {
                children.add(create(child, lineIterator));
            }
            view.setChildren(children);
        }

        return view;
    }

    protected TestContext setupTestContext(IFile file, String relativePath) throws Exception {
        IStructuredModel structuredModel = null;
        org.w3c.dom.Document domDocument = null;
        IStructuredDocument structuredDocument = null;
        Element element = null;

        try {
            IModelManager modelManager = StructuredModelManager.getModelManager();
            structuredModel = modelManager.getModelForRead(file);
            if (structuredModel instanceof IDOMModel) {
                IDOMModel domModel = (IDOMModel) structuredModel;
                domDocument = domModel.getDocument();
                element = domDocument.getDocumentElement();
                structuredDocument = structuredModel.getStructuredDocument();
            }
        } finally {
            if (structuredModel != null) {
                structuredModel.releaseFromRead();
            }
        }

        assertNotNull(structuredModel);
        assertNotNull(domDocument);
        assertNotNull(element);
        assertNotNull(structuredDocument);
        assertTrue(element instanceof IndexedRegion);

        UiViewElementNode model = createModel(null, element);
        ViewInfo info = createInfos(model, relativePath);
        CanvasViewInfo rootView = CanvasViewInfo.create(info, true /* layoutlib5 */).getFirst();
        TestLayoutEditor layoutEditor = new TestLayoutEditor(file, structuredDocument, null);

        TestContext testInfo = createTestContext();
        testInfo.mFile = file;
        testInfo.mStructuredModel = structuredModel;
        testInfo.mStructuredDocument = structuredDocument;
        testInfo.mElement = element;
        testInfo.mDomDocument = domDocument;
        testInfo.mUiModel = model;
        testInfo.mViewInfo = info;
        testInfo.mRootView = rootView;
        testInfo.mLayoutEditor = layoutEditor;

        return testInfo;
    }

    protected TestContext createTestContext() {
        return new TestContext();
    }

    protected static class TestContext {
        protected IFile mFile;
        protected IStructuredModel mStructuredModel;
        protected IStructuredDocument mStructuredDocument;
        protected org.w3c.dom.Document mDomDocument;
        protected Element mElement;
        protected UiViewElementNode mUiModel;
        protected ViewInfo mViewInfo;
        protected CanvasViewInfo mRootView;
        protected TestLayoutEditor mLayoutEditor;
    }

    @Override
    public void testDummy() {
        // To avoid JUnit warning that this class contains no tests, even though
        // this is an abstract class and JUnit shouldn't try
    }
}
