/*
 * 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.common.layout;

import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ANDROID_WIDGET_PREFIX;
import static com.android.SdkConstants.ATTR_ID;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.api.IAttributeInfo;
import com.android.ide.common.api.INode;
import com.android.ide.common.api.INodeHandler;
import com.android.ide.common.api.Margins;
import com.android.ide.common.api.Rect;
import com.android.ide.common.xml.XmlFormatStyle;
import com.android.ide.common.xml.XmlPrettyPrinter;
import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlFormatPreferences;
import com.android.ide.eclipse.adt.internal.editors.formatting.EclipseXmlPrettyPrinter;
import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
import com.google.common.base.Splitter;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;

import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Test/mock implementation of {@link INode} */
@SuppressWarnings("javadoc")
public class TestNode implements INode {
    private TestNode mParent;

    private final List<TestNode> mChildren = new ArrayList<TestNode>();

    private final String mFqcn;

    private Rect mBounds = new Rect(); // Invalid bounds initially

    private Map<String, IAttribute> mAttributes = new HashMap<String, IAttribute>();

    private Map<String, IAttributeInfo> mAttributeInfos = new HashMap<String, IAttributeInfo>();

    private List<String> mAttributeSources;

    public TestNode(String fqcn) {
        this.mFqcn = fqcn;
    }

    public TestNode bounds(Rect bounds) {
        this.mBounds = bounds;

        return this;
    }

    public TestNode id(String id) {
        return set(ANDROID_URI, ATTR_ID, id);
    }

    public TestNode set(String uri, String name, String value) {
        setAttribute(uri, name, value);

        return this;
    }

    public TestNode add(TestNode child) {
        mChildren.add(child);
        child.mParent = this;

        return this;
    }

    public TestNode add(TestNode... children) {
        for (TestNode child : children) {
            mChildren.add(child);
            child.mParent = this;
        }

        return this;
    }

    public static TestNode create(String fcqn) {
        return new TestNode(fcqn);
    }

    public void removeChild(int index) {
        TestNode removed = mChildren.remove(index);
        removed.mParent = null;
    }

    // ==== INODE ====

    @Override
    public @NonNull INode appendChild(@NonNull String viewFqcn) {
        return insertChildAt(viewFqcn, mChildren.size());
    }

    @Override
    public void editXml(@NonNull String undoName, @NonNull INodeHandler callback) {
        callback.handle(this);
    }

    public void putAttributeInfo(String uri, String attrName, IAttributeInfo info) {
        mAttributeInfos.put(uri + attrName, info);
    }

    @Override
    public IAttributeInfo getAttributeInfo(@Nullable String uri, @NonNull String attrName) {
        return mAttributeInfos.get(uri + attrName);
    }

    @Override
    public @NonNull Rect getBounds() {
        return mBounds;
    }

    @Override
    public @NonNull INode[] getChildren() {
        return mChildren.toArray(new INode[mChildren.size()]);
    }

    @Override
    public @NonNull IAttributeInfo[] getDeclaredAttributes() {
        return mAttributeInfos.values().toArray(new IAttributeInfo[mAttributeInfos.size()]);
    }

    @Override
    public @NonNull String getFqcn() {
        return mFqcn;
    }

    @Override
    public @NonNull IAttribute[] getLiveAttributes() {
        return mAttributes.values().toArray(new IAttribute[mAttributes.size()]);
    }

    @Override
    public INode getParent() {
        return mParent;
    }

    @Override
    public INode getRoot() {
        TestNode curr = this;
        while (curr.mParent != null) {
            curr = curr.mParent;
        }

        return curr;
    }

    @Override
    public String getStringAttr(@Nullable String uri, @NonNull String attrName) {
        IAttribute attr = mAttributes.get(uri + attrName);
        if (attr == null) {
            return null;
        }

        return attr.getValue();
    }

    @Override
    public @NonNull INode insertChildAt(@NonNull String viewFqcn, int index) {
        TestNode child = new TestNode(viewFqcn);
        if (index == -1) {
            mChildren.add(child);
        } else {
            mChildren.add(index, child);
        }
        child.mParent = this;
        return child;
    }

    @Override
    public void removeChild(@NonNull INode node) {
        int index = mChildren.indexOf(node);
        if (index != -1) {
            removeChild(index);
        }
    }

    @Override
    public boolean setAttribute(@Nullable String uri, @NonNull String localName,
            @Nullable String value) {
        mAttributes.put(uri + localName, new TestAttribute(uri, localName, value));
        return true;
    }

    @Override
    public String toString() {
        String id = getStringAttr(ANDROID_URI, ATTR_ID);
        return "TestNode [id=" + (id != null ? id : "?") + ", fqn=" + mFqcn + ", infos="
                + mAttributeInfos + ", attributes=" + mAttributes + ", bounds=" + mBounds + "]";
    }

    @Override
    public int getBaseline() {
        return -1;
    }

    @Override
    public @NonNull Margins getMargins() {
        return null;
    }

    @Override
    public @NonNull List<String> getAttributeSources() {
        return mAttributeSources != null ? mAttributeSources : Collections.<String>emptyList();
    }

    public void setAttributeSources(List<String> attributeSources) {
        mAttributeSources = attributeSources;
    }

    /** Create a test node from the given XML */
    public static TestNode createFromXml(String xml) {
        Document document = DomUtilities.parseDocument(xml, false);
        assertNotNull(document);
        assertNotNull(document.getDocumentElement());

        return createFromNode(document.getDocumentElement());
    }

    public static String toXml(TestNode node) {
        assertTrue("This method only works with nodes constructed from XML",
                node instanceof TestXmlNode);
        Document document = ((TestXmlNode) node).mElement.getOwnerDocument();
        // Insert new whitespace nodes etc
        String xml = dumpDocument(document);
        document = DomUtilities.parseDocument(xml, false);

        XmlPrettyPrinter printer = new EclipseXmlPrettyPrinter(EclipseXmlFormatPreferences.create(),
                XmlFormatStyle.LAYOUT, "\n");
        StringBuilder sb = new StringBuilder(1000);
        sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
        printer.prettyPrint(-1, document, null, null, sb, false);
        return sb.toString();
    }

    @SuppressWarnings("deprecation")
    private static String dumpDocument(Document document) {
        // Diagnostics: print out the XML that we're about to render
        org.apache.xml.serialize.OutputFormat outputFormat =
                new org.apache.xml.serialize.OutputFormat(
                        "XML", "ISO-8859-1", true); //$NON-NLS-1$ //$NON-NLS-2$
        outputFormat.setIndent(2);
        outputFormat.setLineWidth(100);
        outputFormat.setIndenting(true);
        outputFormat.setOmitXMLDeclaration(true);
        outputFormat.setOmitDocumentType(true);
        StringWriter stringWriter = new StringWriter();
        // Using FQN here to avoid having an import above, which will result
        // in a deprecation warning, and there isn't a way to annotate a single
        // import element with a SuppressWarnings.
        org.apache.xml.serialize.XMLSerializer serializer =
                new org.apache.xml.serialize.XMLSerializer(stringWriter, outputFormat);
        serializer.setNamespaces(true);
        try {
            serializer.serialize(document.getDocumentElement());
            return stringWriter.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static TestNode createFromNode(Element element) {
        String fqcn = ANDROID_WIDGET_PREFIX + element.getTagName();
        TestNode node = new TestXmlNode(fqcn, element);

        for (Element child : DomUtilities.getChildren(element)) {
            node.add(createFromNode(child));
        }

        return node;
    }

    @Nullable
    public static TestNode findById(TestNode node, String id) {
        id = BaseLayoutRule.stripIdPrefix(id);
        return node.findById(id);
    }

    private TestNode findById(String targetId) {
        String id = getStringAttr(ANDROID_URI, ATTR_ID);
        if (id != null && targetId.equals(BaseLayoutRule.stripIdPrefix(id))) {
            return this;
        }

        for (TestNode child : mChildren) {
            TestNode result = child.findById(targetId);
            if (result != null) {
                return result;
            }
        }

        return null;
    }

    private static String getTagName(String fqcn) {
        return fqcn.substring(fqcn.lastIndexOf('.') + 1);
    }

    private static class TestXmlNode extends TestNode {
        private final Element mElement;

        public TestXmlNode(String fqcn, Element element) {
            super(fqcn);
            mElement = element;
        }

        @Override
        public @NonNull IAttribute[] getLiveAttributes() {
            List<IAttribute> result = new ArrayList<IAttribute>();

            NamedNodeMap attributes = mElement.getAttributes();
            for (int i = 0, n = attributes.getLength(); i < n; i++) {
                Attr attribute = (Attr) attributes.item(i);
                result.add(new TestXmlAttribute(attribute));
            }
            return result.toArray(new IAttribute[result.size()]);
        }

        @Override
        public boolean setAttribute(String uri, String localName, String value) {
            if (value == null) {
                mElement.removeAttributeNS(uri, localName);
            } else {
                mElement.setAttributeNS(uri, localName, value);
            }
            return super.setAttribute(uri, localName, value);
        }

        @Override
        public INode appendChild(String viewFqcn) {
            Element child = mElement.getOwnerDocument().createElement(getTagName(viewFqcn));
            mElement.appendChild(child);
            return new TestXmlNode(viewFqcn, child);
        }

        @Override
        public INode insertChildAt(String viewFqcn, int index) {
            if (index == -1) {
                return appendChild(viewFqcn);
            }
            Element child = mElement.getOwnerDocument().createElement(getTagName(viewFqcn));
            List<Element> children = DomUtilities.getChildren(mElement);
            if (children.size() >= index) {
                Element before = children.get(index);
                mElement.insertBefore(child, before);
            } else {
                fail("Unexpected index");
                mElement.appendChild(child);
            }
            return new TestXmlNode(viewFqcn, child);
        }

        @Override
        public String getStringAttr(String uri, String name) {
            String value;
            if (uri == null) {
                value = mElement.getAttribute(name);
            } else {
                value = mElement.getAttributeNS(uri, name);
            }
            if (value.isEmpty()) {
                value = null;
            }

            return value;
        }

        @Override
        public void removeChild(INode node) {
            assert node instanceof TestXmlNode;
            mElement.removeChild(((TestXmlNode) node).mElement);
        }

        @Override
        public void removeChild(int index) {
            List<Element> children = DomUtilities.getChildren(mElement);
            assertTrue(index < children.size());
            Element oldChild = children.get(index);
            mElement.removeChild(oldChild);
        }
    }

    public static class TestXmlAttribute implements IAttribute {
        private Attr mAttribute;

        public TestXmlAttribute(Attr attribute) {
            this.mAttribute = attribute;
        }

        @Override
        public String getUri() {
            return mAttribute.getNamespaceURI();
        }

        @Override
        public String getName() {
            String name = mAttribute.getLocalName();
            if (name == null) {
                name = mAttribute.getName();
            }
            return name;
        }

        @Override
        public String getValue() {
            return mAttribute.getValue();
        }
    }

    // Recursively initialize this node with the bounds specified in the given hierarchy
    // dump (from ViewHierarchy's DUMP_INFO flag
    public void assignBounds(String bounds) {
        Iterable<String> split = Splitter.on('\n').trimResults().split(bounds);
        assignBounds(split.iterator());
    }

    private void assignBounds(Iterator<String> iterator) {
        assertTrue(iterator.hasNext());
        String desc = iterator.next();

        Pattern pattern = Pattern.compile("^\\s*(.+)\\s+\\[(.+)\\]\\s*(<.+>)?\\s*(\\S+)?\\s*$");
        Matcher matcher = pattern.matcher(desc);
        assertTrue(matcher.matches());
        String fqn = matcher.group(1);
        assertEquals(getFqcn(), fqn);
        String boundsString = matcher.group(2);
        String[] bounds = boundsString.split(",");
        assertEquals(boundsString, 4, bounds.length);
        try {
            int left = Integer.parseInt(bounds[0]);
            int top = Integer.parseInt(bounds[1]);
            int right = Integer.parseInt(bounds[2]);
            int bottom = Integer.parseInt(bounds[3]);
            mBounds = new Rect(left, top, right - left, bottom - top);
        } catch (NumberFormatException nufe) {
            fail(nufe.getLocalizedMessage());
        }
        String tag = matcher.group(3);

        for (INode child : getChildren()) {
            assertTrue(iterator.hasNext());
            ((TestNode) child).assignBounds(iterator);
        }
    }
}