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

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.AbstractMap.SimpleEntry;

import org.apache.olingo.commons.api.Constants;
import org.apache.olingo.commons.api.data.Annotation;
import org.apache.olingo.commons.api.data.ComplexValue;
import org.apache.olingo.commons.api.data.Property;
import org.apache.olingo.commons.api.data.PropertyType;
import org.apache.olingo.commons.api.data.Valuable;
import org.apache.olingo.commons.api.data.ValueType;
import org.apache.olingo.commons.api.edm.EdmPrimitiveType;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.api.edm.geo.Geospatial;
import org.apache.olingo.commons.core.edm.EdmTypeInfo;
import org.apache.olingo.server.api.deserializer.DeserializerException;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class ODataJsonInstanceAnnotationDeserializer {

	private static final String ODATA_CONTROL_INFORMATION_PREFIX = "@odata.";
	
	/**
	 * Consume the instance annotation of an entity or a property
	 * @param key String
	 * @param value JsonNode
	 * @return Annotation of an entity
	 * @throws DeserializerException
	 */
	public Annotation consumeInstanceAnnotation(String key, JsonNode value) 
			throws DeserializerException {
		Annotation annotation = new Annotation();
		annotation.setTerm(key);
		try {
			value(annotation, value);
		} catch (EdmPrimitiveTypeException | IOException e) {
			throw new DeserializerException("Property: " + key,
					DeserializerException.MessageKeys.INVALID_VALUE_FOR_PROPERTY, key);
		}
		return annotation;
	}

	private void value(final Valuable valuable, final JsonNode node) 
			throws IOException, EdmPrimitiveTypeException {

		EdmTypeInfo typeInfo = null;

		final Map.Entry<PropertyType, EdmTypeInfo> guessed = guessPropertyType(node);
		if (typeInfo == null) {
			typeInfo = guessed.getValue();
		}

		final PropertyType propType = typeInfo == null ? guessed.getKey()
				: typeInfo.isCollection() ? PropertyType.COLLECTION
				: typeInfo.isPrimitiveType() ? PropertyType.PRIMITIVE
				: node.isValueNode() ? PropertyType.ENUM : PropertyType.COMPLEX;

		switch (propType) {
		case COLLECTION:
			fromCollection(valuable, node.elements(), typeInfo);
			break;

		case COMPLEX:
			if (node.has(Constants.JSON_TYPE)) {
				valuable.setType(node.get(Constants.JSON_TYPE).asText());
				((ObjectNode) node).remove(Constants.JSON_TYPE);
			}
			final Object value = fromComplex((ObjectNode) node);
			if (value instanceof ComplexValue) {
				((ComplexValue) value).setTypeName(valuable.getType());
			}
			valuable.setValue(ValueType.COMPLEX, value);
			break;

		case ENUM:
			if (!node.isNull()) {
				valuable.setValue(ValueType.ENUM, node.asText());
			}
			break;

		case PRIMITIVE:
			if (valuable.getType() == null && typeInfo != null) {
				valuable.setType(typeInfo.getFullQualifiedName().toString());
			}
			final Object primitiveValue = fromPrimitive(node, typeInfo);
			valuable.setValue(primitiveValue instanceof Geospatial ? 
					ValueType.GEOSPATIAL : ValueType.PRIMITIVE,
					primitiveValue);
			break;

		case EMPTY:
		default:
			valuable.setValue(ValueType.PRIMITIVE, "");
		}
	}

	private Object fromPrimitive(final JsonNode node, final EdmTypeInfo typeInfo) 
			throws EdmPrimitiveTypeException {
		return node.isNull() ? null
				: typeInfo == null ? node.asText()
				: ((EdmPrimitiveType) typeInfo.getType()).valueOfString(node.asText(), true, null,
				Constants.DEFAULT_PRECISION, Constants.DEFAULT_SCALE, true,
				((EdmPrimitiveType) typeInfo.getType()).getDefaultType());
	}

	private Object fromComplex(final ObjectNode node) throws IOException, EdmPrimitiveTypeException {

		final ComplexValue complexValue = new ComplexValue();
		final Set<String> toRemove = new HashSet<>();
		for (final Iterator<Map.Entry<String, JsonNode>> itor = node.fields(); itor.hasNext();) {
			final Map.Entry<String, JsonNode> field = itor.next();
			if (field.getKey().contains(ODATA_CONTROL_INFORMATION_PREFIX)) {
				toRemove.add(field.getKey());
			} else {
				Property property = new Property();
				property.setName(field.getKey());
				value(property, field.getValue());
				complexValue.getValue().add(property);
			}
		}
		node.remove(toRemove);
		return complexValue;
	}

	private void fromCollection(final Valuable valuable, final Iterator<JsonNode> nodeItor, 
			final EdmTypeInfo typeInfo)
			throws IOException, EdmPrimitiveTypeException {

		final List<Object> values = new ArrayList<>();
		ValueType valueType = ValueType.COLLECTION_PRIMITIVE;

		final EdmTypeInfo type = typeInfo == null ? null
				: new EdmTypeInfo.Builder().setTypeExpression(
						typeInfo.getFullQualifiedName().toString()).build();

		while (nodeItor.hasNext()) {
			final JsonNode child = nodeItor.next();

			if (child.isValueNode()) {
				if (typeInfo == null || typeInfo.isPrimitiveType()) {
					final Object value = fromPrimitive(child, type);
					valueType = value instanceof Geospatial ? ValueType.COLLECTION_GEOSPATIAL
							: ValueType.COLLECTION_PRIMITIVE;
					values.add(value);
				} else {
					valueType = ValueType.COLLECTION_ENUM;
					values.add(child.asText());
				}
			} else if (child.isContainerNode()) {
				EdmTypeInfo childType = null;
				if (child.has(Constants.JSON_TYPE)) {
					String typeName = child.get(Constants.JSON_TYPE).asText();
					childType = typeName == null ? null : new EdmTypeInfo.Builder()
							.setTypeExpression(typeName).build();
					((ObjectNode) child).remove(Constants.JSON_TYPE);
				}
				final Object value = fromComplex((ObjectNode) child);
				if (childType != null) {
					((ComplexValue) value).setTypeName(childType.external());
				}
				valueType = ValueType.COLLECTION_COMPLEX;
				values.add(value);
			}
		}
		valuable.setValue(valueType, values);
	}

	private Map.Entry<PropertyType, EdmTypeInfo> guessPropertyType(final JsonNode node) {
		PropertyType type;
		String typeExpression = null;

		if (node.isValueNode() || node.isNull()) {
			type = PropertyType.PRIMITIVE;
			typeExpression = guessPrimitiveTypeKind(node).getFullQualifiedName().toString();
		} else if (node.isArray()) {
			type = PropertyType.COLLECTION;
			if (node.has(0) && node.get(0).isValueNode()) {
				typeExpression = "Collection(" + guessPrimitiveTypeKind(node.get(0)) + ')';
			}
		} else if (node.isObject()) {
			if (node.has(Constants.ATTR_TYPE)) {
				type = PropertyType.PRIMITIVE;
				typeExpression = "Edm.Geography" + node.get(Constants.ATTR_TYPE).asText();
			} else {
				type = PropertyType.COMPLEX;
			}
		} else {
			type = PropertyType.EMPTY;
		}

		final EdmTypeInfo typeInfo = typeExpression == null ? null
				: new EdmTypeInfo.Builder().setTypeExpression(typeExpression).build();
		return new SimpleEntry<>(type, typeInfo);
	}

	private EdmPrimitiveTypeKind guessPrimitiveTypeKind(final JsonNode node) {
		return node.isShort() ? EdmPrimitiveTypeKind.Int16
				: node.isInt() ? EdmPrimitiveTypeKind.Int32
				: node.isLong() ? EdmPrimitiveTypeKind.Int64
				: node.isBoolean() ? EdmPrimitiveTypeKind.Boolean
				: node.isFloat() ? EdmPrimitiveTypeKind.Single
				: node.isDouble() ? EdmPrimitiveTypeKind.Double
				: node.isBigDecimal() ? EdmPrimitiveTypeKind.Decimal
				: EdmPrimitiveTypeKind.String;
	}
}
