package com.jayway.jsonpath.spi.json;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.jayway.jsonpath.InvalidJsonException;
import com.jayway.jsonpath.JsonPathException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;


public class JacksonJsonNodeJsonProvider extends AbstractJsonProvider {

    private static final ObjectMapper defaultObjectMapper = new ObjectMapper();

    protected ObjectMapper objectMapper;

    public ObjectMapper getObjectMapper() {
        return objectMapper;
    }

    /**
     * Initialize the JacksonJsonNodeJsonProvider with the default ObjectMapper and ObjectReader
     */
    public JacksonJsonNodeJsonProvider() {
        this(defaultObjectMapper);
    }

    /**
     * Initialize the JacksonJsonNodeJsonProvider with a custom ObjectMapper and ObjectReader.
     *
     * @param objectMapper the ObjectMapper to use
     */
    public JacksonJsonNodeJsonProvider(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public Object parse(String json) throws InvalidJsonException {
        try {
            return objectMapper.readTree(json);
        } catch (IOException e) {
            throw new InvalidJsonException(e, json);
        }
    }

    @Override
    public Object parse(byte[] json)
        throws InvalidJsonException {
        try {
            return objectMapper.readTree(json);
        } catch (IOException e) {
            throw new InvalidJsonException(e, new String(json, StandardCharsets.UTF_8));
        }
    }

    @Override
    public Object parse(InputStream jsonStream, String charset) throws InvalidJsonException {
        try {
            return objectMapper.readTree(new InputStreamReader(jsonStream, charset));
        } catch (IOException e) {
            throw new InvalidJsonException(e);
        }
    }

    @Override
    public String toJson(Object obj) {
        if (!(obj instanceof JsonNode)) {
            throw new JsonPathException("Not a JSON Node");
        }
        return obj.toString();
    }

    @Override
    public Object createArray() {
        return JsonNodeFactory.instance.arrayNode();
    }

    @Override
    public Object createMap() {
        return JsonNodeFactory.instance.objectNode();
    }

    public Object unwrap(Object o) {
        if (o == null) {
            return null;
        }
        if (!(o instanceof JsonNode)) {
            return o;
        }

        JsonNode e = (JsonNode) o;

        if (e.isValueNode()) {

            if (e.isTextual()) {
                return e.asText();
            } else if (e.isBoolean()) {
                return e.asBoolean();
            } else if (e.isInt()) {
                return e.asInt();
            } else if (e.isLong()) {
                return e.asLong();
            } else if (e.isBigInteger()) {
                return e.bigIntegerValue();
            } else if (e.isDouble()) {
                return e.doubleValue();
            } else if (e.isFloat()) {
                return e.floatValue();
            } else if (e.isBigDecimal()) {
                return e.decimalValue();
            } else if (e.isNull()) {
                return null;
            }
        }
        return o;
    }


    @Override
    public boolean isArray(Object obj) {
        return (obj instanceof ArrayNode || obj instanceof List);
    }

    @Override
    public Object getArrayIndex(Object obj, int idx) {
        return toJsonArray(obj).get(idx);
    }

    @Override
    public void setArrayIndex(Object array, int index, Object newValue) {
        if (!isArray(array)) {
            throw new UnsupportedOperationException();
        } else {
            ArrayNode arrayNode = toJsonArray(array);
            if (index == arrayNode.size()){
                arrayNode.add(createJsonElement(newValue));
            }else {
                arrayNode.set(index, createJsonElement(newValue));
            }
        }
    }

    @Override
    public Object getMapValue(Object obj, String key) {
        ObjectNode jsonObject = toJsonObject(obj);
        Object o = jsonObject.get(key);
        if (!jsonObject.has(key)) {
            return UNDEFINED;
        } else {
            return o;
        }
    }

	@Override
	public void setProperty(Object obj, Object key, Object value) {
		// jlolling: Bug: #211 avoid create cloned nodes
        if (isMap(obj)) {
        	setValueInObjectNode((ObjectNode) obj, key, value);
        } else {
            ArrayNode array = (ArrayNode) obj;
            int index;
            if (key != null) {
                index = key instanceof Integer ? (Integer) key : Integer.parseInt(key.toString());
            } else {
                index = array.size();
            }
            if (index == array.size()) {
                array.add(createJsonElement(value));
            } else {
                array.set(index, createJsonElement(value));
            }
        }
	}

    public void removeProperty(Object obj, Object key) {
        if (isMap(obj)) {
            toJsonObject(obj).remove(key.toString());
        } else {
            ArrayNode array = toJsonArray(obj);
            int index = key instanceof Integer ? (Integer) key : Integer.parseInt(key.toString());
            array.remove(index);
        }
    }

    @Override
    public boolean isMap(Object obj) {
        return (obj instanceof ObjectNode);
    }

    @Override
    public Collection<String> getPropertyKeys(Object obj) {
        List<String> keys = new ArrayList<String>();

        Iterator<String> iter = toJsonObject(obj).fieldNames();
        while (iter.hasNext()){
            keys.add(iter.next());
        }
        return keys;
    }

    @Override
    public int length(Object obj) {
        if (isArray(obj)) {
            return toJsonArray(obj).size();
        } else if (isMap(obj)) {
            return toJsonObject(obj).size();
        } else {
            if (obj instanceof TextNode) {
                TextNode element = (TextNode) obj;
                return element.size();
            }
        }
        throw new JsonPathException("length operation can not applied to " + (obj != null ? obj.getClass().getName()
                : "null"));
    }

    @Override
    public Iterable<?> toIterable(Object obj) {
        ArrayNode arr = toJsonArray(obj);
        Iterator<?> iterator = arr.iterator();
        return new Iterable<Object>() {
            @Override
            public Iterator<Object> iterator() {
                return new Iterator<Object>() {
                    @Override
                    public boolean hasNext() {
                        return iterator.hasNext();
                    }

                    @Override
                    public Object next() {
                        return unwrap(iterator.next());
                    }
                };
            }
        };
    }

    private JsonNode createJsonElement(Object o) {
    	if (o != null) {
    		// jlolling: avoid creating a cloned node: bug #211
    		if (o instanceof JsonNode) {
    			return (JsonNode) o;
    		} else {
    	        return objectMapper.valueToTree(o);
    		}
    	} else {
    		return null;
    	}
    }

    private ArrayNode toJsonArray(Object o) {
        return (ArrayNode) o;
    }

    private ObjectNode toJsonObject(Object o) {
        return (ObjectNode) o;
    }

	private void setValueInObjectNode(ObjectNode objectNode, Object key, Object value) {
		// jlolling: necessary to avoid deprecated methods and to avoid creating a cloned node. Bug: #211
    	if (value instanceof JsonNode) {
            objectNode.set(key.toString(), (JsonNode) value);
    	} else if (value instanceof String) {
    		objectNode.put(key.toString(), (String) value);
    	} else if (value instanceof Integer) {
    		objectNode.put(key.toString(), (Integer) value);
    	} else if (value instanceof Long) {
    		objectNode.put(key.toString(), (Long) value);
    	} else if (value instanceof Short) {
    		objectNode.put(key.toString(), (Short) value);
    	} else if (value instanceof BigInteger) {
            objectNode.put(key.toString(), (BigInteger) value);
        } else if (value instanceof Double) {
    		objectNode.put(key.toString(), (Double) value);
    	} else if (value instanceof Float) {
    		objectNode.put(key.toString(), (Float) value);
    	} else if (value instanceof BigDecimal) {
    		objectNode.put(key.toString(), (BigDecimal) value);
    	} else if (value instanceof Boolean) {
    		objectNode.put(key.toString(), (Boolean) value);
    	} else if (value instanceof byte[]) {
    		objectNode.put(key.toString(), (byte[]) value);
    	} else if (value == null) {
    		objectNode.set(key.toString(), null); // this will create a null-node
    	} else {
    		objectNode.set(key.toString(), createJsonElement(value));
    	}
	}

}
