// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.android.xml;

import com.android.aapt.Resources.Array;
import com.android.aapt.Resources.Array.Element;
import com.android.aapt.Resources.Item;
import com.android.aapt.Resources.Reference;
import com.android.aapt.Resources.Value;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.android.AndroidDataWritingVisitor;
import com.google.devtools.build.android.AndroidDataWritingVisitor.ValuesResourceDefinition;
import com.google.devtools.build.android.AndroidResourceSymbolSink;
import com.google.devtools.build.android.DataSource;
import com.google.devtools.build.android.DependencyInfo;
import com.google.devtools.build.android.FullyQualifiedName;
import com.google.devtools.build.android.XmlResourceValue;
import com.google.devtools.build.android.XmlResourceValues;
import com.google.devtools.build.android.proto.SerializeFormat;
import com.google.devtools.build.android.resources.Visibility;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.annotation.concurrent.Immutable;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;

/**
 * Represents an Android resource array.
 *
 * There are two flavors of Android Resource arrays:
 * <ul>
 * <li>Typed arrays (http://developer.android.com/guide/topics/resources/more-resources
 * .html#TypedArray) which which are indicated by a &lt;array&gt; tag.</li>
 * <li>Integer array (http://developer.android.com/guide/topics/resources/more-resources
 * .html#IntegerArray) which are indicated by &lt;integer-array&gt; tag.</li>
 * <li>String array (http://developer.android.com/guide/topics/resources/string-resource
 * .html#StringArray) which are indicated by &lt;string-array&gt; tag.</li>
 * </ul>
 *
 * Both of these are accessed by R.array.&lt;name&gt; in java.
 */
@Immutable
public class ArrayXmlResourceValue implements XmlResourceValue {
  private static final QName TAG_INTEGER_ARRAY = QName.valueOf("integer-array");
  private static final QName TAG_ARRAY = QName.valueOf("array");
  private static final QName TAG_STRING_ARRAY = QName.valueOf("string-array");
  /**
   * Enumerates the different types of array parentTags.
   */
  public enum ArrayType {
    INTEGER_ARRAY(TAG_INTEGER_ARRAY),
    ARRAY(TAG_ARRAY),
    STRING_ARRAY(TAG_STRING_ARRAY);

    public final QName tagName;

    ArrayType(QName tagName) {
      this.tagName = tagName;
    }

    public static ArrayType fromTagName(StartElement start) {
      final QName tagQName = start.getName();
      for (ArrayType arrayType : values()) {
        if (arrayType.tagName.equals(tagQName)) {
          return arrayType;
        }
      }
      throw new IllegalArgumentException(
          String.format("%s not found in %s", tagQName, Arrays.toString(values())));
    }
  }

  private final Visibility visibility;
  private final Array array;
  // TODO(b/112848607): remove the weakly-typed "values" member in favor of "array" above.
  private final ImmutableList<String> values;
  private final ArrayType arrayType;
  private final ImmutableMap<String, String> attributes;

  private ArrayXmlResourceValue(
      Visibility visibility,
      Array array,
      ArrayType arrayType,
      List<String> values,
      Map<String, String> attributes) {
    this.visibility = visibility;
    this.array = array;
    this.arrayType = arrayType;
    this.values = ImmutableList.copyOf(values);
    this.attributes = ImmutableMap.copyOf(attributes);
  }

  @VisibleForTesting
  public static XmlResourceValue of(ArrayType arrayType, String... values) {
    return of(arrayType, Arrays.asList(values));
  }

  public static XmlResourceValue of(ArrayType arrayType, List<String> values) {
    return of(arrayType, values, ImmutableMap.<String, String>of());
  }

  public static XmlResourceValue of(
      ArrayType arrayType, List<String> values, ImmutableMap<String, String> attributes) {
    return new ArrayXmlResourceValue(
        Visibility.UNKNOWN, Array.getDefaultInstance(), arrayType, values, attributes);
  }

  @SuppressWarnings("deprecation")
  public static XmlResourceValue from(SerializeFormat.DataValueXml proto) {
    return of(
        ArrayType.valueOf(proto.getValueType()),
        proto.getListValueList(),
        ImmutableMap.copyOf(proto.getAttributeMap()));
  }

  public static XmlResourceValue from(Value proto, Visibility visibility) {
    Array array = proto.getCompoundValue().getArray();
    List<String> items = new ArrayList<>();

    for (Element entry : array.getElementList()) {
      Item item = entry.getItem();

      if (item.hasPrim()) {
        items.add(SimpleXmlResourceValue.convertPrimitiveToString(item.getPrim()));
      } else if (item.hasRef()) {
        items.add("@" + item.getRef().getName());
      } else if (item.hasStr()) {
        items.add(item.getStr().getValue());
      }
    }

    return new ArrayXmlResourceValue(visibility, array, ArrayType.ARRAY, items, ImmutableMap.of());
  }

  @Override
  public void write(
      FullyQualifiedName key, DataSource source, AndroidDataWritingVisitor mergedDataWriter) {
    ValuesResourceDefinition definition = mergedDataWriter.define(key).derivedFrom(source)
        .startTag(arrayType.tagName)
        .named(key)
        .addAttributesFrom(attributes.entrySet())
        .closeTag();
    for (String value : values) {
      definition =
          definition
              .startItemTag()
              .closeTag()
              .addCharactersOf(value)
              .endTag()
              .addCharactersOf("\n");
    }
    definition.endTag().save();
  }

  @Override
  public int serializeTo(int sourceId, Namespaces namespaces, OutputStream output)
      throws IOException {
    return XmlResourceValues.serializeProtoDataValue(
        output,
        XmlResourceValues.newSerializableDataValueBuilder(sourceId)
            .setXmlValue(
                SerializeFormat.DataValueXml.newBuilder()
                    .addAllListValue(values)
                    .setType(SerializeFormat.DataValueXml.XmlType.ARRAY)
                    .putAllNamespace(namespaces.asMap())
                    .putAllAttribute(attributes)
                    .setValueType(arrayType.toString())));
  }

  @Override
  public int hashCode() {
    return Objects.hash(visibility, arrayType, values, attributes);
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof ArrayXmlResourceValue)) {
      return false;
    }
    ArrayXmlResourceValue other = (ArrayXmlResourceValue) obj;
    return Objects.equals(visibility, other.visibility)
        // TODO(b/112848607): include the "array" proto in comparison; right now it's redundant.
        && Objects.equals(arrayType, other.arrayType)
        && Objects.equals(values, other.values)
        && Objects.equals(attributes, other.attributes);
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(getClass())
        .add("arrayType", arrayType)
        .add("values", values)
        .add("attributes", attributes)
        .toString();
  }

  @Override
  public XmlResourceValue combineWith(XmlResourceValue value) {
    throw new IllegalArgumentException(this + " is not a combinable resource.");
  }

  @Override
  public int compareMergePriorityTo(XmlResourceValue value) {
    return 0;
  }

  @Override
  public void writeResourceToClass(
      DependencyInfo dependencyInfo, FullyQualifiedName key, AndroidResourceSymbolSink sink) {
    sink.acceptSimpleResource(dependencyInfo, visibility, key.type(), key.name());
  }

  public static XmlResourceValue parseArray(
      XMLEventReader eventReader, StartElement start, Namespaces.Collector namespacesCollector)
      throws XMLStreamException {
    List<String> values = new ArrayList<>();
    for (XMLEvent element = XmlResourceValues.nextTag(eventReader);
        !XmlResourceValues.isEndTag(element, start.getName());
        element = XmlResourceValues.nextTag(eventReader)) {
      if (XmlResourceValues.isItem(element)) {
        if (!element.isStartElement()) {
          throw new XMLStreamException(
              String.format("Expected start element %s", element), element.getLocation());
        }
        String contents =
            XmlResourceValues.readContentsAsString(
                eventReader, element.asStartElement().getName(), namespacesCollector);
        values.add(contents != null ? contents : "");
      }
    }
    try {
      return of(
          ArrayType.fromTagName(start),
          values,
          ImmutableMap.copyOf(XmlResourceValues.parseTagAttributes(start)));
    } catch (IllegalArgumentException e) {
      throw new XMLStreamException(e.getMessage(), start.getLocation());
    }
  }

  @Override
  public String asConflictStringWith(DataSource source) {
    return source.asConflictString();
  }

  @Override
  public Visibility getVisibility() {
    return visibility;
  }

  @Override
  public ImmutableList<Reference> getReferencedResources() {
    return array.getElementList().stream()
        .filter(element -> element.getItem().hasRef())
        .map(element -> element.getItem().getRef())
        .collect(ImmutableList.toImmutableList());
  }
}
