/*******************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 org.apache.olingo.odata2.client.core.ep.deserializer;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.apache.olingo.odata2.api.edm.Edm;
import org.apache.olingo.odata2.api.edm.EdmLiteralKind;
import org.apache.olingo.odata2.api.edm.EdmSimpleTypeException;
import org.apache.olingo.odata2.api.ep.EntityProviderException;
import org.apache.olingo.odata2.api.ep.entry.DeletedEntryMetadata;
import org.apache.olingo.odata2.api.ep.entry.ODataEntry;
import org.apache.olingo.odata2.api.ep.feed.ODataDeltaFeed;
import org.apache.olingo.odata2.client.api.ep.DeserializerProperties;
import org.apache.olingo.odata2.core.edm.EdmDateTimeOffset;
import org.apache.olingo.odata2.core.ep.aggregator.EntityInfoAggregator;
import org.apache.olingo.odata2.core.ep.entry.DeletedEntryMetadataImpl;
import org.apache.olingo.odata2.core.ep.feed.FeedMetadataImpl;
import org.apache.olingo.odata2.core.ep.feed.ODataDeltaFeedImpl;
import org.apache.olingo.odata2.core.ep.util.FormatXml;

/**
 * Atom/XML format reader/consumer for feeds.
 * 
 * {@link XmlFeedDeserializer} instance use
 * {@link XmlEntryDeserializer#readEntry(XMLStreamReader, EntityInfoAggregator, EntityProviderReadProperties)} for
 * read/consume of several entries.
 * 
 * 
 */
public class XmlFeedDeserializer {

  /**
   * 
   * @param reader
   * @param eia
   * @param readProperties
   * @return {@link ODataDeltaFeed} object
   * @throws EntityProviderException
   */
  public ODataDeltaFeed readFeed(final XMLStreamReader reader, final EntityInfoAggregator eia,
      final DeserializerProperties readProperties) throws EntityProviderException {
    try {
      // read xml tag
      reader.require(XMLStreamConstants.START_DOCUMENT, null, null);
      reader.nextTag();

      // read feed tag
      reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_FEED);
      Map<String, String> foundPrefix2NamespaceUri = extractNamespacesFromTag(reader);
      foundPrefix2NamespaceUri.putAll(readProperties.getValidatedPrefixNamespaceUris());
      checkAllMandatoryNamespacesAvailable(foundPrefix2NamespaceUri);
      DeserializerProperties entryReadProperties =
          DeserializerProperties.initFrom(readProperties)
          .addValidatedPrefixes(foundPrefix2NamespaceUri).build();

      // read feed data (metadata and entries)
      return readFeedData(reader, eia, entryReadProperties);
    } catch (XMLStreamException e) {
      throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass()
          .getSimpleName()), e);
    }
  }

  /**
   * Read all feed specific data (like <code>inline count</code> and <code>next link</code>) as well as all feed entries
   * (<code>entry</code>) and delta feed extensions (tombstones).
   * 
   * @param reader xml stream reader with xml content to be read
   * @param eia entity infos for validation and mapping
   * @param entryReadProperties properties which are used for read of feed.
   * @return all feed specific data (like <code>inline count</code> and <code>next link</code>) as well as all feed
   * entries (<code>entry</code>).
   * @throws XMLStreamException if malformed xml is read in stream
   * @throws EntityProviderException if xml contains invalid data (based on odata specification and edm definition)
   */
  private ODataDeltaFeed readFeedData(final XMLStreamReader reader, final EntityInfoAggregator eia,
      final DeserializerProperties entryReadProperties) throws XMLStreamException, EntityProviderException {
    FeedMetadataImpl metadata = new FeedMetadataImpl();
    XmlEntryDeserializer xec = new XmlEntryDeserializer();
    List<ODataEntry> results = new ArrayList<ODataEntry>();
    List<DeletedEntryMetadata> deletedEntries = new ArrayList<DeletedEntryMetadata>();

    while (reader.hasNext() && !isFeedEndTag(reader)) {
      if (FormatXml.ATOM_ENTRY.equals(reader.getLocalName())) {
        ODataEntry entry = xec.readEntry(reader, eia, entryReadProperties, true);
        results.add(entry);
      } else if (FormatXml.ATOM_TOMBSTONE_DELETED_ENTRY.equals(reader.getLocalName())) {
        reader.require(XMLStreamConstants.START_ELEMENT, FormatXml.ATOM_TOMBSTONE_NAMESPACE,
            FormatXml.ATOM_TOMBSTONE_DELETED_ENTRY);

        DeletedEntryMetadataImpl deletedEntryMetadata = readDeletedEntryMetadata(reader);
        deletedEntries.add(deletedEntryMetadata);
        reader.next();
      } else if (FormatXml.M_COUNT.equals(reader.getLocalName())) {
        reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_M_2007_08, FormatXml.M_COUNT);
        readInlineCount(reader, metadata);
      } else if (FormatXml.ATOM_LINK.equals(reader.getLocalName())) {
        reader.require(XMLStreamConstants.START_ELEMENT, Edm.NAMESPACE_ATOM_2005, FormatXml.ATOM_LINK);

        final String rel = reader.getAttributeValue(null, FormatXml.ATOM_REL);
        if (FormatXml.ATOM_NEXT_LINK.equals(rel)) {
          final String uri = reader.getAttributeValue(null, FormatXml.ATOM_HREF);
          metadata.setNextLink(uri);
        } else if (FormatXml.ATOM_DELTA_LINK.equals(rel)) {
          final String uri = reader.getAttributeValue(null, FormatXml.ATOM_HREF);
          metadata.setDeltaLink(uri);
        }
        reader.next();
      } else {
        reader.next();
      }
      readTillNextStartTag(reader);
    }
    return new ODataDeltaFeedImpl(results, metadata, deletedEntries);
  }

  private DeletedEntryMetadataImpl readDeletedEntryMetadata(final XMLStreamReader reader)
      throws EntityProviderException {
    try {
      DeletedEntryMetadataImpl deletedEntryMetadata = new DeletedEntryMetadataImpl();

      String uri = reader.getAttributeValue(null, FormatXml.ATOM_TOMBSTONE_REF);
      String whenStr = reader.getAttributeValue(null, FormatXml.ATOM_TOMBSTONE_WHEN);
      Date when;
      when = EdmDateTimeOffset.getInstance().valueOfString(whenStr, EdmLiteralKind.DEFAULT, null,
          Date.class);

      deletedEntryMetadata.setUri(uri);
      deletedEntryMetadata.setWhen(when);
      return deletedEntryMetadata;
    } catch (EdmSimpleTypeException e) {
      throw new EntityProviderException(EntityProviderException.INVALID_DELETED_ENTRY_METADATA, e);
    }
  }

  private void readInlineCount(final XMLStreamReader reader, final FeedMetadataImpl metadata)
      throws XMLStreamException,
      EntityProviderException {
    String inlineCountString = reader.getElementText();
    if (inlineCountString != null) {
      try {
        int inlineCountNumber = Integer.parseInt(inlineCountString);
        if (inlineCountNumber >= 0) {
          metadata.setInlineCount(inlineCountNumber);
        } else {
          throw new EntityProviderException(EntityProviderException.INLINECOUNT_INVALID
              .addContent(inlineCountNumber));
        }
      } catch (NumberFormatException e) {
        throw new EntityProviderException(EntityProviderException.INLINECOUNT_INVALID.addContent(""), e);
      }
    }
  }

  private void readTillNextStartTag(final XMLStreamReader reader) throws XMLStreamException {
    while (reader.hasNext() && !reader.isStartElement()) {
      reader.next();
    }
  }

  private boolean isFeedEndTag(final XMLStreamReader reader) {
    return reader.isEndElement()
        && Edm.NAMESPACE_ATOM_2005.equals(reader.getNamespaceURI())
        && FormatXml.ATOM_FEED.equals(reader.getLocalName());
  }

  /**
   * Maps all all found namespaces of current xml tag into a map.
   * 
   * @param reader xml reader with current position at a xml tag
   * @return map with all found namespaces of current xml tag
   */
  private Map<String, String> extractNamespacesFromTag(final XMLStreamReader reader) {
    // collect namespaces
    Map<String, String> foundPrefix2NamespaceUri = new HashMap<String, String>();
    int namespaceCount = reader.getNamespaceCount();
    for (int i = 0; i < namespaceCount; i++) {
      String namespacePrefix = reader.getNamespacePrefix(i);
      String namespaceUri = reader.getNamespaceURI(i);

      foundPrefix2NamespaceUri.put(namespacePrefix, namespaceUri);
    }
    return foundPrefix2NamespaceUri;
  }

  /**
   * 
   * @param foundPrefix2NamespaceUri
   * @throws EntityProviderException
   */
  private void checkAllMandatoryNamespacesAvailable(final Map<String, String> foundPrefix2NamespaceUri)
      throws EntityProviderException {
    if (!foundPrefix2NamespaceUri.containsValue(Edm.NAMESPACE_D_2007_08)) {
      throw new EntityProviderException(EntityProviderException.INVALID_NAMESPACE.addContent(Edm.NAMESPACE_D_2007_08));
    } else if (!foundPrefix2NamespaceUri.containsValue(Edm.NAMESPACE_M_2007_08)) {
      throw new EntityProviderException(EntityProviderException.INVALID_NAMESPACE.addContent(Edm.NAMESPACE_M_2007_08));
    } else if (!foundPrefix2NamespaceUri.containsValue(Edm.NAMESPACE_ATOM_2005)) {
      throw new EntityProviderException(EntityProviderException.INVALID_NAMESPACE.addContent(Edm.NAMESPACE_ATOM_2005));
    }
  }
}
