package nl.astraeus.database;

import nl.astraeus.database.annotations.*;
import nl.astraeus.template.EscapeMode;
import nl.astraeus.template.SimpleTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;

/**
 * Date: 11/14/13
 * Time: 8:59 PM
 */
public class FieldMetaData {
    private final static Logger logger = LoggerFactory.getLogger(FieldMetaData.class);

    public static enum ColumnType {
        BASIC,
        COLLECTION,
        SERIALIZED,
        REFERENCE;
    }

    private Field field;
    private String fieldName;

    private ColumnType type;
    private Class<?> javaType;
    private Integer sqlType;

    private ColumnInfo  columnInfo;

    private Length length;
    private Default defaultValue;
    private Class<?> collectionClass;

    private boolean primaryKey = false;

    private static Map<Class<?>, SimpleTemplate> ddlMapping;
    private static Map<Class<?>, Integer> sqlTypeMapping;

//    java.lang.Boolean BOOLEAN
//    java.lang.Byte TINYINT
//    java.lang.BigDecinal DECIMAL(${precision}, ${scale})

    // todo: get these definitions from some configuration file
    static {
        ddlMapping = new HashMap<>();
        sqlTypeMapping = new HashMap<>();

        ddlMapping.put(String.class, new SimpleTemplate("${", "}", EscapeMode.NONE, "VARCHAR(${length})"));
        ddlMapping.put(Long.class, new SimpleTemplate("${", "}", EscapeMode.NONE, "BIGINT"));
        ddlMapping.put(long.class, new SimpleTemplate("${", "}", EscapeMode.NONE, "BIGINT"));
        ddlMapping.put(Integer.class, new SimpleTemplate("${", "}", EscapeMode.NONE, "INT"));
        ddlMapping.put(int.class, new SimpleTemplate("${", "}", EscapeMode.NONE, "INT"));
        ddlMapping.put(Short.class, new SimpleTemplate("${", "}", EscapeMode.NONE, "SMALLINT"));
        ddlMapping.put(short.class, new SimpleTemplate("${", "}", EscapeMode.NONE, "SMALLINT"));
        ddlMapping.put(Double.class, new SimpleTemplate("${", "}", EscapeMode.NONE, "DECIMAL(${precision}, ${scale})"));
        ddlMapping.put(double.class, new SimpleTemplate("${", "}", EscapeMode.NONE, "DECIMAL(${precision}, ${scale})"));

        sqlTypeMapping.put(String.class, Types.VARCHAR);
        sqlTypeMapping.put(Long.class, Types.BIGINT);
        sqlTypeMapping.put(long.class, Types.BIGINT);
        sqlTypeMapping.put(Integer.class, Types.INTEGER);
        sqlTypeMapping.put(int.class, Types.INTEGER);
        sqlTypeMapping.put(Short.class, Types.SMALLINT);
        sqlTypeMapping.put(short.class, Types.SMALLINT);
        sqlTypeMapping.put(Double.class, Types.DECIMAL);
        sqlTypeMapping.put(double.class, Types.DECIMAL);
    }

    public FieldMetaData(Field field) {
        this.field = field;
        this.field.setAccessible(true);

        fieldName = field.getName();
        String columnName = fieldName;
        javaType = field.getType();
        type = ColumnType.BASIC;

        length = field.getAnnotation(Length.class);
        defaultValue = field.getAnnotation(Default.class);
        Column column = field.getAnnotation(Column.class);
        Id id = field.getAnnotation(Id.class);

        if (column != null) {
            columnName = column.name();
        }

        if (id != null) {
            primaryKey = true;
            field.setAccessible(true);
        }

        Map<String, Object> model = new HashMap<>();

        if (length == null) {
            model.put("length", 255);
            model.put("precision", 10);
            model.put("scale", 2);
        } else {
            model.put("length", length.value());
            model.put("precision", length.precision());
            model.put("scale", length.scale());
        }

        SimpleTemplate template = ddlMapping.get(javaType);
        sqlType = sqlTypeMapping.get(javaType);

        String type = "BIGINT"; // default to id ref

        if (template != null) {
            type = template.render(model);
        } else {
            Collection collection = field.getAnnotation(Collection.class);

            if (collection != null) {
                collectionClass = collection.value();
                this.type = ColumnType.COLLECTION;

                // BLOB
                type = "BLOB";
                sqlType = Types.BLOB;
            } else {
                Serialized serialized = field.getAnnotation(Serialized.class);

                if (serialized != null) {
                    // BLOB
                    type = "BLOB";
                    sqlType = Types.BLOB;
                    this.type = ColumnType.SERIALIZED;
                } else if (javaType.getAnnotation(Table.class) != null) {
                    // oneToone
                    type = "BIGINT";
                    sqlType = Types.BIGINT;
                    this.type = ColumnType.REFERENCE;
                } else {
                    throw new IllegalStateException("Type "+field.getType().getSimpleName()+" of field "+field.getDeclaringClass().getSimpleName()+"."+field.getName()+" is not supported!");
                }
            }
        }

        columnInfo = new ColumnInfo(columnName, type);
    }

    public boolean isPrimaryKey() {
        return primaryKey;
    }

    public ColumnType getType() {
        return type;
    }

    public Field getField() {
        return field;
    }

    public String getFieldName() {
        return fieldName;
    }

    public Class getJavaType() {
        return javaType;
    }

    public ColumnInfo getColumnInfo() {
        return columnInfo;
    }

    public Length getLength() {
        return length;
    }

    public Default getDefaultValue() {
        return defaultValue;
    }

    public Object get(Object obj) {
        try {
            return field.get(obj);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    public void set(Object obj, Object value) {
        try {
            field.set(obj, value);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    public void set(PreparedStatement statement, int index, Object obj) throws SQLException {
        Object value = get(obj);
        MetaData metaData = null;
        Long id = null;

        if (value == null) {
            switch(type) {
                case BASIC:
                    statement.setNull(index, sqlType);
                    break;
                case REFERENCE:
                    statement.setNull(index, Types.BIGINT);
                    break;
                case COLLECTION:
                    statement.setNull(index, Types.BLOB);
                    break;

            }
        } else {
            switch(type) {
                case BASIC:
                    switch(sqlType) {
                        case Types.VARCHAR:
                            statement.setString(index, (String)value);
                            break;
                        case Types.BIGINT:
                            statement.setLong(index, (Long) value);
                            break;
                        case Types.INTEGER:
                            statement.setInt(index, (Integer) value);
                            break;
                        case Types.SMALLINT:
                            statement.setShort(index, (Short) value);
                            break;
                        case Types.DECIMAL:
                            statement.setDouble(index, (Double) value);
                            break;
                    }
                    break;
                case REFERENCE:
                    metaData = MetaDataHandler.get().getMetaData(value.getClass());

                    id = metaData.getId(value);

                    if (id == null) {
                        Persister.insert(value);

                        id = metaData.getId(value);
                    }

                    statement.setLong(index, id);
                    break;
                case COLLECTION:
                    metaData = MetaDataHandler.get().getMetaData(collectionClass);
                    java.util.Collection c = (java.util.Collection)value;
                    ByteBuffer buffer = ByteBuffer.allocate(c.size() * 8);
                    for (Object o : c) {
                        id = metaData.getId(o);

                        if (id == null || id == 0) {
                            Persister.insert(o);

                            id = metaData.getId(o);
                        }

                        buffer.putLong(id);
                    }

                    statement.setBlob(index, new ByteArrayInputStream(buffer.array()));
                    break;
            }
        }
    }

    public void set(ResultSet rs, int index, Object obj) throws SQLException {
        Long id;

        switch(type) {
            case BASIC:
                switch(sqlType) {
                    case Types.VARCHAR:
                        set(obj, rs.getString(index));
                        break;
                    case Types.BIGINT:
                        set(obj, rs.getLong(index));
                        break;
                    case Types.INTEGER:
                        set(obj, rs.getInt(index));
                        break;
                    case Types.SMALLINT:
                        set(obj, rs.getShort(index));
                        break;
                    case Types.DECIMAL:
                        set(obj, rs.getDouble(index));
                        break;
                }
                break;
            case REFERENCE:
                id = rs.getLong(index);

                Object object = Persister.find(javaType, id);

                if (object == null) {
                    logger.warn("Missing reference detected "+field.getDeclaringClass().getSimpleName()+"."+getFieldName()+":"+id);
                }

                set(obj, object);
                break;
            case COLLECTION:
                Blob blob = rs.getBlob(index);
                MetaData meta = MetaDataHandler.get().getMetaData(collectionClass);
                ReferentList list = new ReferentList(collectionClass, meta);

                ByteBuffer buffer = ByteBuffer.wrap(blob.getBytes(0, (int) blob.length()));

                while(buffer.hasRemaining()) {
                    id = buffer.getLong();

                    list.addId(id);
                }

                set(obj, list);
                break;
        }

    }

    public <T> void reloadReference(T result) {
        Object current = get(result);

        if (current != null) {
            MetaData meta = MetaDataHandler.get().getMetaData(current.getClass());

            Long id = meta.getId(current);

            Object object = Persister.find(javaType, id);

            if (object == null) {
                logger.warn("Missing reference detected "+field.getDeclaringClass().getSimpleName()+"."+getFieldName()+":"+id);
            }

            set(result, object);
        }
    }

}
