package nl.astraeus.database;

import nl.astraeus.database.annotations.Cache;
import nl.astraeus.database.annotations.Id;
import nl.astraeus.database.annotations.Table;
import nl.astraeus.database.sql.TemplateHandler;
import nl.astraeus.template.SimpleTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.*;
import java.util.Date;

/**
 * Date: 11/13/13
 * Time: 9:41 PM
 */
public class MetaData<T> {
    private final static Logger logger = LoggerFactory.getLogger(MetaData.class);

    private Class<T> cls;
    private String tableName;
    private FieldMetaData pk = null;
    private FieldMetaData [] fieldsMetaData;
    private String insertSql;
    private String selectSql;
    private String updateSql;
    private String deleteSql;

    public MetaData(Class<T> cls) {
        this.cls = cls;

        processAnnotation(cls.getAnnotation(Table.class));

        if (tableName == null) {
            tableName = cls.getSimpleName();
        }

        Cache cache = cls.getAnnotation(Cache.class);

        if (cache != null) {
            nl.astraeus.database.cache.Cache.get().setMaxSize(cls, cache.maxSize());
            nl.astraeus.database.cache.Cache.get().setMaxAge(cls, cache.maxAge());
        }

        Field [] fields = cls.getDeclaredFields();
        List<FieldMetaData> fieldMeta = new ArrayList<>();

        for (Field field : fields) {
            if (!field.getName().contains("jacoco")) {
                FieldMetaData info = new FieldMetaData(field);

                fieldMeta.add(info);

                if (field.getAnnotation(Id.class) != null) {
                    if (!field.getType().equals(Long.class) &&
                        !field.getType().equals(long.class) &&
                        !field.getType().equals(Integer.class) &&
                        !field.getType().equals(int.class)) {

                        throw new IllegalStateException("PK Field must be long or integer! ("+field.getDeclaringClass().getSimpleName()+"."+field.getName()+")");
                    } else if (pk != null) {
                        throw new IllegalStateException("Compound primary keys not supported, multable id field defined in "+field.getDeclaringClass().getSimpleName());
                    }

                    pk = info;
                }
            }
        }

        this.fieldsMetaData = fieldMeta.toArray(new FieldMetaData[fieldMeta.size()]);

        Connection connection = null;
        try {
            // get metadata from database
            connection = Persister.getNewConnection();
            ResultSet result = connection.getMetaData().getTables(null, null, tableName.toUpperCase(), null);

            if (result.next()) {
                for (FieldMetaData meta : fieldsMetaData) {
                    ResultSet columnMetaData = connection.getMetaData().getColumns(null, null, tableName.toUpperCase(), meta.getColumnInfo().getName().toUpperCase());

                    if (columnMetaData.next()) {
                        if (meta.isPrimaryKey()) {

                        }
                        // check type etc
                        // warn if different
                    } else {
                        // create Column....
                        createColumn(meta);
                    }
                }
            } else {
                createTable();
            }

            SimpleTemplate insertTemplate = TemplateHandler.get().getInsertTemplate();
            SimpleTemplate selectTemplate = TemplateHandler.get().getSelectTemplate();
            SimpleTemplate updateTemplate = TemplateHandler.get().getUpdateTemplate();
            SimpleTemplate deleteTemplate = TemplateHandler.get().getDeleteTemplate();

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

            List<String> columns = new LinkedList<>();

            for (FieldMetaData meta : fieldsMetaData) {
                if (!meta.isPrimaryKey()) {
                    columns.add(meta.getColumnInfo().getName());
                }
            }

            model.put("tableName", tableName);
            model.put("columns", columns);
            model.put("key", pk.getColumnInfo().getName());

            insertSql = insertTemplate.render(model);
            updateSql = updateTemplate.render(model);
            deleteSql = deleteTemplate.render(model);

            columns.add(0, pk.getColumnInfo().getName());
            selectSql = selectTemplate.render(model);

            connection.commit();
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    private void processAnnotation(Table table) {
        if (table != null && !table.name().isEmpty() ) {
            tableName = table.name();
        }
    }

    private void createTable() {
        Map<String, Object> model = new HashMap<>();

        List<ColumnInfo> columns = new ArrayList<>();

        for (FieldMetaData meta : fieldsMetaData) {
            if (!meta.isPrimaryKey()) {
                columns.add(meta.getColumnInfo());
            }
        }

        model.put("tableName", tableName);
        model.put("columns", columns);
        model.put("key", pk.getColumnInfo().getName());

        SimpleTemplate template = TemplateHandler.get().getCreateTemplate();

        execute(template, model);
     }

    private void createColumn(FieldMetaData meta) {
        Map<String, Object> model = new HashMap<>();

        model.put("tableName", tableName);
        model.put("column", meta.getColumnInfo());

        SimpleTemplate template = TemplateHandler.get().getCreateColumnTemplate();

        execute(template, model);
    }

    private void execute(SimpleTemplate createTemplate, Map<String, Object> model) {
        Connection connection = null;
        PreparedStatement statement = null;

        try {
            connection = Persister.getNewConnection();

            String sql = createTemplate.render(model);
            logger.info("Executing:\n" + sql);
            statement = connection.prepareStatement(sql);

            statement.execute();
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            try {
                if (statement != null) {
                    statement.close();
                }
            } catch (SQLException e) {
                throw new IllegalStateException(e);
            } finally {
                try {
                if (connection != null) {
                    connection.close();
                }
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    protected <T> T find(final Long id) {
        return executeInNewConnection(new ExecuteConnectionWithResult<T>() {
            @Override
            public T execute(Connection connection) throws SQLException {
                PreparedStatement statement = null;

                try {
                    T result = null;
                    statement = connection.prepareStatement(selectSql);

                    statement.setLong(1, id);

                    ResultSet rs = statement.executeQuery();

                    if (rs.next()) {
                        result = (T)cls.newInstance();
                        int index = 1;

                        for (FieldMetaData meta : fieldsMetaData) {
                            meta.set(rs, index++, result);
                        }
                    }

                    return result;
                } catch (InstantiationException e) {
                    throw new IllegalStateException(e);
                } catch (IllegalAccessException e) {
                    throw new IllegalStateException(e);
                } finally {
                    if (statement != null) {
                        statement.close();
                    }
                }
            }
        });
    }

    protected <T> void insert(T object) {
        PreparedStatement statement = null;

        try {
            statement = Persister.getConnection().prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS);
            int index = 1;

            for (FieldMetaData meta : fieldsMetaData) {
                if (!meta.isPrimaryKey()) {
                    meta.set(statement, index++, object);
                }
            }

            statement.execute();

            ResultSet rs = statement.getGeneratedKeys();

            if (rs != null && rs.next()) {
                pk.set(object, rs.getLong(1));
            }
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    protected <T> void update(T object) {
        PreparedStatement statement = null;

        try {
            statement = Persister.getConnection().prepareStatement(updateSql);
            int index = 1;

            for (FieldMetaData meta : fieldsMetaData) {
                if (!meta.isPrimaryKey()) {
                    meta.set(statement, index++, object);
                }
            }

            pk.set(statement, index++, object);

            statement.execute();
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    public void delete(Long id) {
        PreparedStatement statement = null;

        try {
            statement = Persister.getConnection().prepareStatement(deleteSql);

            statement.setLong(1, id);

            statement.execute();
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    public <T> List<T> selectFrom(String query, final Object[] params) {
        List<T> result;
        SimpleTemplate fromTemplate = TemplateHandler.get().getSelectFromTemplate();

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

        model.put("tableName", tableName);
        model.put("key", pk.getColumnInfo().getName());
        model.put("query", query);

        final String fromSql = fromTemplate.render(model);

        result = executeInNewConnection(new ExecuteConnectionWithResult<List<T>>() {
            @Override
            public List<T> execute(Connection connection) throws SQLException {
                List<T> result = new ArrayList<>();
                PreparedStatement statement = null;

                try {
                    statement = connection.prepareStatement(fromSql);
                    int index = 1;

                    for (Object param : params) {
                        setStatementParameter(statement, index++, param);
                    }

                    ResultSet rs = statement.executeQuery();

                    while (rs.next()) {
                        Long id = rs.getLong(1);

                        result.add((T) Persister.find(cls, id));
                    }

                    return result;
                } finally {
                    if (statement != null) {
                        statement.close();
                    }
                }
            }
        });

        return result;
    }

    public <T> List<T> selectWhere(String query, final Object[] params) {
        List<T> result;
        SimpleTemplate whereTemplate = TemplateHandler.get().getSelectWhereTemplate();

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

        model.put("tableName", tableName);
        model.put("key", pk.getColumnInfo().getName());
        model.put("query", query);

        final String whereSql = whereTemplate.render(model);

        result = executeInNewConnection(new ExecuteConnectionWithResult<List<T>>() {
            @Override
            public List<T> execute(Connection connection) throws SQLException {
                List<T> result = new ArrayList<>();
                PreparedStatement statement = null;

                try {
                    statement = connection.prepareStatement(whereSql);
                    int index = 1;

                    for (Object param : params) {
                        setStatementParameter(statement, index++, param);
                    }

                    ResultSet rs = statement.executeQuery();

                    while (rs.next()) {
                        Long id = rs.getLong(1);

                        result.add((T) Persister.find(cls, id));
                    }

                    return result;
                } finally {
                    if (statement != null) {
                        statement.close();
                    }
                }
            }
        });

        return result;
    }

    private void setStatementParameter(PreparedStatement statement, int index, Object param) throws SQLException {
        if (param.getClass().equals(String.class)) {
            statement.setString(index, (String) param);
        } else if (param.getClass().equals(Long.class) || param.getClass().equals(long.class)) {
            statement.setLong(index, (Long) param);
        } else if (param.getClass().equals(Integer.class) || param.getClass().equals(int.class)) {
            statement.setInt(index, (Integer) param);
        } else if (param.getClass().equals(Short.class) || param.getClass().equals(short.class)) {
            statement.setShort(index, (Short)param);
        } else if (param.getClass().equals(Double.class) || param.getClass().equals(double.class)) {
            statement.setDouble(index, (Double)param);
        } else if (param.getClass().equals(Date.class)) {
            java.sql.Date date = new java.sql.Date(((Date)param).getTime());

            statement.setDate(index, date);
        } else {
            throw new IllegalStateException("Type "+param.getClass()+" not supported in where queries yet!");
        }
    }

    private void executeInNewConnection(ExecuteConnection es) {
        Connection connection = null;

        try {
            connection = Persister.getNewConnection();

            es.execute(connection);
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    private <T> T executeInNewConnection(ExecuteConnectionWithResult<T> es) {
        Connection connection = null;

        try {
            connection = Persister.getNewConnection();

            return es.execute(connection);
        } catch (SQLException e) {
            throw new IllegalStateException(e);
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (SQLException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    public Long getId(Object object) {
        return (Long)pk.get(object);
    }

    public <T> void reloadReferences(T result) {
        for (FieldMetaData field : fieldsMetaData) {
            if (field.getType() == FieldMetaData.ColumnType.REFERENCE) {
                field.reloadReference(result);
            }
        }
    }
}
