package com.tqdev.crudapi.record;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.Field;
import org.jooq.ResultQuery;
import org.jooq.impl.DSL;

import com.tqdev.crudapi.column.reflection.DatabaseReflection;
import com.tqdev.crudapi.column.reflection.ReflectedTable;
import com.tqdev.crudapi.record.container.Record;

public class RelationIncluder {

	private ColumnSelector columns;

	public RelationIncluder(ColumnSelector columns) {
		this.columns = columns;
	}

	public void addMandatoryColumns(ReflectedTable table, DatabaseReflection reflection, Params params) {
		if (!params.containsKey("include") || !params.containsKey("columns")) {
			return;
		}
		for (String tableNames : params.get("include")) {
			ReflectedTable t1 = table;
			for (String tableName : tableNames.split(",")) {
				ReflectedTable t2 = reflection.getTable(tableName);
				if (t2 == null) {
					continue;
				}
				List<Field<Object>> fks1 = t1.getFksTo(t2.getName());
				ReflectedTable t3 = hasAndBelongsToMany(t1, t2, reflection);
				if (t3 != null || !fks1.isEmpty()) {
					params.add("mandatory", t2.getName() + "." + t2.getPk().getName());
				}
				for (Field<Object> fk : fks1) {
					params.add("mandatory", t1.getName() + "." + fk.getName());
				}
				List<Field<Object>> fks2 = t2.getFksTo(t1.getName());
				if (t3 != null || !fks2.isEmpty()) {
					params.add("mandatory", t1.getName() + "." + t1.getPk().getName());
				}
				for (Field<Object> fk : fks2) {
					params.add("mandatory", t2.getName() + "." + fk.getName());
				}
				t1 = t2;
			}
		}
	}

	private PathTree<String, Boolean> getIncludesAsPathTree(DatabaseReflection reflection, Params params) {
		PathTree<String, Boolean> includes = new PathTree<>();
		if (params.containsKey("include")) {
			for (String includedTableNames : params.get("include")) {
				LinkedList<String> path = new LinkedList<>();
				for (String includedTableName : includedTableNames.split(",")) {
					ReflectedTable t = reflection.getTable(includedTableName);
					if (t != null) {
						path.add(t.getName());
					}
				}
				includes.put(path, true);
			}
		}
		return includes;
	}

	public void addIncludes(String tableName, ArrayList<Record> records, DatabaseReflection reflection, Params params,
							DSLContext dsl) {

		PathTree<String, Boolean> includes = getIncludesAsPathTree(reflection, params);
		addIncludesForTables(reflection.getTable(tableName), includes, records, reflection, params, dsl);
	}

	private ReflectedTable hasAndBelongsToMany(ReflectedTable t1, ReflectedTable t2, DatabaseReflection reflection) {
		for (String tableName : reflection.getTableNames()) {
			ReflectedTable t3 = reflection.getTable(tableName);
			if (!t3.getFksTo(t1.getName()).isEmpty() && !t3.getFksTo(t2.getName()).isEmpty()) {
				return t3;
			}
		}
		return null;
	}

	private void addIncludesForTables(ReflectedTable t1, PathTree<String, Boolean> includes, ArrayList<Record> records,
			DatabaseReflection reflection, Params params, DSLContext dsl) {
		for (String t2Name : includes.getKeys()) {

			ReflectedTable t2 = reflection.getTable(t2Name);

			boolean belongsTo = !t1.getFksTo(t2.getName()).isEmpty();
			boolean hasMany = !t2.getFksTo(t1.getName()).isEmpty();
			ReflectedTable t3 = hasAndBelongsToMany(t1, t2, reflection);
			boolean hasAndBelongsToMany = t3 != null;

			ArrayList<Record> newRecords = new ArrayList<>();
			HashMap<Object, Object> fkValues = null;
			HashMap<Object, ArrayList<Object>> pkValues = null;
			HabtmValues habtmValues = null;

			if (belongsTo) {
				fkValues = getFkEmptyValues(t1, t2, records);
				addFkRecords(t2, fkValues, params, dsl, newRecords);
			}
			if (hasMany) {
				pkValues = getPkEmptyValues(t1, records);
				addPkRecords(t1, t2, pkValues, params, dsl, newRecords);
			}
			if (hasAndBelongsToMany) {
				habtmValues = getHabtmEmptyValues(t1, t2, t3, dsl, records);
				addFkRecords(t2, habtmValues.fkValues, params, dsl, newRecords);
			}

			addIncludesForTables(t2, includes.get(t2Name), newRecords, reflection, params, dsl);

			if (fkValues != null) {
				fillFkValues(t2, newRecords, fkValues);
				setFkValues(t1, t2, records, fkValues);
			}
			if (pkValues != null) {
				fillPkValues(t1, t2, newRecords, pkValues);
				setPkValues(t1, t2, records, pkValues);
			}
			if (habtmValues != null) {
				fillFkValues(t2, newRecords, habtmValues.fkValues);
				setHabtmValues(t1, t3, records, habtmValues);
			}
		}
	}

	private HashMap<Object, Object> getFkEmptyValues(ReflectedTable t1, ReflectedTable t2, ArrayList<Record> records) {
		HashMap<Object, Object> fkValues = new HashMap<>();
		List<Field<Object>> fks = t1.getFksTo(t2.getName());
		for (Field<Object> fk : fks) {
			for (Record record : records) {
				Object fkValue = record.get(fk.getName());
				if (fkValue == null) {
					continue;
				}
				fkValues.put(fkValue, null);
			}
		}
		return fkValues;
	}

	private void addFkRecords(ReflectedTable t2, HashMap<Object, Object> fkValues, Params params, DSLContext dsl,
			ArrayList<Record> records) {
		Field<Object> pk = t2.getPk();
		ArrayList<Field<?>> fields = columns.getNames(t2, false, params);
		ResultQuery<org.jooq.Record> query = dsl.select(fields).from(t2).where(pk.in(fkValues.keySet()));
		for (org.jooq.Record record : query.fetch()) {
			records.add(Record.valueOf(record.intoMap()));
		}
	}

	private void fillFkValues(ReflectedTable t2, ArrayList<Record> fkRecords, HashMap<Object, Object> fkValues) {
		Field<Object> pk = t2.getPk();
		for (Record fkRecord : fkRecords) {
			Object pkValue = fkRecord.get(pk.getName());
			fkValues.put(pkValue, fkRecord);
		}
	}

	private void setFkValues(ReflectedTable t1, ReflectedTable t2, ArrayList<Record> records,
			HashMap<Object, Object> fkValues) {
		List<Field<Object>> fks = t1.getFksTo(t2.getName());
		for (Field<Object> fk : fks) {
			for (Record record : records) {
				Object key = record.get(fk.getName());
				if (key == null) {
					continue;
				}
				record.put(fk.getName(), fkValues.get(key));
			}
		}
	}

	private HashMap<Object, ArrayList<Object>> getPkEmptyValues(ReflectedTable t1, ArrayList<Record> records) {
		HashMap<Object, ArrayList<Object>> pkValues = new HashMap<>();
		for (Record record : records) {
			Object key = record.get(t1.getPk().getName());
			pkValues.put(key, new ArrayList<>());
		}
		return pkValues;
	}

	private void addPkRecords(ReflectedTable t1, ReflectedTable t2, HashMap<Object, ArrayList<Object>> pkValues,
			Params params, DSLContext dsl, ArrayList<Record> records) {
		List<Field<Object>> fks = t2.getFksTo(t1.getName());
		ArrayList<Field<?>> fields = columns.getNames(t2, false, params);
		Condition condition = DSL.falseCondition();
		for (Field<Object> fk : fks) {
			condition = condition.or(fk.in(pkValues.keySet()));
		}
		ResultQuery<org.jooq.Record> query = dsl.select(fields).from(t2).where(condition);
		for (org.jooq.Record record : query.fetch()) {
			records.add(Record.valueOf(record.intoMap()));
		}
	}

	private void fillPkValues(ReflectedTable t1, ReflectedTable t2, ArrayList<Record> pkRecords,
			HashMap<Object, ArrayList<Object>> pkValues) {
		List<Field<Object>> fks = t2.getFksTo(t1.getName());
		for (Field<Object> fk : fks) {
			for (Record pkRecord : pkRecords) {
				Object key = pkRecord.get(fk.getName());
				ArrayList<Object> records = pkValues.get(key);
				if (records != null) {
					records.add(pkRecord);
				}
			}
		}
	}

	private void setPkValues(ReflectedTable t1, ReflectedTable t2, ArrayList<Record> records,
			HashMap<Object, ArrayList<Object>> pkValues) {
		for (Record record : records) {
			Object key = record.get(t1.getPk().getName());
			record.put(t2.getName(), pkValues.get(key));
		}
	}

	private HabtmValues getHabtmEmptyValues(ReflectedTable t1, ReflectedTable t2, ReflectedTable t3, DSLContext dsl,
			ArrayList<Record> records) {
		HashMap<Object, ArrayList<Object>> pkValues = getPkEmptyValues(t1, records);
		HashMap<Object, Object> fkValues = new HashMap<>();

		Field<Object> fk1 = t3.getFksTo(t1.getName()).get(0);
		Field<Object> fk2 = t3.getFksTo(t2.getName()).get(0);
		List<Field<?>> fields = Arrays.asList(fk1, fk2);
		Condition condition = fk1.in(pkValues.keySet());
		ResultQuery<org.jooq.Record> query = dsl.select(fields).from(t3).where(condition);
		for (org.jooq.Record record : query.fetch()) {
			Object val1 = record.get(fk1);
			Object val2 = record.get(fk2);
			pkValues.get(val1).add(val2);
			fkValues.put(val2, null);
		}

		return new HabtmValues(pkValues, fkValues);
	}

	private void setHabtmValues(ReflectedTable t1, ReflectedTable t3, ArrayList<Record> records,
			HabtmValues habtmValues) {
		for (Record record : records) {
			Object key = record.get(t1.getPk().getName());
			ArrayList<Object> val = new ArrayList<>();
			ArrayList<Object> fks = habtmValues.pkValues.get(key);
			for (Object fk : fks) {
				val.add(habtmValues.fkValues.get(fk));
			}
			record.put(t3.getName(), val);
		}
	}
}