/*
 * 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.server.core.responses;

import java.util.List;
import java.util.Map;

import org.apache.olingo.commons.api.data.ContextURL;
import org.apache.olingo.commons.api.data.Entity;
import org.apache.olingo.commons.api.edm.EdmAction;
import org.apache.olingo.commons.api.edm.EdmEntitySet;
import org.apache.olingo.commons.api.edm.EdmEntityType;
import org.apache.olingo.commons.api.edm.EdmFunction;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.api.edm.EdmProperty;
import org.apache.olingo.commons.api.format.ContentType;
import org.apache.olingo.commons.api.http.HttpHeader;
import org.apache.olingo.commons.api.http.HttpStatusCode;
import org.apache.olingo.commons.core.Encoder;
import org.apache.olingo.commons.core.edm.primitivetype.EdmPrimitiveTypeFactory;
import org.apache.olingo.commons.core.edm.primitivetype.EdmString;
import org.apache.olingo.server.api.ODataApplicationException;
import org.apache.olingo.server.api.ODataLibraryException;
import org.apache.olingo.server.api.ODataResponse;
import org.apache.olingo.server.api.ODataServerError;
import org.apache.olingo.server.api.ServiceMetadata;
import org.apache.olingo.server.api.serializer.EntitySerializerOptions;
import org.apache.olingo.server.api.serializer.ODataSerializer;
import org.apache.olingo.server.api.serializer.SerializerException;
import org.apache.olingo.server.core.ContentNegotiatorException;
import org.apache.olingo.server.core.ReturnRepresentation;
import org.apache.olingo.server.core.ServiceRequest;
import org.apache.olingo.server.core.serializer.utils.ContentTypeHelper;

public class EntityResponse extends ServiceResponse {
  private final ReturnRepresentation returnRepresentation;
  private final ODataSerializer serializer;
  private final EntitySerializerOptions options;
  private final ContentType responseContentType;
  private final String baseURL;

  private EntityResponse(ServiceMetadata metadata, ODataResponse response,
      ODataSerializer serializer, EntitySerializerOptions options, ContentType responseContentType,
      Map<String, String> preferences, ReturnRepresentation returnRepresentation, String baseURL) {
    super(metadata, response, preferences);
    this.serializer = serializer;
    this.options = options;
    this.responseContentType = responseContentType;
    this.returnRepresentation = returnRepresentation;
    this.baseURL = baseURL;
  }

  public static EntityResponse getInstance(ServiceRequest request, ContextURL contextURL,
      boolean references, ODataResponse response, ReturnRepresentation returnRepresentation)
      throws ContentNegotiatorException, SerializerException {
    EntitySerializerOptions options = request.getSerializerOptions(EntitySerializerOptions.class,
        contextURL, references);
    return new EntityResponse(request.getServiceMetaData(), response, request.getSerializer(),
        options, request.getResponseContentType(), request.getPreferences(), returnRepresentation, 
        request.getODataRequest().getRawBaseUri());
  }

  public static EntityResponse getInstance(ServiceRequest request, ContextURL contextURL,
      boolean references, ODataResponse response)
      throws ContentNegotiatorException, SerializerException {
    EntitySerializerOptions options = request.getSerializerOptions(EntitySerializerOptions.class,
        contextURL, references);
    return new EntityResponse(request.getServiceMetaData(), response, request.getSerializer(),
        options, request.getResponseContentType(), request.getPreferences(), null,
        request.getODataRequest().getRawBaseUri());
  }

  // write single entity
  public void writeReadEntity(EdmEntityType entityType, Entity entity) throws SerializerException {

    assert (!isClosed());

    if (entity == null) {
      writeNotFound(true);
      return;
    }

    if (ContentTypeHelper.isODataMetadataFull(this.responseContentType)) {
      EdmAction action = this.metadata.getEdm().getBoundActionWithBindingType(
          entityType.getFullQualifiedName(), false);
      if (action != null) {
        entity.getOperations().add(buildOperation(action, entity.getId().toASCIIString()));
      }
      
      List<EdmFunction> functions = this.metadata.getEdm()
          .getBoundFunctionsWithBindingType(entityType.getFullQualifiedName(),false);
      
      for (EdmFunction function:functions) {
        entity.getOperations().add(buildOperation(function, entity.getId().toASCIIString()));
      }
    }
    
    // write the entity to response
    this.response.setContent(this.serializer.entity(this.metadata, entityType, entity, this.options).getContent());
    writeOK(responseContentType);
    close();
  }

  public void writeCreatedEntity(EdmEntitySet entitySet, Entity entity)
      throws SerializerException {
    // upsert/insert must created a entity, otherwise should have throw an
    // exception
    assert (entity != null);
    
    String locationHeader;
    try {
      locationHeader = buildLocation(this.baseURL, entity, entitySet.getName(), entitySet.getEntityType());
    } catch (EdmPrimitiveTypeException e) {
      throw new SerializerException(e.getMessage(), e, SerializerException.MessageKeys.WRONG_PRIMITIVE_VALUE);
    }

    // Note that if media written just like Stream, but on entity URL

    // 8.2.8.7
    if (this.returnRepresentation == ReturnRepresentation.MINIMAL) {
      writeNoContent(false);
      writeHeader(HttpHeader.LOCATION, locationHeader);
      writeHeader("Preference-Applied", "return=minimal"); //$NON-NLS-1$ //$NON-NLS-2$
      // 8.3.3
      writeHeader("OData-EntityId", entity.getId().toASCIIString()); //$NON-NLS-1$
      close();
      return;
    }

    // return the content of the created entity
    this.response.setContent(this.serializer.entity(this.metadata, entitySet.getEntityType(), entity, this.options)
        .getContent());
    writeCreated(false);
    writeHeader(HttpHeader.LOCATION, locationHeader);
    if (this.returnRepresentation != ReturnRepresentation.NONE) {
      writeHeader("Preference-Applied", "return=representation"); //$NON-NLS-1$ //$NON-NLS-2$
    }
    writeHeader(HttpHeader.CONTENT_TYPE, this.responseContentType.toContentTypeString());
    close();
  }

  public void writeUpdatedEntity() {
    // spec says just success response; so either 200 or 204. 200 typically has
    // payload
    writeNoContent(true);
  }

  public void writeDeletedEntityOrReference() {
    writeNoContent(true);
  }

  @Override
  public void accepts(ServiceResponseVisior visitor) throws ODataLibraryException,
      ODataApplicationException {
    visitor.visit(this);
  }

  public void writeCreated(boolean closeResponse) {
    this.response.setStatusCode(HttpStatusCode.CREATED.getStatusCode());
    if (closeResponse) {
      close();
    }
  }
  
  public void writeError(ODataServerError error) {
    try {
      writeHeader(HttpHeader.CONTENT_TYPE, this.responseContentType.toContentTypeString());
      writeContent(this.serializer.error(error).getContent(), error.getStatusCode(), true);
    } catch (SerializerException e) {
      writeServerError(true);
    }
  }
  
  public void writeNotModified() {
    this.response.setStatusCode(HttpStatusCode.NOT_MODIFIED.getStatusCode());
    close();
  }  
  
  public static String buildLocation(String baseURL, Entity entity, String enitySetName, EdmEntityType type) 
      throws EdmPrimitiveTypeException {
    StringBuilder location = new StringBuilder();

    location.append(baseURL).append("/").append(enitySetName);
    
    int i = 0;
    boolean usename = type.getKeyPredicateNames().size() > 1;
    location.append("(");
    for (String key : type.getKeyPredicateNames()) {
      if (i > 0) {
        location.append(",");
      }
      i++;
      if (usename) {
        location.append(key).append("=");
      }
      
      EdmProperty property = (EdmProperty)type.getProperty(key);
      String propertyType = entity.getProperty(key).getType();
      Object propertyValue = entity.getProperty(key).getValue();
      
      if (propertyValue == null) {
        throw new EdmPrimitiveTypeException("The key value for property "+key+" is invalid; Key value cannot be null");
      }
      
      if(propertyType.startsWith("Edm.")) {
        propertyType = propertyType.substring(4);
      }
      EdmPrimitiveTypeKind kind = EdmPrimitiveTypeKind.valueOf(propertyType);
      String value =  EdmPrimitiveTypeFactory.getInstance(kind).valueToString(
          propertyValue, property.isNullable(), property.getMaxLength(), property.getPrecision(), property.getScale(), 
          property.isUnicode());
      if (kind == EdmPrimitiveTypeKind.String) {
          value = EdmString.getInstance().toUriLiteral(Encoder.encode(value));
      }
      location.append(value);
    }
    location.append(")");
    return location.toString();
  }  
}
