package org.pac4j.vertx.core;

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.scribejava.core.model.OAuth1RequestToken;
import com.github.scribejava.core.model.Token;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.Scope.Value.Requirement;
import com.nimbusds.oauth2.sdk.token.AccessTokenType;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.pac4j.core.exception.TechnicalException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * <p>Default eventbus object converter</p>
 * <p>The serialization strategy is:</p>
 * <ul>
 * <li>For primitive types (String, Number and Boolean), return as is</li>
 * <li>For arrays, convert to JsonArray</li>
 * <li>Otherwise, convert to a JsonObject with the class name in the "class" attribute and the serialized form with Jackson in the "value" attribute.
 * The (de)serialization Jackson process can be customized using the <code>addMixIn(target, mixinSource)</code> method</li>
 * </ul>
 * 
 * @author Michael Remond
 * @since 1.1.0
 *
 */
public class DefaultJsonConverter implements JsonConverter {

    private final ObjectMapper mapper = new ObjectMapper();
    private static final DefaultJsonConverter INSTANCE = new DefaultJsonConverter();

    public static JsonConverter getInstance() {
        return INSTANCE;
    }

    public DefaultJsonConverter() {
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        mapper.setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE);
        mapper.setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE);

        mapper.addMixIn(OAuth1RequestToken.class, OAuth1RequestTokenMixin.class)
            .addMixIn(BearerAccessToken.class, BearerAccessTokenMixin.class)
            .addMixIn(Scope.Value.class, ValueMixin.class)
            .addMixIn(Token.class, TokenMixin.class);
    }

    @Override
    public Object encodeObject(Object value) {
        if (value == null) {
            return null;
        } else if (isPrimitiveType(value)) {
            return value;
        } else if (value instanceof Object[]) {
            Object[] src = ((Object[]) value);
            List<Object> list = new ArrayList<>(src.length);
            fillEncodedList(src, list);
            return new JsonArray(list);
        } else {
            try {
                return new JsonObject().put("class", value.getClass().getName()).put("value",
                        new JsonObject(encode(value)));
            } catch (Exception e) {
                throw new TechnicalException("Error while encoding object", e);
            }
        }
    }

    private void fillEncodedList(Object[] src, List<Object> list) {
        for (Object object : src) {
            list.add(encodeObject(object));
        }
    }

    @Override
    public Object decodeObject(Object value) {
        if (value == null) {
            return null;
        } else if (isPrimitiveType(value)) {
            return value;
        } else if (value instanceof JsonArray) {
            JsonArray src = (JsonArray) value;
            List<Object> list = new ArrayList<>(src.size());
            fillDecodedList(src, list);
            return list.toArray();
        } else if (value instanceof JsonObject) {
            JsonObject src = (JsonObject) value;
            return decode(src);
        }
        return null;
    }

    private Object decode(JsonObject src) {
        try {
            return decode(src.getJsonObject("value").encode(), Class.forName(src.getString("class")));
        } catch (Exception e) {
            throw new TechnicalException("Error while decoding object", e);
        }
    }

    private void fillDecodedList(JsonArray src, List<Object> list) {
        for (Object object : src) {
            list.add(decodeObject(object));
        }
    }

    private boolean isPrimitiveType(Object value) {
        return value instanceof String || value instanceof Number || value instanceof Boolean;
    }

    private String encode(Object value) throws JsonGenerationException, JsonMappingException, IOException {
        return mapper.writeValueAsString(value);
    }

    @SuppressWarnings("unchecked")
    private <T> T decode(String string, Class<?> clazz) throws JsonParseException, JsonMappingException, IOException {
        return (T) mapper.readValue(string, clazz);
    }

    public static class BearerAccessTokenMixin {
        @JsonIgnore
        private AccessTokenType type;

        @JsonCreator
        public BearerAccessTokenMixin(@JsonProperty("value") String value, @JsonProperty("lifetime") long lifetime,
                @JsonProperty("scope") Scope scope) {
        }
    }

    public static class ValueMixin {
        @JsonCreator
        public ValueMixin(@JsonProperty("value") String value, @JsonProperty("requirement") Requirement requirement) {
        }
    }

    public static class TokenMixin {
        @JsonCreator
        public TokenMixin(@JsonProperty("token") String token, @JsonProperty("secret") String secret,
                @JsonProperty("rawResponse") String rawResponse) {
        }
    }

    public static class OAuth1RequestTokenMixin {
        @JsonCreator
        public OAuth1RequestTokenMixin(@JsonProperty("token") String token,
                                       @JsonProperty("tokenSecret") String tokenSecret,
                                       @JsonProperty("oauthCallbackConfirmed") boolean oauthCallbackConfirmed,
                                       @JsonProperty("rawResponse") String RawResponse) {

        }
    }

}