package io.leangen.graphql.module.common.jackson;

import com.fasterxml.jackson.databind.node.BigIntegerNode;
import com.fasterxml.jackson.databind.node.BinaryNode;
import com.fasterxml.jackson.databind.node.BooleanNode;
import com.fasterxml.jackson.databind.node.DecimalNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.FloatNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.NumericNode;
import com.fasterxml.jackson.databind.node.ShortNode;
import com.fasterxml.jackson.databind.node.TextNode;
import graphql.language.BooleanValue;
import graphql.language.FloatValue;
import graphql.language.IntValue;
import graphql.language.StringValue;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.GraphQLScalarType;

import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static io.leangen.graphql.util.Scalars.errorMessage;
import static io.leangen.graphql.util.Scalars.literalOrException;
import static io.leangen.graphql.util.Scalars.serializationException;
import static io.leangen.graphql.util.Scalars.valueParsingException;

@SuppressWarnings("WeakerAccess")
public class JacksonScalars {

    public static final GraphQLScalarType JsonTextNode = GraphQLScalarType.newScalar()
            .name("JsonText")
            .description("Text JSON node")
            .coercing(new Coercing<TextNode, String>() {
                @Override
                public String serialize(Object dataFetcherResult) {
                    if (dataFetcherResult instanceof String) {
                        return (String) dataFetcherResult;
                    } if (dataFetcherResult instanceof TextNode) {
                        return ((TextNode) dataFetcherResult).textValue();
                    } else {
                        throw serializationException(dataFetcherResult, String.class, TextNode.class);
                    }
                }

                @Override
                public TextNode parseValue(Object input) {
                    if (input instanceof String) {
                        return TextNode.valueOf((String) input);
                    }
                    if (input instanceof TextNode) {
                        return (TextNode) input;
                    }
                    throw valueParsingException(input, String.class, TextNode.class);
                }

                @Override
                public TextNode parseLiteral(Object input) {
                    return TextNode.valueOf(literalOrException(input, StringValue.class).getValue());
                }
            }).build();

    public static final GraphQLScalarType JsonBinaryNode = GraphQLScalarType.newScalar()
            .name("JsonBase64Binary")
            .description("Base64-encoded binary JSON node")
            .coercing(new Coercing<BinaryNode, String>() {
                private final Base64.Encoder encoder = Base64.getEncoder();
                private final Base64.Decoder decoder = Base64.getDecoder();

                @Override
                public String serialize(Object dataFetcherResult) {
                    if (dataFetcherResult instanceof BinaryNode) {
                        return encoder.encodeToString(((BinaryNode) dataFetcherResult).binaryValue());
                    }
                    if (dataFetcherResult instanceof String) {
                        return (String) dataFetcherResult;
                    }
                    throw serializationException(dataFetcherResult, String.class, BinaryNode.class);
                }

                @Override
                public BinaryNode parseValue(Object input) {
                    if (input instanceof String) {
                        return BinaryNode.valueOf(decoder.decode(input.toString()));
                    }
                    if (input instanceof BinaryNode) {
                        return (BinaryNode) input;
                    }
                    throw valueParsingException(input, String.class, BinaryNode.class);
                }

                @Override
                public BinaryNode parseLiteral(Object input) {
                    return new BinaryNode(decoder.decode(literalOrException(input, StringValue.class).getValue()));
                }
            }).build();

    public static final GraphQLScalarType JsonBooleanNode = GraphQLScalarType.newScalar()
            .name("JsonBoolean")
            .description("Boolean JSON node")
            .coercing(new Coercing<BooleanNode, Boolean>() {

                @Override
                public Boolean serialize(Object dataFetcherResult) {
                    if (dataFetcherResult instanceof BooleanNode) {
                        return ((BooleanNode) dataFetcherResult).booleanValue();
                    }
                    if (dataFetcherResult instanceof Boolean) {
                        return (Boolean) dataFetcherResult;
                    }
                    throw serializationException(dataFetcherResult, Boolean.class, BooleanNode.class);
                }

                @Override
                public BooleanNode parseValue(Object input) {
                    if (input instanceof Boolean) {
                        return (Boolean) input ? BooleanNode.TRUE : BooleanNode.FALSE;
                    }
                    if (input instanceof BooleanNode) {
                        return (BooleanNode) input;
                    }
                    throw valueParsingException(input, Boolean.class, BooleanNode.class);
                }

                @Override
                public BooleanNode parseLiteral(Object input) {
                    return literalOrException(input, BooleanValue.class).isValue() ? BooleanNode.TRUE : BooleanNode.FALSE;
                }
            }).build();

    public static final GraphQLScalarType JsonDecimalNode = GraphQLScalarType.newScalar()
            .name("JsonNumber")
            .description("Decimal JSON node")
            .coercing(new Coercing<DecimalNode, Number>() {

                @Override
                public Number serialize(Object dataFetcherResult) {
                    if (dataFetcherResult instanceof DecimalNode) {
                        return ((DecimalNode) dataFetcherResult).numberValue();
                    }
                    throw serializationException(dataFetcherResult, DecimalNode.class);
                }

                @Override
                public DecimalNode parseValue(Object input) {
                    if (input instanceof Number || input instanceof String) {
                        return (DecimalNode) JsonNodeFactory.instance.numberNode(new BigDecimal(input.toString()));
                    }
                    if (input instanceof DecimalNode) {
                        return (DecimalNode) input;
                    }
                    throw valueParsingException(input, Number.class, String.class, DecimalNode.class);
                }

                @Override
                public DecimalNode parseLiteral(Object input) {
                    if (input instanceof IntValue) {
                        return (DecimalNode) JsonNodeFactory.instance.numberNode(((IntValue) input).getValue());
                    } else if (input instanceof FloatValue) {
                        return (DecimalNode) JsonNodeFactory.instance.numberNode(((FloatValue) input).getValue());
                    } else {
                        throw new CoercingParseLiteralException(errorMessage(input, IntValue.class, FloatValue.class));
                    }
                }
            }).build();

    public static final GraphQLScalarType JsonIntegerNode = GraphQLScalarType.newScalar()
            .name("JsonInteger")
            .description("Integer JSON node")
            .coercing(new Coercing<IntNode, Number>() {

                @Override
                public Number serialize(Object dataFetcherResult) {
                    if (dataFetcherResult instanceof IntNode) {
                        return ((IntNode) dataFetcherResult).numberValue();
                    } else {
                        throw serializationException(dataFetcherResult, IntNode.class);
                    }
                }

                @Override
                public IntNode parseValue(Object input) {
                    if (input instanceof Number || input instanceof String) {
                        try {
                            return IntNode.valueOf(new BigInteger(input.toString()).intValueExact());
                        } catch (ArithmeticException e) {
                            throw new CoercingParseValueException(input + " does not fit into an int without a loss of precision");
                        }
                    }
                    if (input instanceof IntNode) {
                        return (IntNode) input;
                    }
                    throw valueParsingException(input, Number.class, String.class, IntNode.class);
                }

                @Override
                public IntNode parseLiteral(Object input) {
                    if (input instanceof IntValue) {
                        try {
                            return IntNode.valueOf(((IntValue) input).getValue().intValueExact());
                        } catch (ArithmeticException e) {
                            throw new CoercingParseLiteralException(input + " does not fit into an int without a loss of precision");
                        }
                    } else {
                        throw new CoercingParseLiteralException(errorMessage(input, IntValue.class));
                    }
                }
            }).build();

    public static final GraphQLScalarType JsonBigIntegerNode = GraphQLScalarType.newScalar()
            .name("JsonBigInteger")
            .description("BigInteger JSON node")
            .coercing(new Coercing<BigIntegerNode, Number>() {

                @Override
                public Number serialize(Object dataFetcherResult) {
                    if (dataFetcherResult instanceof BigIntegerNode) {
                        return ((BigIntegerNode) dataFetcherResult).numberValue();
                    } else {
                        throw serializationException(dataFetcherResult, BigIntegerNode.class);
                    }
                }

                @Override
                public BigIntegerNode parseValue(Object input) {
                    if (input instanceof Number || input instanceof String) {
                        return BigIntegerNode.valueOf(new BigInteger(input.toString()));
                    }
                    if (input instanceof BigIntegerNode) {
                        return (BigIntegerNode) input;
                    }
                    throw valueParsingException(input, Number.class, String.class, BigIntegerNode.class);
                }

                @Override
                public BigIntegerNode parseLiteral(Object input) {
                    if (input instanceof IntValue) {
                        return BigIntegerNode.valueOf(((IntValue) input).getValue());
                    } else {
                        throw new CoercingParseLiteralException(errorMessage(input, IntValue.class));
                    }
                }
            }).build();

    public static final GraphQLScalarType JsonShortNode = GraphQLScalarType.newScalar()
            .name("JsonShort")
            .description("Short JSON node")
            .coercing(new Coercing<ShortNode, Number>() {

                @Override
                public Number serialize(Object dataFetcherResult) {
                    if (dataFetcherResult instanceof ShortNode) {
                        return ((ShortNode) dataFetcherResult).numberValue();
                    } else {
                        throw serializationException(dataFetcherResult, ShortNode.class);
                    }
                }

                @Override
                public ShortNode parseValue(Object input) {
                    if (input instanceof Number || input instanceof String) {
                        try {
                            return ShortNode.valueOf(new BigInteger(input.toString()).shortValueExact());
                        } catch (ArithmeticException e) {
                            throw new CoercingParseValueException(input + " does not fit into a short without a loss of precision");
                        }
                    }
                    if (input instanceof ShortNode) {
                        return (ShortNode) input;
                    }
                    throw valueParsingException(input, Number.class, String.class, ShortNode.class);
                }

                @Override
                public ShortNode parseLiteral(Object input) {
                    if (input instanceof IntValue) {
                        try {
                            return ShortNode.valueOf(((IntValue) input).getValue().shortValueExact());
                        } catch (ArithmeticException e) {
                            throw new CoercingParseLiteralException(input + " does not fit into a short without a loss of precision");
                        }
                    } else {
                        throw new CoercingParseLiteralException(errorMessage(input, IntValue.class));
                    }
                }
            }).build();

    public static final GraphQLScalarType JsonFloatNode = GraphQLScalarType.newScalar()
            .name("JsonFloat")
            .description("Float JSON node")
            .coercing(new Coercing<FloatNode, Number>() {

                @Override
                public Number serialize(Object dataFetcherResult) {
                    if (dataFetcherResult instanceof FloatNode) {
                        return ((FloatNode) dataFetcherResult).numberValue();
                    } else {
                        throw serializationException(dataFetcherResult, FloatNode.class);
                    }
                }

                @Override
                public FloatNode parseValue(Object input) {
                    if (input instanceof Number || input instanceof String) {
                        return FloatNode.valueOf(new BigDecimal(input.toString()).floatValue());
                    }
                    if (input instanceof FloatNode) {
                        return (FloatNode) input;
                    }
                    throw valueParsingException(input, Number.class, String.class, FloatNode.class);
                }

                @Override
                public FloatNode parseLiteral(Object input) {
                    if (input instanceof IntValue) {
                        return FloatNode.valueOf(((IntValue) input).getValue().floatValue());
                    } if (input instanceof FloatValue) {
                        return FloatNode.valueOf(((FloatValue) input).getValue().floatValue());
                    } else {
                        throw new CoercingParseLiteralException(errorMessage(input, IntValue.class, FloatValue.class));
                    }
                }
            }).build();

    public static final GraphQLScalarType JsonDoubleNode = GraphQLScalarType.newScalar()
            .name("JsonDouble")
            .description("Double JSON node")
            .coercing(new Coercing<DoubleNode, Number>() {

                @Override
                public Number serialize(Object dataFetcherResult) {
                    if (dataFetcherResult instanceof DoubleNode) {
                        return ((DoubleNode) dataFetcherResult).numberValue();
                    } else {
                        throw serializationException(dataFetcherResult, DoubleNode.class);
                    }
                }

                @Override
                public DoubleNode parseValue(Object input) {
                    if (input instanceof Number || input instanceof String) {
                        return DoubleNode.valueOf(new BigDecimal(input.toString()).doubleValue());
                    }
                    if (input instanceof DoubleNode) {
                        return (DoubleNode) input;
                    }
                    throw valueParsingException(input, Number.class, String.class, DoubleNode.class);
                }

                @Override
                public DoubleNode parseLiteral(Object input) {
                    if (input instanceof IntValue) {
                        return DoubleNode.valueOf(((IntValue) input).getValue().doubleValue());
                    } if (input instanceof FloatValue) {
                        return DoubleNode.valueOf(((FloatValue) input).getValue().doubleValue());
                    } else {
                        throw new CoercingParseLiteralException(errorMessage(input, IntValue.class, FloatValue.class));
                    }
                }
            }).build();

    private static final Map<Type, GraphQLScalarType> SCALAR_MAPPING = getScalarMapping();

    public static boolean isScalar(Type javaType) {
        return SCALAR_MAPPING.containsKey(javaType);
    }

    public static GraphQLScalarType toGraphQLScalarType(Type javaType) {
        return SCALAR_MAPPING.get(javaType);
    }

    private static Map<Type, GraphQLScalarType> getScalarMapping() {
        Map<Type, GraphQLScalarType> scalarMapping = new HashMap<>();
        scalarMapping.put(TextNode.class, JsonTextNode);
        scalarMapping.put(BooleanNode.class, JsonBooleanNode);
        scalarMapping.put(BinaryNode.class, JsonBinaryNode);
        scalarMapping.put(BigIntegerNode.class, JsonBigIntegerNode);
        scalarMapping.put(IntNode.class, JsonIntegerNode);
        scalarMapping.put(ShortNode.class, JsonShortNode);
        scalarMapping.put(DecimalNode.class, JsonDecimalNode);
        scalarMapping.put(FloatNode.class, JsonFloatNode);
        scalarMapping.put(DoubleNode.class, JsonDoubleNode);
        scalarMapping.put(NumericNode.class, JsonDecimalNode);
        return Collections.unmodifiableMap(scalarMapping);
    }
}