package org.aegee.runanddine.util.model;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.sql.*;
import java.util.LinkedList;
import java.util.List;

import org.aegee.runanddine.util.data.ConnectionManager;
import org.aegee.runanddine.util.data.ConnectionNotExistingException;
import org.aegee.runanddine.util.data.ModelNotExistingException;

/**
 * Manages persistent RunAndDine models
 * and implements most of the common functionality
 * necessary for handling persistent objects
 */
public abstract class MySQLModelManager<T extends Model> implements ModelManager<T> {

    /**
     * Table name
     */
    protected String table = null;
    
    /**
     * SQL connection
     */
    protected Connection con = null;

    /**
     * Init model manager (protected because of singleton pattern)
     */
    protected MySQLModelManager() {
        try {
            this.con = ConnectionManager.getConnection("mysql");
        } catch (ConnectionNotExistingException e) {
            this.con = null;
        }
    }

    /**
     * Get Model by ID
     *
     * @param id ID of desired model
     * @return Model identified by ID
     * @throws ModelNotExistingException Gets thrown if the desired model does not exists persistently
     */
    public T getById(int id) throws ModelNotExistingException {
        try {
            // prepare statement
            PreparedStatement stmt = this.con.prepareStatement("SELECT serialized_object FROM " + this.table + " WHERE id=?");
            // bind id
            stmt.setInt(1, id);
            // retrieve model
            ResultSet rs = stmt.executeQuery();
            // check if model exists
            if (rs.next()) {
                // deserialize model
                T model = this.readObject(rs.getBytes("serialized_object"));
                model.setId(id);
                rs.close();
                stmt.close();

                return model;
            } else {
                throw new ModelNotExistingException();
            }
        } catch (SQLException e) {
            e.printStackTrace(System.err);
            return null;
        }
    }

    /**
     * Deserialize model
     *
     * @param buf Serialized object
     * @return Deserialized object
     */
    protected T readObject(byte[] buf) {
        try {
            ObjectInputStream objectIn = null;

            if (buf != null) {
                objectIn = new ObjectInputStream(new ByteArrayInputStream(buf));
            }

            T model = (T) objectIn.readObject();

            return model;
        } catch (Exception e) {
            e.printStackTrace(System.err);
            return null;
        }
    }

    /**
     * Get list of all existing models
     *
     * @return List of all existing models
     */
    public List<T> getAll() {
        try {
            List<T> models = new LinkedList<T>();
            // select all models
            PreparedStatement stmt = this.con.prepareStatement("SELECT serialized_object, id FROM " + this.table);
            // retrieve models
            ResultSet rs = stmt.executeQuery();
            while (rs.next()) {
                // deserialize object
                T m = this.readObject(rs.getBytes("serialized_object"));
                // set id as serialized id is invalid @see MySQLModelManager#save()
                m.setId(rs.getInt("id"));
                // add to result list
                models.add(m);
            }

            rs.close();
            stmt.close();

            return models;
        } catch (SQLException e) {
            e.printStackTrace(System.err);
            return null;
        }
    }

    /**
     * Stores new model or updates existing one
     *
     * @param model Model to be saved/updated
     */
    public void save(T model) {
        try {
            // update model
            if (this.hasModel(model.getId())) {
                PreparedStatement stmt = this.con.prepareStatement("UPDATE " + this.table + " SET serialized_object=? WHERE id=?");
                // bind value
                stmt.setObject(1, model);
                stmt.setInt(2, model.getId());
                // execute update
                stmt.executeUpdate();
                stmt.close();
                // save new model
            } else {
                PreparedStatement stmt = this.con.prepareStatement("INSERT INTO " + this.table + " (serialized_object) VALUES (?)");
                // bind values to statement
                stmt.setObject(1, model);
                // execute insert
                stmt.executeUpdate();
                // retrieve auto increment id
                ResultSet rs = stmt.getGeneratedKeys();
                rs.next();
                model.setId(rs.getInt(1));
                rs.close();
                stmt.close();
            }
        } catch (SQLException e) {
            e.printStackTrace(System.err);
        }
    }

    /**
     * Deletes model (if existing)
     *
     * @param model Model to be deleted
     */
    public void delete(T model) {
        try {
            PreparedStatement stmt = this.con.prepareStatement("DELETE FROM " + this.table + " WHERE id=?");
            // bind ID
            stmt.setInt(1, model.getId());
            // delete entry
            stmt.executeUpdate();
            stmt.close();
        } catch (SQLException e) {
            e.printStackTrace(System.err);
        }
    }

    /**
     * Check if there is a persistent representation of the model identified by
     * the ID
     *
     * @param id ID of the model to be checked
     * @return Model is persistent
     */
    protected boolean hasModel(int id) {
        try {
            PreparedStatement stmt = this.con.prepareStatement("SELECT id FROM " + this.table + " WHERE id=?");
            // bind ID
            stmt.setInt(1, id);
            // search for model
            ResultSet rs = stmt.executeQuery();
            boolean hasModel = rs.next();
            rs.close();
            stmt.close();

            return hasModel;
        } catch (SQLException e) {
            e.printStackTrace(System.err);
            return false;
        }
    }
}