package com.fitbank.common.hb;

import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;
import org.hibernate.LockMode;
import org.hibernate.Query;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.type.Type;

import com.fitbank.common.Helper;
import com.fitbank.common.exception.FitbankException;
import com.fitbank.common.logger.FitbankLogger;
import org.apache.commons.lang.StringUtils;
import org.hibernate.FlushMode;
import org.hibernate.LockOptions;
import org.hibernate.type.StandardBasicTypes;

/**
 * Clase utilitaria para armar sentencias y obtener resultados a consultas a la
 * base de datos con Hibernate.
 *
 * @param  Tipo de los resultados
 *
 * @author Fitbank
 * @version 2.0
 */
public final class UtilHB {

    /**
     * Session de hibernate empleada en la consulta
     */
    private final Session session;

    /**
     * Map auxiliar empleado para el manejo de Parámetros
     */
    private Map<String, Object> map;

    /**
     * Sentencia HQL a ejecutar
     */
    private String sentence;

    /**
     * Indicador de si la consulta requiere manejar Cache
     */
    private boolean cacheable = false;

    /**
     * Indicador de si la consulta es de Lectura solamente
     */
    private boolean readonly = false;

    /**
     * Tipo de Bloqueo pesimista que debe aplicarse a la consulta
     */
    private LockOptions lockOptions;

    /**
     * Modo de hacer flush antes de ejecutar el query.
     */
    private FlushMode flushMode = null;

    /**
     * Alias de la variable a bloquear
     */
    private String alias;

    /**
     * Página en la que se encuentra la consulta
     */
    private Integer page = null;

    /**
     * Número de registros por página
     */
    private Integer recordperpage = null;

    /**
     * Referencia al Log.
     */
    private static final Logger LOGGER = FitbankLogger.getLogger();

    /**
     * Crea una nueva instancia de UtilHB
     *
     */
    public UtilHB() {
        this(null, LockOptions.NONE, null);
    }

    public UtilHB(Session pSession) {
        this(null, pSession, LockOptions.NONE, null);
    }

    /**
     * Crea una nueva instancia con una sentencia a ejecutar.
     *
     * @param pSentence
     */
    public UtilHB(String pSentence) {
        this(pSentence, LockOptions.NONE, null);
    }

    public UtilHB(String pSentence, LockMode pLockMode, String pAlias) {
        this(pSentence, Helper.getSession(), pLockMode, pAlias);
    }

    public UtilHB(String pSentence, LockOptions pLockOptions, String pAlias) {
        this(pSentence, Helper.getSession(), pLockOptions, pAlias);
    }

    public UtilHB(String pSentence, Session pSession) {
        this(pSentence, pSession, LockOptions.NONE, null);
    }

    public UtilHB(String pSentence, FlushMode flushMode) {
        this(pSentence, Helper.getSession(), LockOptions.NONE, flushMode, null);
    }

    /**
     * Crea una nueva instancia con una sentencia a ejecutar.
     *
     * @param pSentence Sentencia a ejecutar
     * @param pSession Sesion
     * @param pLockMode Tipo de Bloqueo Pesismista
     * @param pAlias Variable a bloquear
     */
    public UtilHB(String pSentence, Session pSession, LockMode pLockMode,
            String pAlias) {
        this.sentence = pSentence;
        this.map = new HashMap<String, Object>();
        this.session = pSession;
        this.lockOptions = new LockOptions(pLockMode);
        this.alias = pAlias;
    }

    /**
     * Crea una nueva instancia con una sentencia a ejecutar.
     *
     * @param pSentence Sentencia a ejecutar
     * @param pSession Sesion
     * @param pLockMode Tipo de Bloqueo Pesismista
     * @param pAlias Variable a bloquear
     */
    public UtilHB(String pSentence, Session pSession, LockOptions pLockOptions,
            String pAlias) {
        this.sentence = pSentence;
        this.map = new HashMap<String, Object>();
        this.session = pSession;
        this.lockOptions = pLockOptions;
        this.alias = pAlias;
    }

    public UtilHB(String pSentence, Session pSession, LockOptions pLockOptions,
            FlushMode flushMode, String pAlias) {
        this.sentence = pSentence;
        this.map = new HashMap<String, Object>();
        this.session = pSession;
        this.lockOptions = pLockOptions;
        this.flushMode = flushMode;
        this.alias = pAlias;
    }

    //<editor-fold defaultstate="collapsed" desc="Getters y Setters">
    public String getSentence() {
        return this.sentence;
    }

    public void setSentence(String pSentence) {
        this.map.clear();
        this.sentence = pSentence;
    }

    public boolean isCacheable() {
        return this.cacheable;
    }

    public void setCacheable(boolean cacheable) {
        this.cacheable = cacheable;
    }

    public boolean isReadonly() {
        return this.readonly;
    }

    public void setReadonly(boolean readonly) {
        this.readonly = readonly;
    }

    public Integer getPage() {
        return this.page;
    }

    public void setPage(Integer pPage) {
        this.page = pPage;
    }

    public Integer getRecordperpage() {
        return this.recordperpage;
    }

    public void setRecordperpage(Integer pRecordperpage) {
        this.recordperpage = pRecordperpage;
    }
    //</editor-fold>

    public List getList() {
        return this.getList(true);
    }

    /**
     * Retorna una lista de entidades o arreglos.
     *
     * @param pNoDataFound lanza o no la excepcion (true=ejecuta, false=ignora)
     *
     * @return lista
     */
    public List getList(boolean pNoDataFound) {
        List result = this.buildQuery().list();
        if (result == null || result.isEmpty()) {
            if (pNoDataFound) {
                throw new FitbankException("HB004",
                        "CONSULTA NO TIENE REGISTROS");
            } else {
                return new ArrayList();
            }
        }
        return result;
    }

    /**
     * Retorna: un entity si la sentencia retorna un entity, si es un conjunto
     * de campos de una o mas tablas retorna un Array.
     * <ol>
     * <li>Objeto que contiene una entidad si la sentencia retorna un entity
     * <br>
     * sentencia ==> from com.test.Test as t where t.pk.c1 = :valor1</li>
     * <li>Array que contiene una entidad si la sentencia retorna mas de un
     * campo
     * <br>
     * sentencia ==> select t.c1,t.c2...t.c3 from com.test.Test as t</li>
     * <li>Array que contiene una entidades o campos cuando existe junturas.
     * sentencia ==> from com.test.Test a,from com.test.Testuno b where
     * a.pk=b.pk
     * and b.pk.lenguage = 'ES'</li>
     * </ol>
     *
     * @return
     */
    public Object getObject() {
        return this.buildQuery().uniqueResult();
    }

    /**
     * Retorna un entity dada la clave primaria.
     *
     * @param <T> Tipo de la entidad, tomado del parametro clase
     * @param clase Clase de la entidad
     * @param key Clave primaria
     *
     * @return El objeto
     */
    public <T> T getObjectByKey(Class<T> clase, Serializable key) {
        Object obj = this.lockOptions == null || this.lockOptions.equals(LockOptions.NONE) ? this.session.get(clase, key)
                : this.session.get(clase, key, this.lockOptions);
        if (!this.cacheable) {
            if (this.lockOptions == null || this.lockOptions.equals(LockOptions.NONE)) {
                if (obj != null) {
                    this.session.refresh(obj);
                }
            } else {
                this.session.refresh(obj, this.lockOptions);
            }
        }
        return (T) obj;
    }

    /**
     * Retorna una lista de entidades o arreglos.
     *
     * @return
     */
    public List getResults() {
        return this.buildQuery().list();
    }

    /**
     * Retorna un resultset sobre el cual se puede iterar.
     *
     * @return
     */
    public ScrollableResults getScroll() {
        Query q = this.buildQuery();
        q.setReadOnly(true);
        return q.scroll(ScrollMode.FORWARD_ONLY);
    }

    /**
     * Retorna un resultset sobre el cual se puede iterar.
     *
     * @param pMode
     * Modo permitidos para la iteracion: FORWARD_ONLY, SCROLL_SENSITIVE,
     * SCROLL_INSENSITIVE
     *
     * @return
     */
    public ScrollableResults getScroll(ScrollMode pMode) {
        return this.buildQuery().scroll(pMode);
    }

    /**
     * Fija un criterio BigDecimal a la sentencia. El nombre del parametro debe
     * ser igual al del where de la sentencia.
     *
     * @param pParameter
     * @param pBigdecimal
     */
    public void setBigDecimal(String pParameter, BigDecimal pBigdecimal) {
        if (pBigdecimal == null) {
            this.map.put(pParameter, StandardBasicTypes.BIG_DECIMAL);
        } else {
            this.map.put(pParameter, pBigdecimal);
        }
    }

    /**
     * Fija un criterio Data a la sentencia. El nombre del parametro debe ser
     * igual al del where de la sentencia.
     *
     * @param pParameter
     * @param pDate
     */
    public void setDate(String pParameter, Date pDate) {
        this.map.put(pParameter, pDate);
    }

    /**
     * Fija un criterio Integer a la sentencia. El nombre del parametro debe ser
     * igual al del where de la sentencia.
     *
     * @param pParameter
     * @param pInteger
     */
    public void setInteger(String pParameter, Integer pInteger) {
        if (pInteger == null) {
            this.map.put(pParameter, StandardBasicTypes.INTEGER);
        } else {
            this.map.put(pParameter, pInteger);
        }
    }

    /**
     * Fija un criterio Integer a la sentencia. El nombre del parametro debe ser
     * igual al del where de la sentencia.
     *
     * @param pParameter
     * @param pLong
     */
    public void setLong(String pParameter, Long pLong) {
        if (pLong == null) {
            this.map.put(pParameter, StandardBasicTypes.LONG);
        } else {
            this.map.put(pParameter, pLong);
        }
    }

    /**
     * Fija un criterio string de la sentencia. El nombre del parametro debe ser
     * igual al del where de la sentencia.
     *
     * @param pParameter
     * @param pString
     */
    public void setString(String pParameter, String pString) {
        this.map.put(pParameter, pString);
    }

    /**
     * Fija un criterio Timestamp a la sentencia. El nombre del parametro debe
     * ser
     * igual al del where de la sentencia.
     *
     * @param pParameter
     * @param pTimestamp
     */
    public void setTimestamp(String pParameter, Timestamp pTimestamp) {
        this.map.put(pParameter, pTimestamp);
    }

    /**
     * Setea valores de parametros definidos previamente en una sentencia.
     *
     * @param pMparamters
     * Map con el nombre del parametro y el valor del mismo.
     */
    public void setParametersValue(Map<String, Object> pMparamters) {
        Iterator<String> itr = pMparamters.keySet().iterator();
        while (itr.hasNext()) {
            String key = itr.next();
            Object value = pMparamters.get(key);
            if (value == null) {
                throw new FitbankException("GEN025",
                        "NO SE HA ENVIADO VALOR PARA EL PARAMETRO {0}", key);
            }
            LOGGER.debug("parametro " + key + " valor " + value + " "
                    + value.getClass());
            if (value instanceof String) {
                this.setString(key, (String) value);
            } else if (value instanceof Integer) {
                this.setInteger(key, (Integer) value);
            } else if (value instanceof Long) {
                this.setLong(key, (Long) value);
            } else if (value instanceof BigDecimal) {
                this.setBigDecimal(key, (BigDecimal) value);
            } else if (value instanceof Timestamp) {
                this.setTimestamp(key, (Timestamp) value);
            } else if (value instanceof Date) {
                this.setDate(key, (Date) value);
            }

        }
    }

    private Query buildQuery() {
        if (this.sentence == null) {
            throw new FitbankException("HB005", "SENTENCIA INVALIDA");
        }
        Query qry = this.session.createQuery(this.sentence);
        qry.setCacheable(this.cacheable);
        String[] param = qry.getNamedParameters();
        for (String parameter : param) {
            Object obj = this.map.get(parameter);
            if (obj == null) {
                throw new FitbankException("HB006", "VALOR NO ENVIADO  {0}",
                        parameter);
            } else if (obj instanceof Type) {
                qry.setParameter(parameter, null, (Type) obj);
                LOGGER.debug("UtilHB.execute null " + parameter);
            } else {
                if (obj instanceof Date) {
                    qry.setDate(parameter, (Date) obj);
                } else if (obj instanceof Timestamp) {
                    qry.setTimestamp(parameter, (Timestamp) obj);
                } else {
                    qry.setParameter(parameter, obj);
                }
            }
        }
        qry.setReadOnly(this.readonly);
        if (this.lockOptions != null && !this.lockOptions.equals(LockOptions.NONE)
                && StringUtils.isNotBlank(this.alias)) {
            qry.setLockMode(this.alias, this.lockOptions.getLockMode());
        }

        if (this.flushMode != null) {
            qry.setFlushMode(this.flushMode);
        }

        if ((this.page != null) && (this.recordperpage != null) && (this.page
                > 0) && (this.recordperpage > 0)) {
            if (this.page > 1) {
                qry.setFirstResult((this.page - 1) * this.recordperpage);
            }
            qry.setMaxResults(this.recordperpage + 1);
        }
        return qry;
    }

    /**
     * Definir el tipo de flush a usar antes de ejecutar este HQL.
     * @param flushmode Tipo de Flush (MANUAL, COMMIT, etc).
     */
    public void setFlushMode(FlushMode flushmode) {
        this.flushMode = flushmode;
    }

    /**
     * Obtener el tipo de flush, a usar antes de ejecutar este HQL.
     * @return Tipo de Flush (MANUAL, COMMIT, etc).
     */
    public FlushMode getFlushMode() {
        return this.flushMode;
    }

    /**
     * Definir el tipo de bloqueo, segun un alias en el HQL.
     * @param lockOptions Tipo de Bloqueo (READ, UPGRADE, NONE, etc).
     * @param pAlias Alias de la referencia a usar en el bloqueo.
     */
    public void setLockOptions(LockOptions lockOptions, String pAlias) {
        this.lockOptions = lockOptions;
        this.alias = pAlias;
    }

    /**
     * Obtener el tipo de bloqueo definido pare este HQL.
     * @return Tipo de Bloqueo definido en el HQL.
     */
    public LockOptions getLockOptions() {
        return this.lockOptions;
    }
}
