package com.fp.persistence.commondb;

import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.FlushModeType;
import javax.persistence.Persistence;
import javax.persistence.Table;
import javax.persistence.metamodel.EntityType;

import org.hibernate.SQLQuery;
import org.hibernate.Session;

import com.fp.common.helper.BeanManager;
import com.fp.common.logger.APPLogger;
import com.fp.persistence.commondb.data.ThreadFacade;

/**
 * Clase que se encarga del manejo de la favtoria de EntityManager.
 * 
 * @author Jorge Vaca
 * @version 2.1
 */
public class PersistenceManager {

    /**
     * Key para genrar un map de los beans mapeados en el archivo de hbconfiguration.
     */
    public static final String NAME = "NAMES";

    /**
     * Key que alamacena el numero de sessiones abiertas en el sessionfactory.
     */
    public static final String OPEN = "OPEN";

    /**
     * Key que alamacena el numero de sessiones cerradas en el sessionfactory.
     */
    public static final String CLOSED = "CLOSED";

    /**
     * Key que almacenal hora en la que se genero el sessionfactory .
     */
    public static final String START_TIME = "START TIME";

    /**
     * Instancia a la que hace referencia el Sigleton
     */
    private static PersistenceManager instance = null;

    /**
     * Map de EntityManagerFactory ejb por compania.
     */
    private final Map<Integer, EntityManagerFactory> memfejb = new HashMap<Integer, EntityManagerFactory>();

    /**
     * Map de EntityManagerFactory local por compania.
     */
    private final Map<Integer, EntityManagerFactory> memf = new HashMap<Integer, EntityManagerFactory>();

    /**
     * Map de EntityManagerFactory auxiliar por persistence unit.
     */
    private final Map<String, EntityManagerFactory> memfaux = new HashMap<String, EntityManagerFactory>();

    /**
     * Metodo que entrega una instancia de PersistenceManager, si existe una creada la entrega, caso contrario crea una.
     * Adicionalmenet hace el mapping de las tablas del modelo relacional al modelo de objetos, creando el
     * EntityManagerFactory.
     * 
     * @return PersistenceManager
     */
    public static PersistenceManager getInstance() {
        synchronized (PersistenceManager.class) {
            if (PersistenceManager.instance == null) {
                PersistenceManager.instance = new PersistenceManager();
                PersistenceManager.instance.fillEntityManagerFactory();
            } else {
                Integer cia = PersistenceManager.getCompany();
                if (((PersistenceManager.instance.memfejb.get(cia) == null) && PersistenceManager.iSEjb())
                        || (PersistenceManager.instance.memf.get(cia) == null)) {
                    PersistenceManager.instance.fillEntityManagerFactory();
                }
            }
        }
        return PersistenceManager.instance;
    }

    /**
     * Contruye el entitymanager falctory.
     */
    private void fillEntityManagerFactory() {
        Integer cia = PersistenceManager.getCompany();
        
        if (cia != null && (PersistenceManager.instance.memfejb.get(cia) == null) && PersistenceManager.iSEjb()) {
               try {
                PersistenceManager.instance.memfejb.put(cia, PersistenceManager.getEntityManagerFactory("ejb", cia));
            } catch (Exception e) {
                APPLogger.getLogger().error(e, e);
            }

        }
        if (cia != null && PersistenceManager.instance.memf.get(cia) == null) {
            try {
                PersistenceManager.instance.memf.put(cia, PersistenceManager.getEntityManagerFactory("local", cia));
            } catch (Exception e) {
                APPLogger.getLogger().error(e, e);
            }

        }
    }

    /**
     * Metodo que crea y entrega un EntityManagerFactory, dado el persistenceunit mas el codigo de compania.
     * 
     * @param pUnit Nombre del persistenceunit con el cual se crea el EntityManagerFactory.
     * @param pCompany Codigo de compania.
     * @return EntityManagerFactory
     * @throws Exception
     */
    private static EntityManagerFactory getEntityManagerFactory(String pUnit, Integer pCompany) throws Exception {
        Map<String, Object> configOverrides = new HashMap<String, Object>();
        configOverrides.put("hibernate.ejb.cfgfile", "hibernateFlipMapping.cfg.xml");
        EntityManagerFactory emf = null;
        emf = Persistence.createEntityManagerFactory(pUnit + pCompany, configOverrides);
        return emf;
    }

    /**
     * Metodo que entrega una instancia de PersistenceManager para pruebas, si existe una creada la entrega, caso
     * contrario crea una. Adicionalmenet hace el mapping de las tablas del modelo relacional al modelo de objetos,
     * creando el EntityManagerFactory.
     * 
     * @return PersistenceManager
     */
    public static PersistenceManager getInstanceTest() {
        synchronized (PersistenceManager.class) {
            if (PersistenceManager.instance == null) {
                PersistenceManager.instance = new PersistenceManager();
            }
        }
        Integer cia = PersistenceManager.getCompany();
        if (PersistenceManager.instance.memf.get(cia) == null) {
            try {
                PersistenceManager.instance.memf.put(cia, PersistenceManager.getEntityManagerFactory("local", cia));
            } catch (Exception e) {
                APPLogger.getLogger().error("NO PUEDE CONSTRUIR EL ENTYTY MANAGER FACTORY LOCAL" + cia, e);
            }
            // Para que no construya uno en pruebas.
            PersistenceManager.instance.memfejb.put(cia, PersistenceManager.instance.memf.get(cia));
        }
        return PersistenceManager.instance;
    }

    /**
     * Constructor que se encarga de crear una instancia de PersistenceManager
     * 
     * @param test
     */
    public PersistenceManager() {
    }

    /**
     * Metodo que se encarga de crear un objeto EntityManager.
     * 
     * @return EntityManager
     */
    public EntityManager createEntityManager() {
        EntityManager em = memfejb.get(PersistenceManager.getCompany()).createEntityManager();
        return em;
    }

    /**
     * Metodo que se encarga de crear un objeto EntityManager.
     * 
     * @return EntityManager
     */
    public EntityManager createEntityManagerLocal() {
        EntityManager em = memf.get(PersistenceManager.getCompany()).createEntityManager();
        em.setFlushMode(FlushModeType.COMMIT);
        return em;
    }

    /** Cierra el SessionFactory */
    public void close() {
        memfejb.get(PersistenceManager.getCompany()).close();
    }

    /**
     * Entrega EntityManagerFactory dado el persistenceunit name, se utiiza para conecciones auxiliares a bases de
     * datos, ejemplo cobis.
     * 
     * @return Map<Integer, EntityManagerFactory>
     */
    public EntityManagerFactory getEntityManagerFactoryAuxiliar(String pUnit) {
        EntityManagerFactory emf = memfaux.get(pUnit);
        if (emf == null) {
            emf = Persistence.createEntityManagerFactory(pUnit);
            memfaux.put(pUnit, emf);
        }
        return emf;
    }

    /**
     * Permite Fijar el estado de Trace de la Conexión
     * 
     * @param pSession Session de hibernate de Referencia
     * @param pStatus Estado del Trace.
     * @throws SQLException
     * @throws Exception
     */
    public void setTrace(Session pSession, boolean pStatus) {
        SQLQuery sql = pSession.createSQLQuery("alter session set sql_trace =" + pStatus);
        sql.executeUpdate();
    }

    /**
     * Entrega el codigo de compania de trabajo.
     * 
     * @return Integer
     * @throws Exception
     */
    private static Integer getCompany() {
        return ThreadFacade.getSessionData().getCompany();
    }

    private static boolean iSEjb() {
        return ThreadFacade.getSessionData().isEjb();
    }

    public Class<?> findEntityClass(String pTable) throws Exception {
        return this.findEntity(pTable).getJavaType();
    }

    public EntityType<?> findEntity(String pTable) throws Exception {
        EntityManagerFactory emf = PersistenceHelper.getEntityManager().getEntityManagerFactory();
        Set<EntityType<?>> set = emf.getMetamodel().getEntities();
        EntityType<?> entity = null;
        for (EntityType<?> entityType : set) {
            Table t = entityType.getJavaType().getAnnotation(Table.class);
            if (t.name().compareTo(pTable) == 0) {
                entity = entityType;
                break;
            }
        }
        return entity;
    }

    @SuppressWarnings("rawtypes")
    public static List<String> getDataBasePKFields(Class bean) {
        List<String> ldata = new ArrayList<String>();
        for (Field f : bean.getDeclaredFields()) {
            if (f.isAnnotationPresent(EmbeddedId.class)) {
                Class<?> pkT = f.getType();
                for (Field f1 : pkT.getDeclaredFields()) {
                    if (f1.isAnnotationPresent(Column.class)) {
                        ldata.add("pk." + f1.getAnnotation(Column.class).name().toLowerCase());
                    }
                }
            }
        }
        if (ldata.isEmpty()) {
            ldata.add("pk");
        }
        return ldata;
    }

    public List<String> getDataBaseFields(String pTable) throws Exception {
        List<String> data = new ArrayList<String>();
        Class<?> et = this.findEntityClass(pTable);
        for (Field f : et.getDeclaredFields()) {
            if (f.isAnnotationPresent(Column.class)) {
                data.add(f.getAnnotation(Column.class).name());
            }
            if (f.isAnnotationPresent(EmbeddedId.class)) {
                Class<?> pkT = f.getType();
                for (Field f1 : pkT.getDeclaredFields()) {
                    if (f1.isAnnotationPresent(Column.class)) {
                        data.add(f1.getAnnotation(Column.class).name());
                    }
                }
            }
        }
        return data;
    }

    /**
     * Entrega el tipo de dato de un campo de una clase. Si no encuentra el campo retorna null.
     * 
     * @param clase Clase a buscar el tipo de dato de un campo.
     * @param field Nombre del campo a buscar en la clase.
     * @return Class<?>
     */
    public static Class<?> getType(Class<?> clase, String field) {
        Class<?> tipo = null;
        for (Field f : clase.getDeclaredFields()) {
            tipo = f.getType();
            if (f.isAnnotationPresent(EmbeddedId.class)) {
                for (Field f1 : tipo.getDeclaredFields()) {
                    if (f1.isAnnotationPresent(Column.class) && f1.getName().equals(field)) {
                        tipo = f1.getType();
                        return tipo;
                    }
                }
            }
            if (f.isAnnotationPresent(Column.class) && f.getName().equals(field)) {
                return tipo;
            }
        }
        return null;
    }

    /**
     * Convierte el valor al tipo de satos del atributo de la clase.
     * 
     * @param bean Nombre de la clase a buscar el tipo de datos de un campo.
     * @param field Nombre del campo a buscar el tipo de dato.
     * @param value Valor a transformar al tipo de dato de una clase.
     * @return Object
     * @throws Exception
     */
    public static Object changeType(String bean, String field, Object value) throws Exception {
        if (value == null) {
            return value;
        }
        // Convierte el valor del criterio al tipo especifico del campo de la clase.
        Class<?> tipo = PersistenceManager.getType(Class.forName(bean), field.replace("pk.", ""));
        if (tipo != null) {
            value = BeanManager.convertObject(value, tipo);
        }
        return value;
    }

}
