package com.avaje.ebeaninternal.server.type;

import com.avaje.ebean.config.dbplatform.DbPlatformType;
import com.avaje.ebeanservice.docstore.api.mapping.DocPropertyType;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.persistence.PersistenceException;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.lang.reflect.Type;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Supports @DbJson properties using Jackson ObjectMapper.
 */
public class ScalarTypeJsonObjectMapper {

  /**
   * Create and return the appropriate ScalarType.
   */
  public static ScalarType<?> createTypeFor(boolean postgres, Class<?> type, ObjectMapper objectMapper,
                                            Type genericType, int dbType, DocPropertyType docType) {

    String pgType = getPostgresType(postgres, dbType);
    if (Set.class.equals(type)) {
      return new OmSet(objectMapper, genericType, dbType, pgType, docType);
    }
    if (List.class.equals(type)) {
      return new OmList(objectMapper, genericType, dbType, pgType, docType);
    }
    if (Map.class.equals(type)) {
      return new OmMap(objectMapper, genericType, dbType, pgType);
    }
    return new GenericObject(objectMapper, genericType, dbType, pgType);
  }

  private static String getPostgresType(boolean postgres, int dbType) {
    if (postgres) {
      switch (dbType) {
        case DbPlatformType.JSON:
          return PostgresHelper.JSON_TYPE;
        case DbPlatformType.JSONB:
          return PostgresHelper.JSONB_TYPE;
      }
    }
    return null;
  }

  /**
   * Maps any type (Object) using Jackson ObjectMapper.
   */
  private static class GenericObject extends Base<Object> {

    public GenericObject(ObjectMapper objectMapper, Type type, int dbType, String pgType) {
      super(Object.class, objectMapper, type, dbType, pgType, DocPropertyType.OBJECT);
    }
  }

  /**
   * Type for Sets wrapping the ObjectMapper Set as a ModifyAwareSet.
   */
  private static class OmSet extends Base<Set> {

    public OmSet(ObjectMapper objectMapper, Type type, int dbType, String pgType, DocPropertyType docType) {
      super(Set.class, objectMapper, type, dbType, pgType, docType);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Set read(DataReader reader) throws SQLException {
      Set value = super.read(reader);
      return value == null ? null : new ModifyAwareSet(value);
    }
  }

  /**
   * Type for Lists wrapping the ObjectMapper List as a ModifyAwareList.
   */
  private static class OmList extends Base<List> {

    public OmList(ObjectMapper objectMapper, Type type, int dbType, String pgType, DocPropertyType docType) {
      super(List.class, objectMapper, type, dbType, pgType, docType);
    }

    @Override
    @SuppressWarnings("unchecked")
    public List read(DataReader reader) throws SQLException {
      List value = super.read(reader);
      return value == null ? null : new ModifyAwareList(value);
    }
  }

  /**
   * Type for Map wrapping the ObjectMapper Map as a ModifyAwareMap.
   */
  private static class OmMap extends Base<Map> {

    public OmMap(ObjectMapper objectMapper, Type type, int dbType, String pgType) {
      super(Map.class, objectMapper, type, dbType, pgType, DocPropertyType.OBJECT);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Map read(DataReader reader) throws SQLException {
      Map value = super.read(reader);
      return value == null ? null : new ModifyAwareMap(value);
    }
  }

  /**
   * ScalarType that uses Jackson ObjectMapper to marshall/unmarshall to/from JSON
   * and storing them in one of JSON, JSONB, VARCHAR, CLOB or BLOB.
   */
  private static abstract class Base<T> extends ScalarTypeBase<T> {

    private final ObjectMapper objectMapper;

    private final JavaType javaType;

    private final String pgType;

    private final DocPropertyType docType;

    /**
     * Construct given the object mapper, property type and DB type for storage.
     */
    public Base(Class<T> cls, ObjectMapper objectMapper, Type type, int dbType, String pgType, DocPropertyType docType) {
      super(cls, false, dbType);
      this.pgType = pgType;
      this.docType = docType;
      this.objectMapper = objectMapper;
      this.javaType = objectMapper.getTypeFactory().constructType(type);
    }

    /**
     * Consider as a mutable type. Use the isDirty() method to check for dirty state.
     */
    @Override
    public boolean isMutable() {
      return true;
    }

    /**
     * Return true if the value should be considered dirty (and included in an update).
     */
    @Override
    public boolean isDirty(Object value) {
      return !(value instanceof ModifyAwareOwner) || ((ModifyAwareOwner) value).isMarkedDirty();
    }

    @Override
    public T read(DataReader reader) throws SQLException {
      String json = reader.getString();
      if (json == null || json.isEmpty()) {
        return null;
      }
      try {
        return objectMapper.readValue(json, javaType);
      } catch (IOException e) {
        throw new SQLException("Unable to convert JSON", e);
      }
    }

    @Override
    public void bind(DataBind bind, T value) throws SQLException {
      if (pgType != null) {
        String rawJson = (value == null) ? null : formatValue(value);
        bind.setObject(PostgresHelper.asObject(pgType, rawJson));
      } else {
        if (value == null) {
          bind.setNull(jdbcType);
        } else {
          try {
            String json = objectMapper.writeValueAsString(value);
            bind.setString(json);
          } catch (JsonProcessingException e) {
            throw new SQLException("Unable to create JSON", e);
          }
        }
      }
    }

    @Override
    public Object toJdbcType(Object value) {
      // no type conversion supported
      return value;
    }

    @Override
    @SuppressWarnings("unchecked")
    public T toBeanType(Object value) {
      // no type conversion supported
      return (T) value;
    }

    @Override
    public String formatValue(T value) {
      try {
        return objectMapper.writeValueAsString(value);
      } catch (JsonProcessingException e) {
        throw new PersistenceException("Unable to create JSON", e);
      }
    }

    @Override
    public T parse(String value) {
      try {
        return objectMapper.readValue(value, javaType);
      } catch (IOException e) {
        throw new PersistenceException("Unable to convert JSON", e);
      }
    }

    @Override
    public DocPropertyType getDocType() {
      return docType;
    }

    @Override
    public boolean isDateTimeCapable() {
      return false;
    }

    @Override
    public T convertFromMillis(long dateTime) {
      throw new IllegalStateException("Not supported");
    }

    @Override
    public T jsonRead(JsonParser parser) throws IOException {
      return objectMapper.readValue(parser, javaType);
    }

    @Override
    public void jsonWrite(JsonGenerator writer, T value) throws IOException {
      objectMapper.writeValue(writer, value);
    }

    @Override
    public T readData(DataInput dataInput) throws IOException {
      if (!dataInput.readBoolean()) {
        return null;
      } else {
        return parse(dataInput.readUTF());
      }
    }

    @Override
    public void writeData(DataOutput dataOutput, T value) throws IOException {
      if (value == null) {
        dataOutput.writeBoolean(false);
      } else {
        ScalarHelp.writeUTF(dataOutput, format(value));
      }
    }
  }
}
