/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * 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 com.android.tools.idea.rendering;

import com.android.resources.ResourceFolderType;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.android.AndroidTestCase;
import org.jetbrains.annotations.Nullable;
import org.kxml2.io.KXmlParser;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.StringReader;
import java.util.SortedSet;
import java.util.TreeSet;

import static com.android.SdkConstants.*;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_TAG;

public class LayoutPsiPullParserTest extends AndroidTestCase {
  @SuppressWarnings("SpellCheckingInspection")
  public static final String BASE_PATH = "xmlpull/";

  public LayoutPsiPullParserTest() {
  }

  public void testDesignAttributes() throws Exception {
    @SuppressWarnings("SpellCheckingInspection")
    VirtualFile virtualFile = myFixture.copyFileToProject("xmlpull/designtime.xml", "res/layout/designtime.xml");
    assertNotNull(virtualFile);
    PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(virtualFile);
    assertTrue(psiFile instanceof XmlFile);
    XmlFile xmlFile = (XmlFile)psiFile;
    LayoutPsiPullParser parser = LayoutPsiPullParser.create(xmlFile, new RenderLogger("test", myModule));
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("LinearLayout", parser.getName());
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("TextView", parser.getName());
    assertEquals("@+id/first", parser.getAttributeValue(ANDROID_URI, ATTR_ID));
    assertEquals(END_TAG, parser.nextTag());
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("TextView", parser.getName());
    assertEquals("fill_parent", parser.getAttributeValue(ANDROID_URI, ATTR_LAYOUT_WIDTH)); // auto converted from match_parent
    assertEquals("wrap_content", parser.getAttributeValue(ANDROID_URI, ATTR_LAYOUT_HEIGHT));
    assertEquals("Designtime Text", parser.getAttributeValue(ANDROID_URI, ATTR_TEXT)); // overriding runtime text attribute
    assertEquals("@android:color/darker_gray", parser.getAttributeValue(ANDROID_URI, "textColor"));
    assertEquals(END_TAG, parser.nextTag());
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("TextView", parser.getName());
    assertEquals("@+id/blank", parser.getAttributeValue(ANDROID_URI, ATTR_ID));
    assertEquals("", parser.getAttributeValue(ANDROID_URI, ATTR_TEXT)); // Don't unset when no framework attribute is defined
    assertEquals(END_TAG, parser.nextTag());
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("ListView", parser.getName());
    assertEquals("@+id/listView", parser.getAttributeValue(ANDROID_URI, ATTR_ID));
    assertNull(parser.getAttributeValue(ANDROID_URI, "fastScrollAlwaysVisible")); // Cleared by overriding defined framework attribute
  }

  public void testRootFragment() throws Exception {
    @SuppressWarnings("SpellCheckingInspection")
    VirtualFile virtualFile = myFixture.copyFileToProject("xmlpull/root_fragment.xml", "res/layout/root_fragment.xml");
    assertNotNull(virtualFile);
    PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(virtualFile);
    assertTrue(psiFile instanceof XmlFile);
    XmlFile xmlFile = (XmlFile)psiFile;
    LayoutPsiPullParser parser = LayoutPsiPullParser.create(xmlFile, new RenderLogger("test", myModule));
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("FrameLayout", parser.getName()); // Automatically inserted surrounding the <include>
    assertEquals(7, parser.getAttributeCount());
    assertEquals("@+id/item_list", parser.getAttributeValue(ANDROID_URI, ATTR_ID));
    assertEquals("com.unit.test.app.ItemListFragment", parser.getAttributeValue(ANDROID_URI, ATTR_NAME));
    assertEquals("fill_parent", parser.getAttributeValue(ANDROID_URI, "layout_width"));
    assertEquals("fill_parent", parser.getAttributeValue(ANDROID_URI, "layout_height"));
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("include", parser.getName());
    assertEquals(null, parser.getAttributeValue(ANDROID_URI, ATTR_ID));
    //noinspection ConstantConditions
    assertEquals("@android:layout/list_content", parser.getAttributeValue(null, ATTR_LAYOUT));
    assertEquals("fill_parent", parser.getAttributeValue(ANDROID_URI, "layout_width"));
    assertEquals("fill_parent", parser.getAttributeValue(ANDROID_URI, "layout_height"));
    assertEquals(END_TAG, parser.nextTag());
  }

  public void testFrameLayoutInclude() throws Exception {
    @SuppressWarnings("SpellCheckingInspection")
    VirtualFile virtualFile = myFixture.copyFileToProject("xmlpull/frame_tools_layout.xml", "res/layout/frame_tools_layout.xml");
    assertNotNull(virtualFile);
    PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(virtualFile);
    assertTrue(psiFile instanceof XmlFile);
    XmlFile xmlFile = (XmlFile)psiFile;
    LayoutPsiPullParser parser = LayoutPsiPullParser.create(xmlFile, new RenderLogger("test", myModule));
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("FrameLayout", parser.getName()); // Automatically inserted surrounding the <include>
    assertEquals(5, parser.getAttributeCount());
    assertEquals("fill_parent", parser.getAttributeValue(ANDROID_URI, "layout_width"));
    assertEquals("fill_parent", parser.getAttributeValue(ANDROID_URI, "layout_height"));
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("include", parser.getName());
    assertEquals(null, parser.getAttributeValue(ANDROID_URI, ATTR_ID));
    //noinspection ConstantConditions
    assertEquals("@android:layout/list_content", parser.getAttributeValue(null, ATTR_LAYOUT));
    assertEquals("fill_parent", parser.getAttributeValue(ANDROID_URI, "layout_width"));
    assertEquals("fill_parent", parser.getAttributeValue(ANDROID_URI, "layout_height"));
    assertEquals(END_TAG, parser.nextTag());
  }

  public void testVisibleChild() throws Exception {
    @SuppressWarnings("SpellCheckingInspection")
    VirtualFile virtualFile = myFixture.copyFileToProject("xmlpull/visible_child.xml", "res/layout/visible_child.xml");
    assertNotNull(virtualFile);
    PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(virtualFile);
    assertTrue(psiFile instanceof XmlFile);
    XmlFile xmlFile = (XmlFile)psiFile;
    LayoutPsiPullParser parser = LayoutPsiPullParser.create(xmlFile, new RenderLogger("test", myModule));
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("FrameLayout", parser.getName());
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("Button", parser.getName());
    assertEquals("New Button", parser.getAttributeValue(ANDROID_URI, ATTR_TEXT));
    assertEquals("gone", parser.getAttributeValue(ANDROID_URI, ATTR_VISIBILITY));
    assertEquals(END_TAG, parser.nextTag());
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("CheckBox", parser.getName());
    assertEquals("New CheckBox", parser.getAttributeValue(ANDROID_URI, ATTR_TEXT));
    assertEquals("visible", parser.getAttributeValue(ANDROID_URI, ATTR_VISIBILITY));
    assertEquals(END_TAG, parser.nextTag());
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("TextView", parser.getName());
    assertEquals("New TextView", parser.getAttributeValue(ANDROID_URI, ATTR_TEXT));
    assertEquals("gone", parser.getAttributeValue(ANDROID_URI, ATTR_VISIBILITY));
    assertEquals(END_TAG, parser.nextTag());
    assertEquals(START_TAG, parser.nextTag());
    assertEquals("Switch", parser.getName());
    assertEquals("New Switch", parser.getAttributeValue(ANDROID_URI, ATTR_TEXT));
    assertEquals("visible", parser.getAttributeValue(ANDROID_URI, ATTR_VISIBILITY));
    assertEquals(END_TAG, parser.nextTag());
  }

  public void test1() throws Exception {
    checkFile("layout.xml", ResourceFolderType.LAYOUT);
  }

  public void test2() throws Exception {
    checkFile("simple.xml",  ResourceFolderType.LAYOUT);
  }

  enum NextEventType { NEXT, NEXT_TOKEN, NEXT_TAG }

  private void compareParsers(PsiFile file, NextEventType nextEventType) throws Exception {
    assertTrue(file instanceof XmlFile);
    XmlFile xmlFile = (XmlFile)file;
    KXmlParser referenceParser = createReferenceParser(file);
    LayoutPsiPullParser parser = LayoutPsiPullParser.create(xmlFile, new RenderLogger("test", myModule));

    assertEquals("Expected " + name(referenceParser.getEventType()) + " but was "
                 + name(parser.getEventType())
                 + " (at line:column " + describePosition(referenceParser) + ")",
                 referenceParser.getEventType(), parser.getEventType());

    while (true) {
      int expected, next;
      switch (nextEventType) {
        case NEXT:
          expected = referenceParser.next();
          next = parser.next();
          break;
        case NEXT_TOKEN:
          expected = referenceParser.nextToken();
          next = parser.nextToken();
          break;
        case NEXT_TAG: {
          try {
            expected = referenceParser.nextTag();
          } catch (Exception e) {
            expected = referenceParser.getEventType();
          }
          try {
            next = parser.nextTag();
          } catch (Exception e) {
            next = parser.getEventType();
          }
          break;
        }
        default:
          fail("Unexpected type");
          return;
      }

      PsiElement element = null;
      if (expected == XmlPullParser.START_TAG) {
        assertNotNull(parser.getViewKey());
        assertNotNull(parser.getViewCookie());
        assertTrue(parser.getViewCookie() instanceof PsiElement);
        element = (PsiElement)parser.getViewCookie();
      }

      if (expected == XmlPullParser.START_TAG) {
        assertEquals(referenceParser.getName(), parser.getName());
        if (element != xmlFile.getRootTag()) { // KXmlParser seems to not include xmlns: attributes on the root tag!{
          SortedSet<String> referenceAttributes = new TreeSet<String>();
          SortedSet<String> attributes = new TreeSet<String>();
          for (int i = 0; i < referenceParser.getAttributeCount(); i++) {
            String s = referenceParser.getAttributePrefix(i) + ':' + referenceParser.getAttributeName(i) + '='
                       + referenceParser.getAttributeValue(i);
            referenceAttributes.add(s);
          }
          for (int i = 0; i < parser.getAttributeCount(); i++) {
            String s = parser.getAttributePrefix(i) + ':' + parser.getAttributeName(i) + '=' + parser.getAttributeValue(i);
            attributes.add(s);
            if (parser.getAttributeNamespace(i) != null) {
              //noinspection ConstantConditions
              assertEquals(normalizeValue(parser.getAttributeValue(i)),
                           normalizeValue(parser.getAttributeValue(parser.getAttributeNamespace(i), parser.getAttributeName(i))));
            }
          }

          assertEquals(referenceAttributes, attributes);
        }

        // We're not correctly implementing this; it turns out Android doesn't need it, so we haven't bothered
        // pulling out the state correctly to do it
        //assertEquals(referenceParser.isEmptyElementTag(), parser.isEmptyElementTag());

        if (element instanceof XmlTag) {
          XmlTag tag = (XmlTag)element;
          for (XmlAttribute attribute : tag.getAttributes()) {
            String namespace = attribute.getNamespace();
            String name = attribute.getLocalName();
            if (namespace.isEmpty()) {
              String prefix = attribute.getNamespacePrefix();
              if (!prefix.isEmpty()) {
                name = prefix + ":" + prefix;
              }
            }
            //noinspection ConstantConditions
            assertEquals(namespace + ':' + name + " in element " + parser.getName(),
                         normalizeValue(referenceParser.getAttributeValue(namespace, name)),
                         normalizeValue(parser.getAttributeValue(namespace, name)));
          }
        }
      } else if (expected == XmlPullParser.TEXT || expected == XmlPullParser.COMMENT) {
        assertEquals(StringUtil.notNullize(referenceParser.getText()).trim(), StringUtil.notNullize(parser.getText()).trim());
      }

      if (expected != next) {
        assertEquals("Expected " + name(expected) + " but was " + name(next)
                     + "(At " + describePosition(referenceParser) + ")",
                     expected, next);
      }
      if (expected == XmlPullParser.END_DOCUMENT) {
        break;
      }
    }
  }

  @Nullable
  private static String normalizeValue(String value) {
    // Some parser translate values; ensure that these are identical
    if (value != null && value.equals(VALUE_MATCH_PARENT)) {
      return VALUE_FILL_PARENT;
    }
    return value;
  }

  private static String name(int event) {
    return XmlPullParser.TYPES[event];
  }

  private void checkFile(String filename, ResourceFolderType folder) throws Exception {
    VirtualFile file = myFixture.copyFileToProject(BASE_PATH + filename, "res/" + folder.getName() + "/" + filename);
    assertNotNull(file);
    PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(file);
    assertNotNull(psiFile);
    compareParsers(psiFile, NextEventType.NEXT_TAG);
    // The LayoutPsiPullParser only supports tags, not text (no text is used in layouts)
    //compareParsers(psiFile, NextEventType.NEXT);
    //compareParsers(psiFile, NextEventType.NEXT_TOKEN);
  }

  private static KXmlParser createReferenceParser(PsiFile file) throws XmlPullParserException {
    KXmlParser referenceParser = new KXmlParser();
    referenceParser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
    referenceParser.setInput(new StringReader(file.getText()));
    return referenceParser;
  }

  private static String describePosition(KXmlParser referenceParser) {
    return referenceParser.getLineNumber() + ":" + referenceParser.getColumnNumber();
  }
}
