/*******************************************************************************
 * 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.serializer;

import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.olingo.odata2.api.edm.Edm;
import org.apache.olingo.odata2.api.edm.EdmEntitySet;
import org.apache.olingo.odata2.api.edm.EdmEntityType;
import org.apache.olingo.odata2.api.edm.EdmException;
import org.apache.olingo.odata2.api.edm.EdmMapping;
import org.apache.olingo.odata2.api.edm.EdmMultiplicity;
import org.apache.olingo.odata2.api.edm.EdmNavigationProperty;
import org.apache.olingo.odata2.api.ep.EntityProviderException;
import org.apache.olingo.odata2.client.api.ep.Entity;
import org.apache.olingo.odata2.client.api.ep.EntityCollection;
import org.apache.olingo.odata2.client.api.ep.EntityCollectionSerializerProperties;
import org.apache.olingo.odata2.client.api.ep.EntitySerializerProperties;
import org.apache.olingo.odata2.core.commons.ContentType;
import org.apache.olingo.odata2.core.ep.EntityProviderProducerException;
import org.apache.olingo.odata2.core.ep.aggregator.EntityInfoAggregator;
import org.apache.olingo.odata2.core.ep.util.FormatJson;
import org.apache.olingo.odata2.core.ep.util.JsonStreamWriter;

/**
 * Producer for writing an entity in JSON, also usable for function imports
 * returning a single instance of an entity type.
 * 
 */
public class JsonEntryEntitySerializer {

  private final EntitySerializerProperties properties;
  private String location;
  private String idlocation;
  private JsonStreamWriter jsonStreamWriter;
  private static final String VALUE = "/$value";

  /**
   * 
   * @param properties
   * @throws EntityProviderException
   */
  public JsonEntryEntitySerializer(final EntitySerializerProperties properties) {
    this.properties = properties == null ? EntitySerializerProperties.serviceRoot(null).build() : properties;
  }

  /**
   * This serializes the json payload entry
   * @param writer
   * @param entityInfo
   * @param data
   * @param isRootElement
   * @throws EntityProviderException
   */
  public void append(final Writer writer, final EntityInfoAggregator entityInfo, final Entity data) 
      throws EntityProviderException {
   if (data == null) {
     throw new EntityProviderException(EntityProviderException.NULL_VALUE);
   }
    final EdmEntityType type = entityInfo.getEntityType();

    try {
      jsonStreamWriter = new JsonStreamWriter(writer);
     
      jsonStreamWriter.beginObject();

      boolean containsMetadata = false;
      if (properties.isIncludeMetadata()) {
        writeMetadata(entityInfo, data.getProperties(), type);
        containsMetadata = true;
      } 
      writeProperties(entityInfo, data.getProperties(), type, containsMetadata);

      writeNavigationProperties(writer, entityInfo, data.getNavigations(), type, data.getProperties().isEmpty());
      jsonStreamWriter.endObject();
      
      writer.flush();

    } catch (final IOException e) {
      throw new EntityProviderProducerException(EntityProviderException.EXCEPTION_OCCURRED.addContent(e.getClass()
          .getSimpleName()), e);
    } catch (final EdmException e) {
      throw new EntityProviderProducerException(e.getMessageReference(), e);
    }
  }

  private void writeNavigationProperties(final Writer writer, final EntityInfoAggregator entityInfo,
      final Map<String, Object> data,
      final EdmEntityType type, boolean emptyData) throws EdmException, EntityProviderException, IOException {
    for (final String navigationPropertyName : type.getNavigationPropertyNames()) {
      if (data.containsKey(navigationPropertyName)) {
        if (data.get(navigationPropertyName) == null) {
          throw new EntityProviderException(EntityProviderException.NULL_VALUE);
        }
        if (data.get(navigationPropertyName) instanceof Entity || 
            data.get(navigationPropertyName) instanceof EntityCollection) {
          if( !emptyData){
            jsonStreamWriter.separator();
          }
          emptyData=false;
          jsonStreamWriter.name(navigationPropertyName);
          writeExpandedNavigationProperty(writer, entityInfo, data, type, navigationPropertyName);
        } else if (data.get(navigationPropertyName) instanceof Map<?,?>){
          writeNavigationLinks(entityInfo, data);
        } else {
          throw new EntityProviderException(EntityProviderException.INCORRECT_NAVIGATION_TYPE);
        }
      }
    }
  }

  private void writeExpandedNavigationProperty(final Writer writer, final EntityInfoAggregator entityInfo,
      final Map<String, Object> data,
      final EdmEntityType type, final String navigationPropertyName) throws EdmException, EntityProviderException,
      IOException {
    
    final EdmNavigationProperty navigationProperty = (EdmNavigationProperty) type.getProperty(navigationPropertyName);
    final boolean isFeed = navigationProperty.getMultiplicity() == EdmMultiplicity.MANY;
    final EdmEntitySet entitySet = entityInfo.getEntitySet();
    final EdmEntitySet inlineEntitySet = entitySet.getRelatedEntitySet(navigationProperty);

    if (isFeed) {
      EntityCollection inlineData = (EntityCollection) data.get(navigationProperty.getName());
      
      if(inlineData == null){
        throw new EntityProviderException(EntityProviderException.NULL_VALUE);
      }
      final EntityCollectionSerializerProperties inlineProperties = inlineData.getCollectionProperties() == null ? 
          EntityCollectionSerializerProperties.
          serviceRoot(properties.getServiceRoot()).build() : 
            inlineData.getCollectionProperties();
      JsonFeedEntitySerializer jsonFeedEntityProducer = new JsonFeedEntitySerializer(inlineProperties);
      final EntityInfoAggregator inlineEntityInfo =
          EntityInfoAggregator.create(inlineEntitySet, null);
      jsonFeedEntityProducer.appendAsArray(writer, inlineEntityInfo, inlineData);
    } else {
      Entity inlineData = (Entity) data.get(navigationProperty.getName());
    //This statement is used for the client use case. Flag should never be set on server side
      if(inlineData == null){
        throw new EntityProviderException(EntityProviderException.NULL_VALUE);
      }
      if (inlineData != null && inlineData.getProperties() != null) {
        final EntitySerializerProperties inlineProperties = inlineData.getWriteProperties() == null ?
            EntitySerializerProperties.
            serviceRoot(properties.getServiceRoot()).build() : inlineData.getWriteProperties();
        final EntityInfoAggregator inlineEntityInfo =
            EntityInfoAggregator.create(inlineEntitySet, null);
        new JsonEntryEntitySerializer(inlineProperties).append(writer, inlineEntityInfo, inlineData);
      } else {
        jsonStreamWriter.beginObject();
        jsonStreamWriter.endObject();
      }
    }
   }

  private void writeProperties(final EntityInfoAggregator entityInfo, final Map<String, Object> data,
      final EdmEntityType type, boolean containsMetadata) throws EdmException, EntityProviderException, IOException {
    // if the payload contains metadata we must not omit the first comm as it separates the _metadata object form the
    // properties
    boolean omitComma = !containsMetadata;

    List<String> propertyNames = type.getPropertyNames();
    for (final String propertyName : propertyNames) {
      if (data.containsKey(propertyName)) {
        omitComma = appendPropertyNameValue(entityInfo, data, omitComma, propertyName);
      } 
    }
  }

  /**
   * @param entityInfo
   * @param data
   * @param omitComma
   * @param propertyName
   * @return
   * @throws IOException
   * @throws EdmException
   * @throws EntityProviderException
   */
  private boolean appendPropertyNameValue(final EntityInfoAggregator entityInfo, final Map<String, Object> data,
      boolean omitComma, String propertyName) throws IOException, EdmException, EntityProviderException {
    if (omitComma) {
      omitComma = false; //NOSONAR
    } else {
      jsonStreamWriter.separator();
    }
    jsonStreamWriter.name(propertyName);
 
    JsonPropertyEntitySerializer.appendPropertyValue(jsonStreamWriter,
        entityInfo.getPropertyInfo(propertyName),
        data.get(propertyName),
        properties.isValidatingFacets());
    return omitComma;
  }
  
  private void writeMetadata(final EntityInfoAggregator entityInfo, final Map<String, Object> data,
      final EdmEntityType type) throws IOException, EntityProviderException, EdmException {
    if (properties.getServiceRoot() == null) {
      location = "";
      idlocation = "";
    } else {
      location = properties.getServiceRoot().toASCIIString() +
          AtomEntryEntitySerializer.createSelfLink(entityInfo, data, null, properties.isKeyAutoGenerated(), false);
      idlocation = properties.getServiceRoot().toASCIIString() +
          AtomEntryEntitySerializer.createSelfLink(entityInfo, data, null, properties.isKeyAutoGenerated(), true);
    }

    jsonStreamWriter.name(FormatJson.METADATA);
    jsonStreamWriter.beginObject();
    jsonStreamWriter.namedStringValue(FormatJson.ID, idlocation);
    jsonStreamWriter.separator();
    jsonStreamWriter.namedStringValue(FormatJson.URI, location);
    jsonStreamWriter.separator();
    jsonStreamWriter.namedStringValueRaw(FormatJson.TYPE, type.getNamespace() + Edm.DELIMITER + type.getName());
    
    if (type.hasStream()) {
      jsonStreamWriter.separator();

      EdmMapping entityTypeMapping = entityInfo.getEntityType().getMapping();
      String mediaResourceMimeType = null;
      String mediaSrc = null;

      if (entityTypeMapping != null) {
        String mediaResourceSourceKey = entityTypeMapping.getMediaResourceSourceKey();
        if (mediaResourceSourceKey != null) {
          mediaSrc = (String) data.get(mediaResourceSourceKey);
        }
        if (mediaSrc == null) {
          mediaSrc = location + VALUE;
        }
        String mediaResourceMimeTypeKey = entityTypeMapping.getMediaResourceMimeTypeKey();
        if (mediaResourceMimeTypeKey != null) {
          mediaResourceMimeType = (String) data.get(mediaResourceMimeTypeKey);
        }
        if (mediaResourceMimeType == null) {
          mediaResourceMimeType = ContentType.APPLICATION_OCTET_STREAM.toString();
        }
      } else {
        mediaSrc = location + VALUE;
        mediaResourceMimeType = ContentType.APPLICATION_OCTET_STREAM.toString();
      }

      jsonStreamWriter.namedStringValueRaw(FormatJson.CONTENT_TYPE, mediaResourceMimeType);
      jsonStreamWriter.separator();

      jsonStreamWriter.namedStringValue(FormatJson.MEDIA_SRC, mediaSrc);
      jsonStreamWriter.separator();
      jsonStreamWriter.namedStringValue(FormatJson.EDIT_MEDIA, location + VALUE);
    }
    jsonStreamWriter.endObject();
  }

  private String createCustomTargetLink(final EntityInfoAggregator entityInfo, final String navigationPropertyName,
      final Map<String, Object> key) throws EntityProviderException, EdmException {
    String target;
    final EntityInfoAggregator targetEntityInfo = EntityInfoAggregator.create(
        entityInfo.getEntitySet().getRelatedEntitySet(
            (EdmNavigationProperty) entityInfo.getEntityType().getProperty(navigationPropertyName)));
    target = (properties.getServiceRoot() == null ? "" : properties.getServiceRoot().toASCIIString())
        + AtomEntryEntitySerializer.createSelfLink(targetEntityInfo, key, null, properties.isKeyAutoGenerated(), false);
    return target;
  }

  private void writeNavigationLinks(final EntityInfoAggregator entityInfo,
      Map<String, Object> navigationLinks) throws IOException, 
  EntityProviderException, EdmException {
    if (navigationLinks != null && !navigationLinks.isEmpty()) {
      for (Entry<String, Object> entry : navigationLinks.entrySet()) {
        String target = null;
        if (entry.getValue() instanceof HashMap<?, ?>) {
          @SuppressWarnings("unchecked")
          Map<String, Object> navigationKeyMap = (Map<String, Object>) entry.getValue();
          if (navigationKeyMap != null && !navigationKeyMap.isEmpty()) { //NOSONAR
            target = createCustomTargetLink(entityInfo, entry.getKey(), navigationKeyMap);
            jsonStreamWriter.separator();
            jsonStreamWriter.name(entry.getKey());
            jsonStreamWriter.beginObject()
                .name(FormatJson.DEFERRED);
            JsonLinkEntitySerializer.appendUri(jsonStreamWriter, target);
            jsonStreamWriter.endObject();
          }
        }
      }
    }
  }

  public String getLocation() {
    return location;
  }
}
