package com.amazonaws.services.dynamodbv2.json.converter.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.amazonaws.services.dynamodbv2.json.converter.JacksonConverter;
import com.amazonaws.services.dynamodbv2.json.converter.JacksonConverterException;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

/**
 * Implementation of the {@link JacksonConverter}.
 */
public class JacksonConverterImpl implements JacksonConverter {
    /**
     * Maximum JSON depth.
     */
    private static final int MAX_DEPTH = 50;

    /**
     * Constructs a {@link JacksonConverterImpl}.
     */
    public JacksonConverterImpl() {
    }

    /**
     * Asserts the depth is not greater than {@link #MAX_DEPTH}.
     *
     * @param depth
     *            Current JSON depth
     * @throws JacksonConverterException
     *             Depth is greater than {@link #MAX_DEPTH}
     */
    private void assertDepth(final int depth) throws JacksonConverterException {
        if (depth > MAX_DEPTH) {
            throw new JacksonConverterException("Max depth reached. The object/array has too much depth.");
        }
    }

    /**
     * Gets an DynamoDB representation of a JsonNode.
     *
     * @param node
     *            The JSON to convert
     * @param depth
     *            Current JSON depth
     * @return DynamoDB representation of the JsonNode
     * @throws JacksonConverterException
     *             Unknown JsonNode type or JSON is too deep
     */
    private AttributeValue getAttributeValue(final JsonNode node, final int depth) throws JacksonConverterException {
        assertDepth(depth);
        switch (node.asToken()) {
            case VALUE_STRING:
                return new AttributeValue().withS(node.textValue());
            case VALUE_NUMBER_INT:
            case VALUE_NUMBER_FLOAT:
                return new AttributeValue().withN(node.numberValue().toString());
            case VALUE_TRUE:
            case VALUE_FALSE:
                return new AttributeValue().withBOOL(node.booleanValue());
            case VALUE_NULL:
                return new AttributeValue().withNULL(true);
            case START_OBJECT:
                return new AttributeValue().withM(jsonObjectToMap(node, depth));
            case START_ARRAY:
                return new AttributeValue().withL(jsonArrayToList(node, depth));
            default:
                throw new JacksonConverterException("Unknown node type: " + node);
        }
    }

    /**
     * Converts a DynamoDB attribute to a JSON representation.
     *
     * @param av
     *            DynamoDB attribute
     * @param depth
     *            Current JSON depth
     * @return JSON representation of the DynamoDB attribute
     * @throws JacksonConverterException
     *             Unknown DynamoDB type or JSON is too deep
     */
    private JsonNode getJsonNode(final AttributeValue av, final int depth) throws JacksonConverterException {
        assertDepth(depth);
        if (av.getS() != null) {
            return JsonNodeFactory.instance.textNode(av.getS());
        } else if (av.getN() != null) {
            try {
                return JsonNodeFactory.instance.numberNode(Integer.parseInt(av.getN()));
            } catch (final NumberFormatException e) {
                // Not an integer
                try {
                    return JsonNodeFactory.instance.numberNode(Float.parseFloat(av.getN()));
                } catch (final NumberFormatException e2) {
                    // Not a number
                    throw new JacksonConverterException(e.getMessage());
                }
            }
        } else if (av.getBOOL() != null) {
            return JsonNodeFactory.instance.booleanNode(av.getBOOL());
        } else if (av.getNULL() != null) {
            return JsonNodeFactory.instance.nullNode();
        } else if (av.getL() != null) {
            return listToJsonArray(av.getL(), depth);
        } else if (av.getM() != null) {
            return mapToJsonObject(av.getM(), depth);
        } else {
            throw new JacksonConverterException("Unknown type value " + av);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JsonNode itemListToJsonArray(final List<Map<String, AttributeValue>> items) throws JacksonConverterException {
        if (items != null) {
            final ArrayNode array = JsonNodeFactory.instance.arrayNode();
            for (final Map<String, AttributeValue> item : items) {
                array.add(mapToJsonObject(item, 0));
            }
            return array;
        }
        throw new JacksonConverterException("Items cannnot be null");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<AttributeValue> jsonArrayToList(final JsonNode node) throws JacksonConverterException {
        return jsonArrayToList(node, 0);
    }

    /**
     * Helper method to convert a JsonArrayNode to a DynamoDB list.
     *
     * @param node
     *            Array node to convert
     * @param depth
     *            Current JSON depth
     * @return DynamoDB list representation of the array node
     * @throws JacksonConverterException
     *             JsonNode is not an array or depth is too great
     */
    private List<AttributeValue> jsonArrayToList(final JsonNode node, final int depth) throws JacksonConverterException {
        assertDepth(depth);
        if (node != null && node.isArray()) {
            final List<AttributeValue> result = new ArrayList<AttributeValue>();
            final Iterator<JsonNode> children = node.elements();
            while (children.hasNext()) {
                final JsonNode child = children.next();
                result.add(getAttributeValue(child, depth));
            }
            return result;
        }
        throw new JacksonConverterException("Expected JSON array, but received " + node);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<String, AttributeValue> jsonObjectToMap(final JsonNode node) throws JacksonConverterException {
        return jsonObjectToMap(node, 0);
    }

    /**
     * Transforms a JSON object to a DynamoDB object.
     *
     * @param node
     *            JSON object
     * @param depth
     *            Current JSON depth
     * @return DynamoDB object representation of JSON
     * @throws JacksonConverterException
     *             JSON is not an object or depth is too great
     */
    private Map<String, AttributeValue> jsonObjectToMap(final JsonNode node, final int depth)
        throws JacksonConverterException {
        assertDepth(depth);
        if (node != null && node.isObject()) {
            final Map<String, AttributeValue> result = new HashMap<String, AttributeValue>();
            final Iterator<String> keys = node.fieldNames();
            while (keys.hasNext()) {
                final String key = keys.next();
                result.put(key, getAttributeValue(node.get(key), depth + 1));
            }
            return result;
        }
        throw new JacksonConverterException("Expected JSON Object, but received " + node);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JsonNode listToJsonArray(final List<AttributeValue> item) throws JacksonConverterException {
        return listToJsonArray(item, 0);
    }

    /**
     * Converts a DynamoDB list to a JSON list.
     *
     * @param item
     *            DynamoDB list
     * @param depth
     *            Current JSON depth
     * @return JSON array node representation of DynamoDB list
     * @throws JacksonConverterException
     *             Null DynamoDB list or JSON too deep
     */
    private JsonNode listToJsonArray(final List<AttributeValue> item, final int depth) throws JacksonConverterException {
        assertDepth(depth);
        if (item != null) {
            final ArrayNode node = JsonNodeFactory.instance.arrayNode();
            for (final AttributeValue value : item) {
                node.add(getJsonNode(value, depth + 1));
            }
            return node;
        }
        throw new JacksonConverterException("Item cannot be null");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public JsonNode mapToJsonObject(final Map<String, AttributeValue> item) throws JacksonConverterException {
        return mapToJsonObject(item, 0);
    }

    /**
     * Converts a DynamoDB object to a JSON map.
     *
     * @param item
     *            DynamoDB object
     * @param depth
     *            Current JSON depth
     * @return JSON map representation of the DynamoDB object
     * @throws JacksonConverterException
     *             Null DynamoDB object or JSON too deep
     */
    private JsonNode mapToJsonObject(final Map<String, AttributeValue> item, final int depth)
        throws JacksonConverterException {
        assertDepth(depth);
        if (item != null) {
            final ObjectNode node = JsonNodeFactory.instance.objectNode();

            for (final Entry<String, AttributeValue> entry : item.entrySet()) {
                node.put(entry.getKey(), getJsonNode(entry.getValue(), depth + 1));
            }
            return node;
        }
        throw new JacksonConverterException("Item cannot be null");
    }

}