package com.fitbank.schemautils;

import java.io.InputStream;
import java.util.Map;

import com.fitbank.serializador.xml.ExcepcionParser;
import com.fitbank.util.CaseInsensitiveMap;
import com.fitbank.util.Debug;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;

/**
 * Clase Schema.
 * 
 * @author FitBank
 */
public class Schema {

    private final Map<String, Table> tables = new CaseInsensitiveMap<Table>();

    private transient final Map<String, DescriptionKey> descriptionTables =
            new CaseInsensitiveMap<DescriptionKey>() {

                @Override
                public boolean containsKey(Object key) {
                    return super.containsKey(baseKey(key));
                }

                @Override
                public DescriptionKey get(Object key) {
                    return super.get(baseKey(key));
                }

                @Override
                public DescriptionKey put(String key, DescriptionKey value) {
                    return super.put(baseKey(key), value);
                }

            };

    public static Schema schema;

    public static Schema get() {
        if (schema == null) {
            schema = load();
            if (schema != null) {
                schema.consolidate();
            }
        }

        return schema;
    }

    public static Schema schemaN;

    public static Schema getN() {
        if (schemaN == null) {
            schemaN = load();
        }

        return schemaN;
    }

    public Field getField(String tableName, String fieldName) {
        Table table = getTables().get(tableName);

        if (table == null) {
            return null;
        }

        return table.getFields().get(fieldName);
    }

    private static Schema load() {
        InputStream is = Schema.class.getClassLoader().getResourceAsStream(
                "com/fitbank/schemautils/schema.xml");
        try {
            return SchemaXML.parse(is);
        } catch (ExcepcionParser e) {
            Debug.error(e);
            return null;
        }
    }

    public static <T> T baseKey(T key) {
        if (key instanceof String) {
            String keyString = (String) key;
            if (keyString.contains("_")) {
                key = (T) keyString.substring(0, keyString.indexOf('_'));
            }
        }
        return key;
    }

    /**
     * Crea un nuevo objeto Schema.
     */
    public Schema() {
    }

    /**
     * Junta las tablas con sus tablas Id y setea la propiedad rootTable.
     */
    public void consolidate() {
        removeIDTables();
        extractDescriptionTables();
        findDescriptionTable();
    }

    private void removeIDTables() {
        // Juntar tablas con sus ID
        Iterator<Table> tables = getTables().values().iterator();
        while (tables.hasNext()) {
            Table table = tables.next();
            String noIdName = table.getName().replaceAll("ID$", "");
            if (table.getName().endsWith("ID") && getTables().containsKey(
                    noIdName)) {
                Table noIdTable = getTables().get(noIdName);
                for (Field field : table.getFields().values()) {
                    noIdTable.addField(field);
                }
                tables.remove();
            }
        }
        // Cambiar referencias a tablas ID por la tabla normal
        for (Table table : getTables().values()) {
            String noIdName = table.getName().replaceAll("ID$", "");
            for (Field field : table.getFields().values()) {
                Collection<String> newTables =
                        new LinkedList<String>();
                Iterator<String> parentTables =
                        field.getParentTables().iterator();
                while (parentTables.hasNext()) {
                    String parentTable = parentTables.next();
                    String noIdParent = parentTable.replaceAll("ID$", "");
                    if (getTables().containsKey(noIdParent)) {
                        parentTables.remove();
                        newTables.add(noIdParent);
                    } else if (!getTables().containsKey(parentTable)) {
                        Debug.error("Tabla padre no encontrada: " + parentTable);
                        parentTables.remove();
                    }
                }
                field.getParentTables().addAll(newTables);
            }
        }
    }

    /**
     * Busca la tabla raíz de todos los campos.
     */
    private void extractDescriptionTables() {
        for (Table table : getTables().values()) {
            if (table.getName().startsWith("V")) {
                continue;
            }
            if (!table.getFields().containsKey("DESCRIPCION") && !table.
                    getFields().containsKey("NOMBRE")) {
                continue;
            }

            DescriptionKey dk = new DescriptionKey();

            dk.setTable(table.getName());
            dk.getFields().addAll(table.getPrimaryKeys());

            for (Field field : table.getFields().values()) {
                if (field.getPrimaryKey() && field.getParentTables().isEmpty()) {
                    dk.getBaseFields().add(field.getName());
                }
                if (field.getName().matches("(DESCRIPCION|NOMBRE)")) {
                    dk.setDescriptionField(field.getName());
                }
            }

            dk.getFields().remove("FHASTA");
            dk.getBaseFields().remove("FHASTA");

            if (dk.getBaseFields().size() > 1) {
                Debug.debug("Tabla con más de un campo principal: " + table.
                        getName() + " => " + dk.getBaseFields());
            }

            for (String name : dk.getBaseFields()) {
                if (descriptionTables.containsKey(name)) {
                    Debug.debug("Dos tablas con el mismo campo principal: "
                            + name + " => " + descriptionTables.get(name)
                            + " Y " + dk);
                }
                descriptionTables.put(name, dk);
            }
        }
    }

    /**
     * Busca la tabla raíz de todos los campos.
     */
    private void findDescriptionTable() {
        for (Table table : getTables().values()) {
            for (Field field : table.getFields().values()) {
                if (field.getParentTables().isEmpty()) {
                    continue;
                }

                String fieldName = field.getName();

                if (descriptionTables.containsKey(fieldName)) {
                    DescriptionKey dk = descriptionTables.get(fieldName);
                    field.setDescriptionKey(dk);
                } else {
                    Debug.debug("No se encontró tabla de descripción para: " + table.
                            getName() + "." + field.getName() + " en " + field.
                            getParentTables());
                }
            }
        }
    }

    /**
     * Añade una tabla a este objeto.
     * 
     * @param table
     *            Table a ser añadida
     */
    public void addTable(Table table) {
        tables.put(table.getName(), table);
    }

    /**
     * Encuentra una tabla dado el código.
     * 
     * @param code
     *            Código de la tabla a obtener
     * 
     * @return Table
     */
    public Table findTable(int code) {
        for (Table t : tables.values()) {
            if (Integer.parseInt(t.getCode()) == code) {
                return t;
            }
        }

        return null;
    }

    public Map<String, Table> getTables() {
        return tables;
    }

}
