package org.robolectric.util.inject;

import static java.util.Collections.reverseOrder;
import static java.util.Comparator.comparing;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceConfigurationError;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.Priority;

@SuppressWarnings({"NewApi", "AndroidJdkLibsChecker"})
class PluginFinder {

  private final ServiceFinderAdapter serviceFinderAdapter;

  public PluginFinder() {
    this(new ServiceFinderAdapter(null));
  }

  /**
   * @param classLoader
   *         the classloader to be used to load provider-configuration files
   *         and provider classes, or `null` if the system classloader (or,
   *         failing that, the bootstrap classloader) is to be used
   */
  public PluginFinder(ClassLoader classLoader) {
    this(new ServiceFinderAdapter(classLoader));
  }

  PluginFinder(ServiceFinderAdapter serviceFinderAdapter) {
    this.serviceFinderAdapter = serviceFinderAdapter;
  }

  /**
   * Returns an implementation class for the specified plugin.
   *
   * If there is more than such one candidate, the classes will be sorted by {@link Priority}
   * and the one with the highest priority will be returned. If multiple classes claim the same
   * priority, a {@link ServiceConfigurationError} will be thrown. Classes without a Priority
   * are treated as `@Priority(0)`.
   *
   * @param pluginType the class of the plugin type
   * @param <T> the class of the plugin type
   * @return the implementing class with the highest priority
   */
  @Nullable
  <T> Class<? extends T> findPlugin(Class<T> pluginType) {
    return best(pluginType, findPlugins(pluginType));
  }

  /**
   * Returns a list of implementation classes for the specified plugin, ordered from highest to
   * lowest priority. If no implementing classes can be found, an empty list is returned.
   *
   * @param pluginType the class of the plugin type
   * @param <T> the class of the plugin type
   * @return a prioritized list of implementation classes
   */
  @Nonnull
  <T> List<Class<? extends T>> findPlugins(Class<T> pluginType) {
    return prioritize(filter(serviceFinderAdapter.load(pluginType)));
  }

  private <T> Iterable<Class<? extends T>> filter(Iterable<Class<? extends T>> classes) {
    Set<Class<?>> superceded = new HashSet<>();
    for (Class<? extends T> clazz : classes) {
      Supercedes supercedes = clazz.getAnnotation(Supercedes.class);
      if (supercedes != null) {
        superceded.add(supercedes.value());
      }
    }
    if (superceded.isEmpty()) {
      return classes;
    } else {
      return () -> new Filterator<>(classes.iterator(), o -> !superceded.contains(o));
    }
  }

  @Nullable
  private <T> Class<? extends T> best(Class<T> pluginType,
      List<Class<? extends T>> serviceClasses) {
    if (serviceClasses.isEmpty()) {
      return null;
    }

    Class<? extends T> first = serviceClasses.get(0);
    if (serviceClasses.size() == 1) {
      return first;
    }

    int topPriority = priority(first);
    serviceClasses = serviceClasses.stream()
        .filter(it -> priority(it) == topPriority)
        .collect(Collectors.toList());

    if (serviceClasses.size() == 1) {
      return serviceClasses.get(0);
    } else {
      throw new InjectionException(pluginType, "too many implementations: " + serviceClasses);
    }
  }

  static class ServiceFinderAdapter {

    private final ClassLoader classLoader;

    ServiceFinderAdapter(ClassLoader classLoader) {
      this.classLoader = classLoader;
    }

    @Nonnull
    <T> Iterable<Class<? extends T>> load(Class<T> pluginType) {
      if (classLoader == null) {
        return ServiceFinder.load(pluginType);
      } else {
        return ServiceFinder.load(pluginType, classLoader);
      }
    }
  }

  @Nonnull
  private <T> List<Class<? extends T>> prioritize(Iterable<Class<? extends T>> iterable) {
    List<Class<? extends T>> serviceClasses = new ArrayList<>();

    for (Class<? extends T> serviceClass : iterable) {
      serviceClasses.add(serviceClass);
    }

    Comparator<Class<? extends T>> c = reverseOrder(comparing(PluginFinder::priority));
    c = c.thenComparing(Class::getName);
    serviceClasses.sort(c);

    return serviceClasses;
  }

  private static <T> int priority(Class<? extends T> pluginClass) {
    Priority priority = pluginClass.getAnnotation(Priority.class);
    return priority == null ? 0 : priority.value();
  }

  private static class Filterator<T> implements Iterator<T> {

    private final Iterator<T> delegate;
    private final Predicate<T> predicate;
    private T next;

    public Filterator(Iterator<T> delegate, Predicate<T> predicate) {
      this.delegate = delegate;
      this.predicate = predicate;
      findNext();
    }

    void findNext() {
      while (delegate.hasNext()) {
        next = delegate.next();
        if (predicate.test(next)) {
          return;
        }
      }
      next = null;
    }

    @Override
    public boolean hasNext() {
      return next != null;
    }

    @Override
    public T next() {
      try {
        return next;
      } finally {
        findNext();
      }
    }
  }
}


package org.robolectric.util;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

/**
 * An implementation of the Unix strftime with some glibc extensions.
 */
public class Strftime {

  /**
   * Format a date string.
   *
   * @param format The format in strftime syntax.
   * @param date The date to format.
   * @param locale The locale to use for formatting.
   * @param zone The timezone to use for formatting.
   * @return The formatted datetime.
   */
  public static String format(String format, final Date date, Locale locale, TimeZone zone) {
    StringBuilder buffer = new StringBuilder();

    class Formatter {
      SimpleDateFormat formatter;

      public Formatter(
          Date date,
          Locale locale,
          TimeZone timeZone) {
        if (locale != null) {
          formatter = new SimpleDateFormat("", locale);
        } else {
          formatter = new SimpleDateFormat("");
        }
        if (timeZone != null) {
          formatter.setTimeZone(timeZone);
        }
      }

      public String format(String format) {
        formatter.applyPattern(format);
        return formatter.format(date);
      }
    }

    Formatter formatter = new Formatter(date, locale, zone);

    Boolean inside = false;

    Boolean removePad = false;
    Boolean zeroPad = false;
    Boolean spacePad = false;

    Boolean upperCase = false;
    Boolean swapCase = false;

    StringBuilder padWidthBuffer = new StringBuilder();

    for (int i = 0; i < format.length(); i++) {
      Character c = format.charAt(i);

      if (!inside && c == '%') {
        inside = true;
        removePad = false;
        zeroPad = false;
        spacePad = false;
        upperCase = false;
        swapCase = false;
        padWidthBuffer = new StringBuilder();
      } else if(inside) {
        inside = false;
        switch (c) {
          // %a  Abbreviated weekday name according to locale.
          case 'a':
            buffer.append(
                correctCase(
                    formatter.format("EEE"),
                    upperCase,
                    swapCase));
            break;

          // %A  Full weekday name according to locale.
          case 'A':
            buffer.append(
                correctCase(
                    formatter.format("EEEE"),
                    upperCase,
                    swapCase));
            break;

          // %b  Abbreviated month name according to locale.
          case 'b':
            buffer.append(
                correctCase(
                    formatter.format("MMM"),
                    upperCase,
                    swapCase));
            break;

          // %B  Full month name according to locale.
          case 'B':
            buffer.append(
                correctCase(
                    formatter.format("MMMM"),
                    upperCase,
                    swapCase));
            break;

          // %c  Preferred date and time representation for locale.
          case 'c':
            // NOTE: en_US locale
            buffer.append(
                formatter.format("EEE dd MMM yyyy hh:mm:ss aa z"));
            break;

          // %C  Year divided by 100 and truncated to integer (00-99).
          case 'C':
            buffer.append(
                formatter.format("y").substring(0, 2));
            break;

          // %d   Day of the month as decimal number (01-31).
          case 'd':
            buffer.append(
                formatter.format("dd"));
            break;

          // %D  Same as "%m/%d/%y"
          case 'D':
            buffer.append(
                formatter.format("MM/dd/yy"));
            break;

          // %e  Day of the month as decimal number, padded with space.
          case 'e':
            buffer.append(
                correctPad(
                    formatter.format("dd"),
                    zeroPad,
                    true,
                    removePad,
                    (padWidthBuffer.length() <= 0
                        ? new StringBuilder("2")
                        : padWidthBuffer)));
            break;

          // %E  Modifier, use a locale-dependent alternative representation.
          case 'E':
            inside = true;
            throw new UnsupportedOperationException("Not implemented yet");
//            break;

          // %F  ISO 8601 date format: "%Y-%m-%d".
          case 'F':
            buffer.append(
                formatter.format("yyyy-MM-dd"));
            break;

          // %g  2-digit year version of %G, (00-99)
          case 'g':
            buffer.append(
                formatter.format("YY"));
            break;

          // %G  ISO 8601 week-based year.
          case 'G':
            buffer.append(
                formatter.format("YYYY"));
            break;

          // %h  Like %b.
          case 'h':
            buffer.append(
                formatter.format("MMM"));
            break;

          // %H  Hour (24-hour clock) as decimal number (00-23).
          case 'H':
            buffer.append(
                formatter.format("HH"));
            break;

          // %I  Hour (12-hour clock) as decimal number (01-12).
          case 'I':
            buffer.append(
                formatter.format("hh"));
            break;

          // %j  Day of the year as decimal number (001-366).
          case 'j':
            buffer.append(
                formatter.format("DDD"));
            break;

          // %k  Hour (24-hour clock) as decimal number (0-23), space padded.
          case 'k':
            buffer.append(
                correctPad(
                    formatter.format("HH"),
                    zeroPad,
                    spacePad,
                    removePad,
                    (padWidthBuffer.length() <= 0
                        ? new StringBuilder("2")
                        : padWidthBuffer)));
            break;

          // %l  Hour (12-hour clock) as decimal number (1-12), space padded.
          case 'l':
            buffer.append(
                correctPad(
                    formatter.format("hh"),
                    zeroPad,
                    spacePad || !zeroPad,
                    removePad,
                    (padWidthBuffer.length() <= 0
                        ? new StringBuilder("2")
                        : padWidthBuffer)));
            break;

          // %m  Month as decimal number (01-12).
          case 'm':
            buffer.append(
                correctPad(
                    formatter.format("MM"),
                    zeroPad,
                    spacePad,
                    removePad,
                    (padWidthBuffer.length() <= 0
                        ? new StringBuilder("2")
                        : padWidthBuffer)));
            break;

          // %M  Minute as decimal number (00-59).
          case 'M':
            buffer.append(
                correctCase(
                    formatter.format("mm"),
                    upperCase,
                    swapCase));
            break;

          // %n  Newline.
          case 'n':
            buffer.append(
                formatter.format("\n"));
            break;

          // %O  Modifier, use alternative numeric symbols (say, Roman numerals).
          case 'O':
            inside = true;
            throw new UnsupportedOperationException("Not implemented yet");
//            break;

          // %p  "AM", "PM", or locale string. Noon = "PM", midnight = "AM".
          case 'p':
            buffer.append(
                correctCase(
                    formatter.format("a"),
                    upperCase,
                    swapCase));
            break;

          // %P  "am", "pm", or locale string. Noon = "pm", midnight = "am".
          case 'P':
            buffer.append(
                correctCase(
                    formatter.format("a").toLowerCase(),
                    upperCase,
                    swapCase));
            break;

          // %r  12-hour clock time.
          case 'r':
            buffer.append(
                formatter.format("hh:mm:ss a"));
            break;

          // %R  24-hour clock time, "%H:%M".
          case 'R':
            buffer.append(
                formatter.format("HH:mm"));
            break;

          // %s  Number of seconds since Epoch, 1970-01-01 00:00:00 +0000 (UTC).
          case 's':
            buffer.append(
                ((Long) (date.getTime() / 1000)).toString());
            break;

          // %S  Second as decimal number (00-60). 60 for leap seconds.
          case 'S':
            buffer.append(
                formatter.format("ss"));
            break;

          // %t  Tab.
          case 't':
            buffer.append(
                formatter.format("\t"));
            break;

          // %T  24-hour time, "%H:%M:%S".
          case 'T':
            buffer.append(
                formatter.format("HH:mm:ss"));
            break;

          // %u  The day of the week as a decimal, (1-7). Monday being 1.
          case 'u':
            buffer.append(
                formatter.format("u"));
            break;

          // %U  week number of the current year as a decimal number, (00-53).
          // Starting with the first Sunday as the first day of week 01.
          case 'U':
            throw new UnsupportedOperationException("Not implemented yet");
            // buffer.append(
            //     formatter.format("ww"));
            // break;

          // %V  ISO 8601 week number (00-53).
          // Week 1 is the first week that has at least 4 days in the new year.
          case 'V':
            buffer.append(
                formatter.format("ww"));
            break;

          // %w  Day of the week as a decimal, (0-6). Sunday being 0.
          case 'w':
            String dayNumberOfWeek = formatter.format("u"); // (1-7)
            buffer.append(
                (dayNumberOfWeek.equals("7") ? "0" : dayNumberOfWeek));
            break;

          // %W  Week number of the current year as a decimal number, (00-53).
          // Starting with the first Monday as the first day of week 01.
          case 'W':
            throw new UnsupportedOperationException("Not implemented yet");
            // buffer.append(
            //     formatter.format("ww"));
            // break;

          // %x  Locale date without time.
          case 'x':
            buffer.append(
                formatter.format("MM/dd/yyyy"));
            break;

          // %X  Locale time without date.
          case 'X':
            buffer.append(
                formatter.format("hh:mm:ss aa"));
            // buffer.append(
            //     formatter.format("HH:mm:ss"));
            break;

          // %y  Year as decimal number without century (00-99).
          case 'y':
            buffer.append(
                formatter.format("yy"));
            break;

          // %Y  Year as decimal number with century.
          case 'Y':
            buffer.append(
                formatter.format("yyyy"));
            break;

          // %z  Numeric timezone as hour and minute offset from UTC "+hhmm" or "-hhmm".
          case 'z':
            buffer.append(
                formatter.format("Z"));
            break;

          // %Z  Timezone, name, or abbreviation.
          case 'Z':
            buffer.append(
                formatter.format("z"));
            break;

          // %%  Literal '%'.
          case '%':
            buffer.append(
                formatter.format("%"));
            break;

          // glibc extension

          // %^  Force upper case.
          case '^':
            inside = true;
            upperCase = true;
            break;

          // %#  Swap case.
          case '#':
            inside = true;
            swapCase = true;
            break;

          // %-  Remove padding.
          case '-':
            inside = true;
            removePad = true;
            break;

          // %_  Space pad.
          case '_':
            inside = true;
            spacePad = true;
            break;

          // %0  Zero pad.
          //  0  Alternatively if preceded by another digit, defines padding width.
          case '0':
            inside = true;
            if (padWidthBuffer.length() == 0) {
              zeroPad = true;
              spacePad = false;
            } else {
              padWidthBuffer.append(c);
            }
            break;

          // %1  Padding width.
          case '1':
          case '2':
          case '3':
          case '4':
          case '5':
          case '6':
          case '7':
          case '8':
          case '9':
            inside = true;
            // zeroPad = !spacePad; // Default to zero padding.
            padWidthBuffer.append(c);
            break;

          default:
            buffer.append(c.toString());
            break;
        }
      } else {
        buffer.append(c.toString());
      }
    }

    return buffer.toString();
  }

  private static String correctCase(
      String simple,
      Boolean upperCase,
      Boolean swapCase) {
    if (upperCase) {
      return simple.toUpperCase();
    }

    if (!swapCase) {
      return simple;
    }

    // swap case
    StringBuilder buffer = new StringBuilder();
    for (int i = 0; i < simple.length(); i++) {
      Character c = simple.charAt(i);
      buffer.append(
          (Character.isLowerCase(c)
              ? Character.toUpperCase(c)
              : Character.toLowerCase(c))
          );
    }

    return buffer.toString();
  }

  private static String correctPad(
      String simple,
      Boolean zeroPad,
      Boolean spacePad,
      Boolean removePad,
      StringBuilder padWidthBuffer) {
    String unpadded = simple.replaceFirst("^(0+| +)(?!$)", "");

    if (removePad) {
      return unpadded;
    }

    int padWidth = 0;
    if (padWidthBuffer.length() > 0) {
      padWidth = (
          Integer.parseInt(padWidthBuffer.toString()) - unpadded.length());
    }

    if (spacePad || zeroPad) {
      StringBuilder buffer = new StringBuilder();
      char padChar = (spacePad ? ' ' : '0');
      for (int i = 0 ; i < padWidth ; i++) {
        buffer.append(padChar);
      }
      buffer.append(unpadded);
      return buffer.toString();
    }

    return simple;
  }
}


package org.robolectric.android;

import static org.robolectric.res.AttributeResource.ANDROID_RES_NS_PREFIX;
import static org.robolectric.res.AttributeResource.RES_AUTO_NS_URI;

import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import com.android.internal.util.XmlUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import org.robolectric.res.AttributeResource;
import org.robolectric.res.Fs;
import org.robolectric.res.ResName;
import org.robolectric.res.ResourceTable;
import org.robolectric.res.StringResources;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xmlpull.v1.XmlPullParserException;

/**
 * Concrete implementation of the {@link XmlResourceParser}.
 *
 * Clients expects a pull parser while the resource loader
 * initialise this object with a {@link Document}.
 * This implementation navigates the dom and emulates a pull
 * parser by raising all the opportune events.
 *
 * Note that the original android implementation is based on
 * a set of native methods calls. Here those methods are
 * re-implemented in java when possible.
 */
public class XmlResourceParserImpl implements XmlResourceParser {

  /**
   * All the parser features currently supported by Android.
   */
  public static final String[] AVAILABLE_FEATURES = {
      XmlResourceParser.FEATURE_PROCESS_NAMESPACES,
      XmlResourceParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES
  };
  /**
   * All the parser features currently NOT supported by Android.
   */
  public static final String[] UNAVAILABLE_FEATURES = {
      XmlResourceParser.FEATURE_PROCESS_DOCDECL,
      XmlResourceParser.FEATURE_VALIDATION
  };

  private final Document document;
  private final Path fileName;
  private final String packageName;
  private final ResourceTable resourceTable;
  private final String applicationNamespace;

  private Node currentNode;

  private boolean mStarted = false;
  private boolean mDecNextDepth = false;
  private int mDepth = 0;
  private int mEventType = START_DOCUMENT;

  /**
   * @deprecated use {@link XmlResourceParserImpl#XmlResourceParserImpl(Document, Path, String,
   *     String, ResourceTable)} instead.
   */
  @Deprecated
  public XmlResourceParserImpl(
      Document document,
      String fileName,
      String packageName,
      String applicationPackageName,
      ResourceTable resourceTable) {
    this(document, Fs.fromUrl(fileName), packageName, applicationPackageName, resourceTable);
  }

  public XmlResourceParserImpl(
      Document document,
      Path fileName,
      String packageName,
      String applicationPackageName,
      ResourceTable resourceTable) {
    this.document = document;
    this.fileName = fileName;
    this.packageName = packageName;
    this.resourceTable = resourceTable;
    this.applicationNamespace = ANDROID_RES_NS_PREFIX + applicationPackageName;
  }

  @Override
  public void setFeature(String name, boolean state)
      throws XmlPullParserException {
    if (isAndroidSupportedFeature(name) && state) {
      return;
    }
    throw new XmlPullParserException("Unsupported feature: " + name);
  }

  @Override
  public boolean getFeature(String name) {
    return isAndroidSupportedFeature(name);
  }

  @Override
  public void setProperty(String name, Object value)
      throws XmlPullParserException {
    throw new XmlPullParserException("setProperty() not supported");
  }

  @Override
  public Object getProperty(String name) {
    // Properties are not supported. Android returns null
    // instead of throwing an XmlPullParserException.
    return null;
  }

  @Override
  public void setInput(Reader in) throws XmlPullParserException {
    throw new XmlPullParserException("setInput() not supported");
  }

  @Override
  public void setInput(InputStream inputStream, String inputEncoding)
      throws XmlPullParserException {
    throw new XmlPullParserException("setInput() not supported");
  }

  @Override
  public void defineEntityReplacementText(
      String entityName, String replacementText)
      throws XmlPullParserException {
    throw new XmlPullParserException(
        "defineEntityReplacementText() not supported");
  }

  @Override
  public String getNamespacePrefix(int pos)
      throws XmlPullParserException {
    throw new XmlPullParserException(
        "getNamespacePrefix() not supported");
  }

  @Override
  public String getInputEncoding() {
    return null;
  }

  @Override
  public String getNamespace(String prefix) {
    throw new RuntimeException(
        "getNamespaceCount() not supported");
  }

  @Override
  public int getNamespaceCount(int depth)
      throws XmlPullParserException {
    throw new XmlPullParserException(
        "getNamespaceCount() not supported");
  }

  @Override
  public String getPositionDescription() {
    return "XML file " + fileName + " line #" + getLineNumber() + " (sorry, not yet implemented)";
  }

  @Override
  public String getNamespaceUri(int pos)
      throws XmlPullParserException {
    throw new XmlPullParserException(
        "getNamespaceUri() not supported");
  }

  @Override
  public int getColumnNumber() {
    // Android always returns -1
    return -1;
  }

  @Override
  public int getDepth() {
    return mDepth;
  }

  @Override
  public String getText() {
    if (currentNode == null) {
      return "";
    }
    return StringResources.processStringResources(currentNode.getTextContent());
  }

  @Override
  public int getLineNumber() {
    // TODO(msama): The current implementation is
    //   unable to return line numbers.
    return -1;
  }

  @Override
  public int getEventType()
      throws XmlPullParserException {
    return mEventType;
  }

  /*package*/
  public boolean isWhitespace(String text)
      throws XmlPullParserException {
    if (text == null) {
      return false;
    }
    return text.split("\\s").length == 0;
  }

  @Override
  public boolean isWhitespace()
      throws XmlPullParserException {
    // Note: in android whitespaces are automatically stripped.
    // Here we have to skip them manually
    return isWhitespace(getText());
  }

  @Override
  public String getPrefix() {
    throw new RuntimeException("getPrefix not supported");
  }

  @Override
  public char[] getTextCharacters(int[] holderForStartAndLength) {
    String txt = getText();
    char[] chars = null;
    if (txt != null) {
      holderForStartAndLength[0] = 0;
      holderForStartAndLength[1] = txt.length();
      chars = new char[txt.length()];
      txt.getChars(0, txt.length(), chars, 0);
    }
    return chars;
  }

  @Override
  public String getNamespace() {
    String namespace = currentNode != null ? currentNode.getNamespaceURI() : null;
    if (namespace == null) {
      return "";
    }

    return maybeReplaceNamespace(namespace);
  }

  @Override
  public String getName() {
    if (currentNode == null) {
      return null;
    }
    return currentNode.getNodeName();
  }

  Node getAttributeAt(int index) {
    if (currentNode == null) {
      throw new IndexOutOfBoundsException(String.valueOf(index));
    }
    NamedNodeMap map = currentNode.getAttributes();
    if (index >= map.getLength()) {
      throw new IndexOutOfBoundsException(String.valueOf(index));
    }
    return map.item(index);
  }

  public String getAttribute(String namespace, String name) {
    if (currentNode == null) {
      return null;
    }

    Element element = (Element) currentNode;
    if (element.hasAttributeNS(namespace, name)) {
      return element.getAttributeNS(namespace, name).trim();
    } else if (applicationNamespace.equals(namespace)
        && element.hasAttributeNS(AttributeResource.RES_AUTO_NS_URI, name)) {
      return element.getAttributeNS(AttributeResource.RES_AUTO_NS_URI, name).trim();
    }

    return null;
  }

  @Override
  public String getAttributeNamespace(int index) {
    Node attr = getAttributeAt(index);
    if (attr == null) {
      return "";
    }
    return maybeReplaceNamespace(attr.getNamespaceURI());
  }

  private String maybeReplaceNamespace(String namespace) {
    if (namespace == null) {
      return "";
    } else if (namespace.equals(applicationNamespace)) {
      return AttributeResource.RES_AUTO_NS_URI;
    } else {
      return namespace;
    }
  }

  @Override
  public String getAttributeName(int index) {
    Node attr = getAttributeAt(index);
    String name = attr.getLocalName();
    return name == null ? attr.getNodeName() : name;
  }

  @Override
  public String getAttributePrefix(int index) {
    throw new RuntimeException("getAttributePrefix not supported");
  }

  @Override
  public boolean isEmptyElementTag() throws XmlPullParserException {
    // In Android this method is left unimplemented.
    // This implementation is mirroring that.
    return false;
  }

  @Override
  public int getAttributeCount() {
    if (currentNode == null) {
      return -1;
    }
    return currentNode.getAttributes().getLength();
  }

  @Override
  public String getAttributeValue(int index) {
    return qualify(getAttributeAt(index).getNodeValue());
  }

  // for testing only...
  public String qualify(String value) {
    if (value == null) return null;
    if (AttributeResource.isResourceReference(value)) {
      return "@" + ResName.qualifyResourceName(value.trim().substring(1).replace("+", ""), packageName, "attr");
    } else if (AttributeResource.isStyleReference(value)) {
      return "?" + ResName.qualifyResourceName(value.trim().substring(1), packageName, "attr");
    } else {
      return StringResources.processStringResources(value);
    }
  }

  @Override
  public String getAttributeType(int index) {
    // Android always returns CDATA even if the
    // node has no attribute.
    return "CDATA";
  }

  @Override
  public boolean isAttributeDefault(int index) {
    // The android implementation always returns false
    return false;
  }

  @Override
  public int nextToken() throws XmlPullParserException, IOException {
    return next();
  }

  @Override
  public String getAttributeValue(String namespace, String name) {
    return qualify(getAttribute(namespace, name));
  }

  @Override
  public int next() throws XmlPullParserException, IOException {
    if (!mStarted) {
      mStarted = true;
      return START_DOCUMENT;
    }
    if (mEventType == END_DOCUMENT) {
      return END_DOCUMENT;
    }
    int ev = nativeNext();
    if (mDecNextDepth) {
      mDepth--;
      mDecNextDepth = false;
    }
    switch (ev) {
      case START_TAG:
        mDepth++;
        break;
      case END_TAG:
        mDecNextDepth = true;
        break;
    }
    mEventType = ev;
    if (ev == END_DOCUMENT) {
      // Automatically close the parse when we reach the end of
      // a document, since the standard XmlPullParser interface
      // doesn't have such an API so most clients will leave us
      // dangling.
      close();
    }
    return ev;
  }

  /**
   * A twin implementation of the native android nativeNext(status)
   *
   * @throws XmlPullParserException
   */
  private int nativeNext() throws XmlPullParserException {
    switch (mEventType) {
      case (CDSECT): {
        throw new IllegalArgumentException(
            "CDSECT is not handled by Android");
      }
      case (COMMENT): {
        throw new IllegalArgumentException(
            "COMMENT is not handled by Android");
      }
      case (DOCDECL): {
        throw new IllegalArgumentException(
            "DOCDECL is not handled by Android");
      }
      case (ENTITY_REF): {
        throw new IllegalArgumentException(
            "ENTITY_REF is not handled by Android");
      }
      case (END_DOCUMENT): {
        // The end document event should have been filtered
        // from the invoker. This should never happen.
        throw new IllegalArgumentException(
            "END_DOCUMENT should not be found here.");
      }
      case (END_TAG): {
        return navigateToNextNode(currentNode);
      }
      case (IGNORABLE_WHITESPACE): {
        throw new IllegalArgumentException(
            "IGNORABLE_WHITESPACE");
      }
      case (PROCESSING_INSTRUCTION): {
        throw new IllegalArgumentException(
            "PROCESSING_INSTRUCTION");
      }
      case (START_DOCUMENT): {
        currentNode = document.getDocumentElement();
        return START_TAG;
      }
      case (START_TAG): {
        if (currentNode.hasChildNodes()) {
          // The node has children, navigate down
          return processNextNodeType(
              currentNode.getFirstChild());
        } else {
          // The node has no children
          return END_TAG;
        }
      }
      case (TEXT): {
        return navigateToNextNode(currentNode);
      }
      default: {
        // This can only happen if mEventType is
        // assigned with an unmapped integer.
        throw new RuntimeException(
            "Robolectric-> Uknown XML event type: " + mEventType);
      }
    }

  }

  /*protected*/ int processNextNodeType(Node node)
      throws XmlPullParserException {
    switch (node.getNodeType()) {
      case (Node.ATTRIBUTE_NODE): {
        throw new IllegalArgumentException("ATTRIBUTE_NODE");
      }
      case (Node.CDATA_SECTION_NODE): {
        return navigateToNextNode(node);
      }
      case (Node.COMMENT_NODE): {
        return navigateToNextNode(node);
      }
      case (Node.DOCUMENT_FRAGMENT_NODE): {
        throw new IllegalArgumentException("DOCUMENT_FRAGMENT_NODE");
      }
      case (Node.DOCUMENT_NODE): {
        throw new IllegalArgumentException("DOCUMENT_NODE");
      }
      case (Node.DOCUMENT_TYPE_NODE): {
        throw new IllegalArgumentException("DOCUMENT_TYPE_NODE");
      }
      case (Node.ELEMENT_NODE): {
        currentNode = node;
        return START_TAG;
      }
      case (Node.ENTITY_NODE): {
        throw new IllegalArgumentException("ENTITY_NODE");
      }
      case (Node.ENTITY_REFERENCE_NODE): {
        throw new IllegalArgumentException("ENTITY_REFERENCE_NODE");
      }
      case (Node.NOTATION_NODE): {
        throw new IllegalArgumentException("DOCUMENT_TYPE_NODE");
      }
      case (Node.PROCESSING_INSTRUCTION_NODE): {
        throw new IllegalArgumentException("DOCUMENT_TYPE_NODE");
      }
      case (Node.TEXT_NODE): {
        if (isWhitespace(node.getNodeValue())) {
          // Skip whitespaces
          return navigateToNextNode(node);
        } else {
          currentNode = node;
          return TEXT;
        }
      }
      default: {
        throw new RuntimeException(
            "Robolectric -> Unknown node type: " +
                node.getNodeType() + ".");
      }
    }
  }

  /**
   * Navigate to the next node after a node and all of his
   * children have been explored.
   *
   * If the node has unexplored siblings navigate to the
   * next sibling. Otherwise return to its parent.
   *
   * @param node the node which was just explored.
   * @return {@link XmlPullParserException#START_TAG} if the given
   *         node has siblings, {@link XmlPullParserException#END_TAG}
   *         if the node has no unexplored siblings or
   *         {@link XmlPullParserException#END_DOCUMENT} if the explored
   *         was the root document.
   * @throws XmlPullParserException if the parser fails to
   *                                parse the next node.
   */
  int navigateToNextNode(Node node)
      throws XmlPullParserException {
    Node nextNode = node.getNextSibling();
    if (nextNode != null) {
      // Move to the next siblings
      return processNextNodeType(nextNode);
    } else {
      // Goes back to the parent
      if (document.getDocumentElement().equals(node)) {
        currentNode = null;
        return END_DOCUMENT;
      }
      currentNode = node.getParentNode();
      return END_TAG;
    }
  }

  @Override
  public void require(int type, String namespace, String name)
      throws XmlPullParserException, IOException {
    if (type != getEventType()
        || (namespace != null && !namespace.equals(getNamespace()))
        || (name != null && !name.equals(getName()))) {
      throw new XmlPullParserException(
          "expected " + TYPES[type] + getPositionDescription());
    }
  }

  @Override
  public String nextText() throws XmlPullParserException, IOException {
    if (getEventType() != START_TAG) {
      throw new XmlPullParserException(
          getPositionDescription()
              + ": parser must be on START_TAG to read next text", this, null);
    }
    int eventType = next();
    if (eventType == TEXT) {
      String result = getText();
      eventType = next();
      if (eventType != END_TAG) {
        throw new XmlPullParserException(
            getPositionDescription()
                + ": event TEXT it must be immediately followed by END_TAG", this, null);
      }
      return result;
    } else if (eventType == END_TAG) {
      return "";
    } else {
      throw new XmlPullParserException(
          getPositionDescription()
              + ": parser must be on START_TAG or TEXT to read text", this, null);
    }
  }

  @Override
  public int nextTag() throws XmlPullParserException, IOException {
    int eventType = next();
    if (eventType == TEXT && isWhitespace()) { // skip whitespace
      eventType = next();
    }
    if (eventType != START_TAG && eventType != END_TAG) {
      throw new XmlPullParserException(
          "Expected start or end tag. Found: " + eventType, this, null);
    }
    return eventType;
  }

  @Override
  public int getAttributeNameResource(int index) {
    String attributeNamespace = getAttributeNamespace(index);
    if (attributeNamespace.equals(RES_AUTO_NS_URI)) {
      attributeNamespace = packageName;
    } else if (attributeNamespace.startsWith(ANDROID_RES_NS_PREFIX)) {
      attributeNamespace = attributeNamespace.substring(ANDROID_RES_NS_PREFIX.length());
    }
    return getResourceId(getAttributeName(index), attributeNamespace, "attr");
  }

  @Override
  public int getAttributeListValue(String namespace, String attribute,
      String[] options, int defaultValue) {
    String attr = getAttribute(namespace, attribute);
    if (attr == null) {
      return 0;
    }
    List<String> optList = Arrays.asList(options);
    int index = optList.indexOf(attr);
    if (index == -1) {
      return defaultValue;
    }
    return index;
  }

  @Override
  public boolean getAttributeBooleanValue(String namespace, String attribute,
      boolean defaultValue) {
    String attr = getAttribute(namespace, attribute);
    if (attr == null) {
      return defaultValue;
    }
    return Boolean.parseBoolean(attr);
  }

  @Override
  public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) {
    String attr = getAttribute(namespace, attribute);
    if (attr != null && attr.startsWith("@") && !AttributeResource.isNull(attr)) {
      return getResourceId(attr, packageName, null);
    }
    return defaultValue;
  }

  @Override
  public int getAttributeIntValue(String namespace, String attribute, int defaultValue) {
    return XmlUtils.convertValueToInt(this.getAttributeValue(namespace, attribute), defaultValue);
  }

  @Override
  public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) {
    int value = getAttributeIntValue(namespace, attribute, defaultValue);
    if (value < 0) {
      return defaultValue;
    }
    return value;
  }

  @Override
  public float getAttributeFloatValue(String namespace, String attribute,
      float defaultValue) {
    String attr = getAttribute(namespace, attribute);
    if (attr == null) {
      return defaultValue;
    }
    try {
      return Float.parseFloat(attr);
    } catch (NumberFormatException ex) {
      return defaultValue;
    }
  }

  @Override
  public int getAttributeListValue(
      int idx, String[] options, int defaultValue) {
    try {
      String value = getAttributeValue(idx);
      List<String> optList = Arrays.asList(options);
      int index = optList.indexOf(value);
      if (index == -1) {
        return defaultValue;
      }
      return index;
    } catch (IndexOutOfBoundsException ex) {
      return defaultValue;
    }
  }

  @Override
  public boolean getAttributeBooleanValue(
      int idx, boolean defaultValue) {
    try {
      return Boolean.parseBoolean(getAttributeValue(idx));
    } catch (IndexOutOfBoundsException ex) {
      return defaultValue;
    }
  }

  @Override
  public int getAttributeResourceValue(int idx, int defaultValue) {
    String attributeValue = getAttributeValue(idx);
    if (attributeValue != null && attributeValue.startsWith("@")) {
      int resourceId = getResourceId(attributeValue.substring(1), packageName, null);
      if (resourceId != 0) {
        return resourceId;
      }
    }
    return defaultValue;
  }

  @Override
  public int getAttributeIntValue(int idx, int defaultValue) {
    try {
      return Integer.parseInt(getAttributeValue(idx));
    } catch (NumberFormatException ex) {
      return defaultValue;
    } catch (IndexOutOfBoundsException ex) {
      return defaultValue;
    }
  }

  @Override
  public int getAttributeUnsignedIntValue(int idx, int defaultValue) {
    int value = getAttributeIntValue(idx, defaultValue);
    if (value < 0) {
      return defaultValue;
    }
    return value;
  }

  @Override
  public float getAttributeFloatValue(int idx, float defaultValue) {
    try {
      return Float.parseFloat(getAttributeValue(idx));
    } catch (NumberFormatException ex) {
      return defaultValue;
    } catch (IndexOutOfBoundsException ex) {
      return defaultValue;
    }
  }

  @Override
  public String getIdAttribute() {
    return getAttribute(null, "id");
  }

  @Override
  public String getClassAttribute() {
    return getAttribute(null, "class");
  }

  @Override
  public int getIdAttributeResourceValue(int defaultValue) {
    return getAttributeResourceValue(null, "id", defaultValue);
  }

  @Override
  public int getStyleAttribute() {
    String attr = getAttribute(null, "style");
    if (attr == null ||
        (!AttributeResource.isResourceReference(attr) && !AttributeResource.isStyleReference(attr))) {
      return 0;
    }

    int style = getResourceId(attr, packageName, "style");
    if (style == 0) {
      // try again with underscores...
      style = getResourceId(attr.replace('.', '_'), packageName, "style");
    }
    return style;
  }

  @Override
  public void close() {
    // Nothing to do
  }

  @Override
  protected void finalize() throws Throwable {
    close();
  }

  private int getResourceId(String possiblyQualifiedResourceName, String defaultPackageName, String defaultType) {

    if (AttributeResource.isNull(possiblyQualifiedResourceName)) return 0;

    if (AttributeResource.isStyleReference(possiblyQualifiedResourceName)) {
      ResName styleReference = AttributeResource.getStyleReference(possiblyQualifiedResourceName, defaultPackageName, "attr");
      Integer resourceId = resourceTable.getResourceId(styleReference);
      if (resourceId == null) {
        throw new Resources.NotFoundException(styleReference.getFullyQualifiedName());
      }
      return resourceId;
    }

    if (AttributeResource.isResourceReference(possiblyQualifiedResourceName)) {
      ResName resourceReference = AttributeResource.getResourceReference(possiblyQualifiedResourceName, defaultPackageName, defaultType);
      Integer resourceId = resourceTable.getResourceId(resourceReference);
      if (resourceId == null) {
        throw new Resources.NotFoundException(resourceReference.getFullyQualifiedName());
      }
      return resourceId;
    }
    possiblyQualifiedResourceName = removeLeadingSpecialCharsIfAny(possiblyQualifiedResourceName);
    ResName resName = ResName.qualifyResName(possiblyQualifiedResourceName, defaultPackageName, defaultType);
    Integer resourceId = resourceTable.getResourceId(resName);
    return resourceId == null ? 0 : resourceId;
  }

  private static String removeLeadingSpecialCharsIfAny(String name){
    if (name.startsWith("@+")) {
      return name.substring(2);
    }
    if (name.startsWith("@")) {
      return name.substring(1);
    }
    return name;
  }

  /**
   * Tell is a given feature is supported by android.
   *
   * @param name Feature name.
   * @return True if the feature is supported.
   */
  private static boolean isAndroidSupportedFeature(String name) {
    if (name == null) {
      return false;
    }
    for (String feature : AVAILABLE_FEATURES) {
      if (feature.equals(name)) {
        return true;
      }
    }
    return false;
  }
}