package db;

import java.util.List;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import db.annotation.DbField;
import db.annotation.DbId;
import db.annotation.DbLink;
import db.annotation.DbTable;
import db.mapper.DatabaseMapper;

public abstract class DatabaseTable<T extends Persistable> {
	
	private Map<Long, T> cacheMap = new HashMap<>();
	
	public T get(T obj) {
		return getById(obj.getId());
	}
	
	public Optional<T> find(T obj) {
		return findById(obj.getId());
	}
	
	public T getById(long id) {
		if (cacheMap.containsKey(id)) {
			return cacheMap.get(id);
		}
		Optional<T> opt = getFromDbById(id);
		if(opt.isPresent()) {
			return cache(opt.get());
		}
		return null;
	}
	
	public Optional<T> findById(long id) {
		T obj = getById(id);
		if (obj == null) {
			return Optional.empty();
		}
		return Optional.of(obj);
	}
	
	public List<T> getByField(String fieldname, Object value) {
		List<T> list = getFromDbByField(fieldname, value);
		list.forEach(elt -> cache(elt));
		return list;
	}
	
	public List<T> getWhere(List<String> fields, List<Object> values) {
		List<T> list = getWhereFromDb(fields, values);
		list.forEach(elt -> cache(elt));
		return list;
	}
	
	public List<T> getAll() {
		List<T> list = getAllFromDb();
		list.forEach(elt -> cache(elt));
		return list;
	}
	
	public T refresh(long id) {
		if (!cacheMap.containsKey(id)) {
			throw new IllegalArgumentException("Entity is not load, can't refresh");
		}
		Optional<T> optObj = getFromDbById(id);
		if(!optObj.isPresent()) {
			throw new IllegalStateException("Unable to find entity in the database");
		}
		return cache(optObj.get());
	}
	
	public T refresh(T obj) {
		return refresh(obj.getId());
	}
	
	public T save(T obj) {
		if (cacheMap.containsKey(obj.getId())) {
			update(obj);
		} else {
			insert(obj);
		}
		return cache(obj);
	}
	
	public void delete(long id) {
		DbTable dbTable = getDbTableAnnotation();
		// Cherche l'id
		String idField = null;
		for(Field field : dbTable.entity().getFields()) {
			if(!field.isAnnotationPresent(DbId.class)) {
				continue;
			}
			// Lecture du nom de l'id dans la base
			if(field.isAnnotationPresent(DbField.class)) {
				idField = field.getAnnotation(DbField.class).value();
			} else {
				throw new IllegalStateException("Unable to find DbField annotation on id");
			}
		}
		if (idField == null) {
			throw new IllegalStateException("Unable to find find DbField annotation");
		}
		// Requete SQL
		String sql = "Delete From " + dbTable.name() + " Where " + idField + " = ?";
		List<Object> params = new ArrayList<>();
		params.add(id);
		// Execution requete
		if (!Database.execute(sql, params)) {
			throw new IllegalStateException("Unable to delete data");
		}
		// Retire du cache
		remove(id);
	}
	
	public void delete(T obj) {
		delete(obj.getId());
	}
	
	private T cache(T obj) {
		// Si le cache est actif
		if (DatabaseProperties.getBool("cache")) {
			if (cacheMap.containsKey(obj.getId())) {
				cacheMap.replace(obj.getId(), obj);
			} else {
				cacheMap.put(obj.getId(), obj);
			}
		}
		//System.out.println("Cache size: " + cacheMap.size());
		//cacheMap.forEach((key, val) -> System.out.println("Cache: " + key));
		return obj;
	}
	
	private void remove(long id) {
		if (cacheMap.containsKey(id)) {
			cacheMap.remove(id);
		}
	}
	
	private DbTable getDbTableAnnotation() {
		Class<?> clazz = this.getClass();
		// Recupération info anotation DbTable
		if (!clazz.isAnnotationPresent(DbTable.class)) {
			throw new IllegalStateException("Unable ton find DbTable annotation");
		}
		return clazz.getAnnotation(DbTable.class);
	}
	
	private Optional<T> getFromDbById(long id) {
		DbTable dbTable = getDbTableAnnotation();
		// Cherche l'id
		String idField = null;
		for(Field field : dbTable.entity().getFields()) {
			if(!field.isAnnotationPresent(DbId.class)) {
				continue;
			}
			// Lecture du nom de l'id dans la base
			if(field.isAnnotationPresent(DbField.class)) {
				idField = field.getAnnotation(DbField.class).value();
			} else {
				throw new IllegalStateException("Unable to find DbField annotation on id");
			}
		}
		if (idField == null) {
			throw new IllegalStateException("Unable to find find DbField annotation");
		}
		// Requete sql
		String sql = "Select * From " + dbTable.name() + " Where " + idField + " = ?";
		List<Object> params = new ArrayList<>();
		params.add(id);
		Optional<T> result = Database.query(sql, params, DatabaseMapper.objectMapper(dbTable));
		// Return
		return result;
	}
	
	private List<T> getFromDbByField(String fieldname, Object value) {
		DbTable dbTable = getDbTableAnnotation();
		// Requete sql
		String sql = "Select * From " + dbTable.name() + " Where " + fieldname + " = ?";
		List<Object> params = new ArrayList<>();
		params.add(value);
		Optional<List<T>> result = Database.query(sql, params, DatabaseMapper.listMapper(dbTable));
		// Return
		if(result.isPresent()) {
			return result.get();
		}
		return new ArrayList<>();
	}
	
	private List<T> getWhereFromDb(List<String> where, List<Object> params) {
		DbTable dbTable = getDbTableAnnotation();
		// Requete sql
		StringBuilder sql = new StringBuilder();
		sql.append("Select * From " + dbTable.name() + "Where 1=1");
		where.forEach(elt -> sql.append(" And " + elt + " = ?"));
		// Execution requete
		Optional<List<T>> result = Database.query(sql.toString(), params, DatabaseMapper.listMapper(dbTable));
		// Return
		if(result.isPresent()) {
			return result.get();
		}
		return new ArrayList<>();
	}
	
	private List<T> getAllFromDb() {
		DbTable dbTable = getDbTableAnnotation();
		// Requete sql
		String sql = "Select * From " + dbTable.name();
		Optional<List<T>> result = Database.query(sql, DatabaseMapper.listMapper(dbTable));
		// Return
		if(result.isPresent()) {
			return result.get();
		}
		return new ArrayList<>();	
	}
	
	private void insert(T obj) {
		DbTable dbTable = getDbTableAnnotation();
		// Requete SQL
		StringBuilder sql = new StringBuilder();
		StringBuilder values = new StringBuilder();
		String dbIdName = null; // Nom de l'id en base
		String attrIdName = null; // Nom de l'id dans la class
		List<Object> params = new ArrayList<>();
		sql.append("Insert Into " + dbTable.name() + "(");
		boolean first = true;
		try {
			for(Field field : obj.getClass().getFields()) {
				if (!field.isAnnotationPresent(DbField.class)) {
					continue;
				}
				// Cherche l'id
				if (dbIdName == null && field.isAnnotationPresent(DbId.class)) {
					dbIdName = field.getAnnotation(DbField.class).value();
					attrIdName = field.getName();
					continue;
				}
				// Recupere la valeur des autres attributs
				if (!first) {
					sql.append(",");
					values.append(",");
				}
				sql.append(field.getAnnotation(DbField.class).value());
				values.append("?");
				// Si il y a un lien avec une autre entité
				if(field.isAnnotationPresent(DbLink.class)) {
					DatabaseUtils.checkIfPersistable(field);
					if(field.get(obj) == null) {
						params.add(null);
					} else {
						Persistable persistable = (Persistable) field.get(obj);
						// TODO Sauvegarder l'autre objet avant
						params.add(persistable.getId());
					}
				}
				// Sinon ajoute simplement le parametre
				else {
					params.add(field.get(obj));
				}
				first = false;
			}
		} catch (IllegalArgumentException | IllegalAccessException e) {
			throw new IllegalStateException("Unable to access to the attribute", e);
		}	
		sql.append(") Values (");
		sql.append(values);
		sql.append(")");
		// Verif que l'on a bien le nom de l'id
		if(dbIdName == null) {
			throw new IllegalStateException("Unable to find id field");
		}
		// Execution de la requete
		Map<String, Object> newId = Database.insert(sql.toString(), params, new String[]{dbIdName});
		if (newId.isEmpty()) {
			throw new IllegalStateException("Unable to save data");
		}
		// Application nouvelle id
		Object val = newId.get(dbIdName);
		try {
			if (val instanceof BigDecimal) {
				BigDecimal b = (BigDecimal) val;
				obj.getClass().getField(attrIdName).set(obj, b.longValue());
			} else {
				obj.getClass().getField(attrIdName).set(obj, val);
			}
		} catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
			throw new IllegalStateException("Unable to set id", e);
		}
	}
	
	private void update(T obj) {
		DbTable dbTable = getDbTableAnnotation();
		// Requete SQL
		StringBuilder sql = new StringBuilder();
		StringBuilder where = new StringBuilder();
		List<Object> params = new ArrayList<>();
		sql.append("Update " + dbTable.name() + " Set ");
		where.append(" Where ");
		boolean first = true;
		for (Field field : obj.getClass().getFields()) {
			try {
				if (!field.isAnnotationPresent(DbField.class)) {
					continue;
				}
				// Si c'est l'id
				if (field.isAnnotationPresent(DbId.class)) {
					where.append(field.getAnnotation(DbField.class).value() + " = ?");
					continue;
				}
				// Sinon ajoute dans le set
				if (!first) {
					sql.append(",");
				}
				sql.append(field.getAnnotation(DbField.class).value() + " = ?");
				// Si il y a un lien avec une autre entité
				if(field.isAnnotationPresent(DbLink.class)) {
					DatabaseUtils.checkIfPersistable(field);
					if(field.get(obj) == null) {
						params.add(null);
					} else {
						Persistable persistable = (Persistable) field.get(obj);
						// TODO Sauvegarder l'autre objet avant
						params.add(persistable.getId());
					}
				}
				// Sinon ajoute simplement le parametre
				else {
					params.add(field.get(obj));
				}
				first = false;
			} catch (IllegalArgumentException | IllegalAccessException e) {
				throw new IllegalStateException("Unable to access to the attribute", e);
			}
		}
		sql.append(where);
		params.add(obj.getId());
		// Execution requete
		if(!Database.execute(sql.toString(), params)) {
			throw new IllegalStateException("Unable to save data");
		}
	}

}
