/*******************************************************************************
 * 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.core.ep.consumer;

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.olingo.odata2.api.edm.Edm;
import org.apache.olingo.odata2.api.edm.EdmEntitySet;
import org.apache.olingo.odata2.api.edm.EdmException;
import org.apache.olingo.odata2.api.edm.EdmLiteralKind;
import org.apache.olingo.odata2.api.edm.EdmMultiplicity;
import org.apache.olingo.odata2.api.edm.EdmNavigationProperty;
import org.apache.olingo.odata2.api.edm.EdmSimpleTypeException;
import org.apache.olingo.odata2.api.ep.EntityProviderException;
import org.apache.olingo.odata2.api.ep.EntityProviderReadProperties;
import org.apache.olingo.odata2.api.ep.callback.OnReadInlineContent;
import org.apache.olingo.odata2.api.ep.callback.ReadEntryResult;
import org.apache.olingo.odata2.api.ep.callback.ReadFeedResult;
import org.apache.olingo.odata2.api.ep.entry.ODataEntry;
import org.apache.olingo.odata2.api.ep.feed.ODataFeed;
import org.apache.olingo.odata2.api.exception.ODataApplicationException;
import org.apache.olingo.odata2.core.edm.EdmDateTimeOffset;
import org.apache.olingo.odata2.core.ep.aggregator.EntityInfoAggregator;
import org.apache.olingo.odata2.core.ep.aggregator.EntityPropertyInfo;
import org.apache.olingo.odata2.core.ep.aggregator.NavigationPropertyInfo;
import org.apache.olingo.odata2.core.ep.entry.DeletedEntryMetadataImpl;
import org.apache.olingo.odata2.core.ep.entry.EntryMetadataImpl;
import org.apache.olingo.odata2.core.ep.entry.MediaMetadataImpl;
import org.apache.olingo.odata2.core.ep.entry.ODataEntryImpl;
import org.apache.olingo.odata2.core.ep.feed.JsonFeedEntry;
import org.apache.olingo.odata2.core.ep.util.FormatJson;
import org.apache.olingo.odata2.core.uri.ExpandSelectTreeNodeImpl;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;

/**
 *  
 */
public class JsonEntryConsumer {

  private final Map<String, Object> typeMappings;
  private final EntityInfoAggregator eia;
  private final JsonReader reader;
  private final EntityProviderReadProperties readProperties;

  private ODataEntryImpl resultEntry;
  private Map<String, Object> properties;
  private MediaMetadataImpl mediaMetadata;
  private EntryMetadataImpl entryMetadata;
  private ExpandSelectTreeNodeImpl expandSelectTree;

  private DeletedEntryMetadataImpl resultDeletedEntry;

  public JsonEntryConsumer(final JsonReader reader, final EntityInfoAggregator eia,
      final EntityProviderReadProperties readProperties) {
    typeMappings = readProperties.getTypeMappings();
    this.eia = eia;
    this.readProperties = readProperties;
    this.reader = reader;
  }

  public ODataEntry readSingleEntry() throws EntityProviderException {
    try {
      reader.beginObject();
      String nextName = reader.nextName();
      if (FormatJson.D.equals(nextName)) {
        reader.beginObject();
        readEntryContent();
        reader.endObject();
      } else {
        handleName(nextName);
        readEntryContent();
      }
      reader.endObject();

      if (reader.peek() != JsonToken.END_DOCUMENT) {
        throw new EntityProviderException(EntityProviderException.END_DOCUMENT_EXPECTED.addContent(reader.peek()
            .toString()));
      }
    } catch (IOException e) {
      throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass()
          .getSimpleName()), e);
    } catch (EdmException e) {
      throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass()
          .getSimpleName()), e);
    } catch (IllegalStateException e) {
      throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass()
          .getSimpleName()), e);
    }

    return resultEntry;
  }

  public JsonFeedEntry readFeedEntry() throws EdmException, EntityProviderException, IOException {
    reader.beginObject();
    readEntryContent();
    reader.endObject();

    if (resultDeletedEntry == null) {
      return new JsonFeedEntry(resultEntry);
    } else {
      return new JsonFeedEntry(resultDeletedEntry);
    }
  }

  private void readEntryContent() throws IOException, EdmException, EntityProviderException {
    while (reader.hasNext()) {
      final String name = reader.nextName();
      handleName(name);
    }
  }

  /**
   * Ensure that instance field {@link #resultEntry} exists.
   * If it not already exists create an instance (as well as all other necessary objects like: {@link #properties},
   * {@link #mediaMetadata}, {@link #entryMetadata}, {@link #expandSelectTree}).
   */
  private void ensureODataEntryExists() {
    if (resultEntry == null) {
      properties = new HashMap<String, Object>();
      mediaMetadata = new MediaMetadataImpl();
      entryMetadata = new EntryMetadataImpl();
      expandSelectTree = new ExpandSelectTreeNodeImpl();

      resultEntry = new ODataEntryImpl(properties, mediaMetadata, entryMetadata, expandSelectTree);
    }
  }

  /**
   * Ensure that instance field {@link #resultDeletedEntry} exists.
   * If it not already exists create an instance.
   */
  private void ensureDeletedEntryMetadataExists() {
    if (resultDeletedEntry == null) {
      resultDeletedEntry = new DeletedEntryMetadataImpl();
    }
  }

  private void handleName(final String name) throws IOException, EdmException, EntityProviderException {
    if (FormatJson.METADATA.equals(name)) {
      ensureODataEntryExists();
      readMetadata();
      validateMetadata();
    } else if (FormatJson.ODATA_CONTEXT.equals(name)) {
      readODataContext();
    } else {
      ensureODataEntryExists();
      EntityPropertyInfo propertyInfo = eia.getPropertyInfo(name);
      if (propertyInfo != null) {
        Object propertyValue = new JsonPropertyConsumer()
            .readPropertyValue(reader, propertyInfo, typeMappings.get(name), readProperties);
        if (properties.containsKey(name)) {
          throw new EntityProviderException(EntityProviderException.DOUBLE_PROPERTY.addContent(name));
        }
        properties.put(name, propertyValue);
      } else {
        readNavigationProperty(name);
      }
    }
  }

  private void readODataContext() throws IOException, EntityProviderException {
    String contextValue = reader.nextString();
    if (contextValue == null) {
      throw new EntityProviderException(EntityProviderException.MISSING_ATTRIBUTE.addContent(FormatJson.ODATA_CONTEXT)
          .addContent(FormatJson.RESULTS));
    }

    if (contextValue.startsWith(FormatJson.DELTA_CONTEXT_PREFIX)
        && contextValue.endsWith(FormatJson.DELTA_CONTEXT_POSTFIX)) {
      while (reader.hasNext()) {
        ensureDeletedEntryMetadataExists();
        String name = reader.nextName();
        String value = reader.nextString();
        if (FormatJson.ID.equals(name)) {
          resultDeletedEntry.setUri(value);
        } else if (FormatJson.DELTA_WHEN.equals(name)) {
          Date when = parseWhen(value);
          resultDeletedEntry.setWhen(when);
        }
      }
    }
  }

  private Date parseWhen(final String value) throws EntityProviderException {
    try {
      return EdmDateTimeOffset.getInstance().valueOfString(value, EdmLiteralKind.JSON, null, Date.class);
    } catch (EdmSimpleTypeException e) {
      throw new EntityProviderException(EntityProviderException.INVALID_DELETED_ENTRY_METADATA
          .addContent("Unparsable format for when field value."));
    }
  }

  private void readMetadata() throws IOException, EdmException, EntityProviderException {
    String name = null;
    String value = null;
    reader.beginObject();
    while (reader.hasNext()) {
      name = reader.nextName();

      if (FormatJson.PROPERTIES.equals(name)) {
        reader.skipValue();
        continue;
      }

      value = reader.nextString();
      if (FormatJson.ID.equals(name)) {
        entryMetadata.setId(value);
      } else if (FormatJson.URI.equals(name)) {
        entryMetadata.setUri(value);
      } else if (FormatJson.TYPE.equals(name)) {
        String fullQualifiedName = eia.getEntityType().getNamespace() + Edm.DELIMITER + eia.getEntityType().getName();
        if (!fullQualifiedName.equals(value)) {
          throw new EntityProviderException(EntityProviderException.INVALID_ENTITYTYPE.addContent(fullQualifiedName)
              .addContent(value));
        }
      } else if (FormatJson.ETAG.equals(name)) {
        entryMetadata.setEtag(value);
      } else if (FormatJson.EDIT_MEDIA.equals(name)) {
        mediaMetadata.setEditLink(value);
      } else if (FormatJson.MEDIA_SRC.equals(name)) {
        mediaMetadata.setSourceLink(value);
      } else if (FormatJson.MEDIA_ETAG.equals(name)) {
        mediaMetadata.setEtag(value);
      } else if (FormatJson.CONTENT_TYPE.equals(name)) {
        mediaMetadata.setContentType(value);
      } else {
        throw new EntityProviderException(EntityProviderException.INVALID_CONTENT.addContent(name).addContent(
            FormatJson.METADATA));
      }
    }

    reader.endObject();
  }

  private void validateMetadata() throws EdmException, EntityProviderException {
    if (eia.getEntityType().hasStream()) {
      if (mediaMetadata.getSourceLink() == null) {
        throw new EntityProviderException(EntityProviderException.MISSING_ATTRIBUTE.addContent(FormatJson.MEDIA_SRC)
            .addContent(FormatJson.METADATA));
      }
      if (mediaMetadata.getContentType() == null) {
        throw new EntityProviderException(EntityProviderException.MISSING_ATTRIBUTE.addContent(FormatJson.CONTENT_TYPE)
            .addContent(FormatJson.METADATA));
      }
    } else {
      if (mediaMetadata.getContentType() != null || mediaMetadata.getEditLink() != null
          || mediaMetadata.getEtag() != null || mediaMetadata.getSourceLink() != null) {
        throw new EntityProviderException(EntityProviderException.MEDIA_DATA_NOT_INITIAL);
      }
    }
  }

  private void readNavigationProperty(final String navigationPropertyName) throws IOException, EntityProviderException,
      EdmException {
    NavigationPropertyInfo navigationPropertyInfo = eia.getNavigationPropertyInfo(navigationPropertyName);
    if (navigationPropertyInfo == null) {
      throw new EntityProviderException(EntityProviderException.ILLEGAL_ARGUMENT.addContent(navigationPropertyName));
    }

    JsonToken peek = reader.peek();
    if (peek == JsonToken.BEGIN_OBJECT) {
      reader.beginObject();
      String name = reader.nextName();
      if (FormatJson.DEFERRED.equals(name)) {
        reader.beginObject();
        String uri = reader.nextName();
        if (FormatJson.URI.equals(uri)) {
          String value = reader.nextString();
          entryMetadata.putAssociationUri(navigationPropertyInfo.getName(), value);
        } else {
          throw new EntityProviderException(EntityProviderException.ILLEGAL_ARGUMENT.addContent(uri));
        }
        reader.endObject();
      } else {
        EdmNavigationProperty navigationProperty =
            (EdmNavigationProperty) eia.getEntityType().getProperty(navigationPropertyName);
        EdmEntitySet inlineEntitySet = eia.getEntitySet().getRelatedEntitySet(navigationProperty);
        EntityInfoAggregator inlineEia = EntityInfoAggregator.create(inlineEntitySet);
        EntityProviderReadProperties inlineReadProperties;
        OnReadInlineContent callback = readProperties.getCallback();
        try {
          if (callback == null) {
            inlineReadProperties =
                EntityProviderReadProperties.init()
                    .mergeSemantic(readProperties.getMergeSemantic())
                    .isValidatingFacets(readProperties.isValidatingFacets())
                    .build();

          } else {
            inlineReadProperties = callback.receiveReadProperties(readProperties, navigationProperty);
          }

          if (navigationProperty.getMultiplicity() == EdmMultiplicity.MANY) {
            JsonFeedConsumer inlineConsumer = new JsonFeedConsumer(reader, inlineEia, inlineReadProperties);
            ODataFeed feed = inlineConsumer.readStartedInlineFeed(name);
            updateExpandSelectTree(navigationPropertyName, feed);
            if (callback == null) {
              properties.put(navigationPropertyName, feed);
              resultEntry.setContainsInlineEntry(true);
            } else {
              ReadFeedResult result = new ReadFeedResult(inlineReadProperties, navigationProperty, 
                  feed, entryMetadata.getId() != null ? entryMetadata.getId() : fetchParentIdInfo(eia, resultEntry));
              callback.handleReadFeed(result);
            }
          } else {
            JsonEntryConsumer inlineConsumer = new JsonEntryConsumer(reader, inlineEia, inlineReadProperties);
            ODataEntry entry = inlineConsumer.readInlineEntry(name);
            updateExpandSelectTree(navigationPropertyName, entry);
            if (callback == null) {
              properties.put(navigationPropertyName, entry);
              resultEntry.setContainsInlineEntry(true);
            } else {
              ReadEntryResult result = new ReadEntryResult(inlineReadProperties, navigationProperty, 
                  entry, entryMetadata.getId() != null ? entryMetadata.getId() : fetchParentIdInfo(eia, resultEntry));
              callback.handleReadEntry(result);
            }
          }

        } catch (ODataApplicationException e) {
          throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass()
              .getSimpleName()), e);
        }
      }
      reader.endObject();
    } else if (peek == JsonToken.NULL) {
      reader.nextNull();
    } else {
      final EdmNavigationProperty navigationProperty =
          (EdmNavigationProperty) eia.getEntityType().getProperty(navigationPropertyName);
      final EdmEntitySet inlineEntitySet = eia.getEntitySet().getRelatedEntitySet(navigationProperty);
      final EntityInfoAggregator inlineInfo = EntityInfoAggregator.create(inlineEntitySet);
      OnReadInlineContent callback = readProperties.getCallback();
      EntityProviderReadProperties inlineReadProperties;
      if (callback == null) {
        inlineReadProperties =
            EntityProviderReadProperties.init()
                .mergeSemantic(readProperties.getMergeSemantic())
                .isValidatingFacets(readProperties.isValidatingFacets())
                .build();
      } else {
        try {
          inlineReadProperties = callback.receiveReadProperties(readProperties, navigationProperty);
        } catch (final ODataApplicationException e) {
          throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass()
              .getSimpleName()), e);
        }
      }
      ODataFeed feed = new JsonFeedConsumer(reader, inlineInfo, inlineReadProperties).readInlineFeedStandalone();
      updateExpandSelectTree(navigationPropertyName, feed);
      if (callback == null) {
        properties.put(navigationPropertyName, feed);
        resultEntry.setContainsInlineEntry(true);
      } else {
        ReadFeedResult result = new ReadFeedResult(inlineReadProperties, navigationProperty,
            feed, entryMetadata.getId() != null ? entryMetadata.getId() : fetchParentIdInfo(eia, resultEntry));
        try {
          callback.handleReadFeed(result);
        } catch (final ODataApplicationException e) {
          throw new EntityProviderException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass()
              .getSimpleName()), e);
        }
      }
    }
  }

  /**
   * 
   * @param eia
   * @param resultEntry
   * @return
   * @throws EdmException
   */
  private String fetchParentIdInfo(EntityInfoAggregator eia, ODataEntryImpl resultEntry) throws EdmException {
    String keyInfo = "";
    if (null != resultEntry) {
      if (eia != null && eia.getEntityType() != null) {
        List<String> keys = eia.getEntityType().getKeyPropertyNames();
        int i = 0;
        for (String key : keys) {
          keyInfo += key + "=" + resultEntry.getProperties().get(key);
          i++;
          if (i < keys.size()) {
            keyInfo += ",";
          }
        }
      }
    }
    return keyInfo;
  }
  
  private void updateExpandSelectTree(final String navigationPropertyName, final ODataFeed feed) {
    List<ODataEntry> entries = feed.getEntries();
    if (!entries.isEmpty()) {
      updateExpandSelectTree(navigationPropertyName, entries.get(0));
      for(ODataEntry entry : entries){
        ExpandSelectTreeNodeImpl newExpandedSelectedTree = new ExpandSelectTreeNodeImpl();
        newExpandedSelectedTree.setExpanded();
        newExpandedSelectedTree.setExplicitlySelected();
        newExpandedSelectedTree.putLink(navigationPropertyName, 
            (ExpandSelectTreeNodeImpl) entry.getExpandSelectTree());
        expandSelectTree.getExpandedList().add(newExpandedSelectedTree);
      }
    } else {
      expandSelectTree.setExpanded();
      expandSelectTree.setExplicitlySelected();
      expandSelectTree.putLink(navigationPropertyName, new ExpandSelectTreeNodeImpl());
    }
  }

  private void updateExpandSelectTree(final String navigationPropertyName, final ODataEntry entry) {
    expandSelectTree.setExpanded();
    expandSelectTree.setExplicitlySelected();
    expandSelectTree.putLink(navigationPropertyName, (ExpandSelectTreeNodeImpl) entry.getExpandSelectTree());
  }

  private ODataEntry readInlineEntry(final String name) throws EdmException, EntityProviderException, IOException {
    // consume the already started content
    handleName(name);
    // consume the rest of the entry content
    readEntryContent();
    return resultEntry;
  }

}
