// 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.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSortedMap;
import com.google.devtools.build.android.DataResourceXml;
import com.google.devtools.build.android.XmlResourceValue;
import com.google.devtools.build.android.XmlResourceValues;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.logging.Logger;
import javax.xml.namespace.QName;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.Namespace;
import javax.xml.stream.events.StartElement;

/**
 * Represents a collection of xml namespaces.
 *
 * <p>Each &lt;resources&gt; can have xmlns declarations. Since the merging process generates the
 * resources tag to combining multiple {@link DataResourceXml}s, the Namespaces must be tracked and
 * kept with each value.
 */
public class Namespaces implements Iterable<Map.Entry<String, String>> {
  private static final Logger logger = Logger.getLogger(Namespaces.class.getCanonicalName());
  private static final Namespaces EMPTY_INSTANCE =
      new Namespaces(ImmutableSortedMap.<String, String>of());

  /** Collects prefix and uri pairs from elements. */
  public static class Collector {
    private Map<String, String> prefixToUri = new LinkedHashMap<>();

    public Namespaces toNamespaces() {
      return Namespaces.from(prefixToUri);
    }

    /**
     * Collects all the prefix and uri pairs from a start element.
     *
     * <p>Since {@link Namespaces} represents top level declarations, collectFrom ignores any prefix
     * that is declared on the element. Those will be handled by the {@link XmlResourceValue}
     * individually.
     *
     * @param start The element to collect prefix and uris from.
     * @return The current namespace builder.
     */
    @CanIgnoreReturnValue
    public Collector collectFrom(StartElement start) {
      Iterator<Attribute> attributes = XmlResourceValues.iterateAttributesFrom(start);
      Iterator<Namespace> localNamespaces = XmlResourceValues.iterateNamespacesFrom(start);
      // Collect the local prefixes to make sure a prefix isn't declared locally.
      Set<String> prefixes = new LinkedHashSet<>();
      while (localNamespaces.hasNext()) {
        prefixes.add(localNamespaces.next().getPrefix());
      }
      collectFrom(start.getName(), prefixes);
      while (attributes.hasNext()) {
        collectFrom(attributes.next().getName(), prefixes);
      }
      return this;
    }

    void collectFrom(QName name, Set<String> localPrefixes) {
      String prefix = name.getPrefix();
      if (!prefix.isEmpty() && !localPrefixes.contains(prefix)) {
        // If the prefix exists and is not a locally declared prefix, add the prefix and uri.
        prefixToUri.put(prefix, name.getNamespaceURI());
      }
    }
  }

  public static Collector collector() {
    return new Collector();
  }

  public static Namespaces from(Map<String, String> prefixToUri) {
    if (prefixToUri.isEmpty()) {
      return empty();
    }
    return new Namespaces(ImmutableSortedMap.copyOf(prefixToUri));
  }

  /**
   * Create a {@link Namespaces} containing the singular namespace used by the name, or an empty
   * one.
   */
  public static Namespaces from(QName name) {
    if (name.getPrefix().isEmpty()) {
      return empty();
    }
    return new Namespaces(ImmutableSortedMap.of(name.getPrefix(), name.getNamespaceURI()));
  }

  public static Namespaces empty() {
    return EMPTY_INSTANCE;
  }

  // Keep the prefixes in a sorted map so that when this object is iterated over, the order is
  // deterministic.
  private final ImmutableSortedMap<String, String> prefixToUri;

  private Namespaces(ImmutableSortedMap<String, String> prefixToUri) {
    this.prefixToUri = prefixToUri;
  }

  /** Combines two {@link Namespaces} into a new instance and returns it. */
  public Namespaces union(Namespaces other) {
    // No prefixes to add, return the other.
    if (prefixToUri.isEmpty()) {
      return other;
    }
    // TODO(corysmith): Issue error when prefixes are mapped to different uris.
    // Keeping behavior for backwards compatibility.
    Map<String, String> combinedNamespaces = new LinkedHashMap<>();
    combinedNamespaces.putAll(other.prefixToUri);
    for (Map.Entry<String, String> namespace : prefixToUri.entrySet()) {
      String prefix = namespace.getKey();
      String namespaceUri = namespace.getValue();
      if (combinedNamespaces.containsKey(prefix)
          && !combinedNamespaces.get(prefix).equals(namespaceUri)) {
        logger.warning(
            String.format(
                "%s has multiple namespaces: %s and %s. Using %s."
                    + " This will be an error in the future.",
                prefix, namespaceUri, combinedNamespaces.get(prefix), namespaceUri));
      }
      combinedNamespaces.put(prefix, namespaceUri);
    }
    return Namespaces.from(combinedNamespaces);
  }

  @Override
  public Iterator<Map.Entry<String, String>> iterator() {
    return prefixToUri.entrySet().iterator();
  }

  public Map<String, String> asMap() {
    return prefixToUri;
  }

  @Override
  public int hashCode() {
    return prefixToUri.hashCode();
  }

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

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof Namespaces) {
      Namespaces other = (Namespaces) obj;
      return Objects.equals(prefixToUri, other.prefixToUri);
    }
    return false;
  }
}
