package com.fp.persistence.commondb.helper;

import java.sql.Date;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;

import com.fp.common.helper.CalculationBase;
import com.fp.common.logger.APPLogger;
import com.fp.persistence.commondb.exception.CommondbException;

/**
 * Clase utilitaria para el manejo de fechas.
 * 
 * @author Jorge Vaca
 * @version 2.1
 */
public class APPDates implements Comparable {

    /* Fecha base para las Operaciones */
    private Date date;

    /* Base de Calculo */
    private CalculationBase base;

    /* Calendario Encargado de las operaciones con las Fechas */
    private final GregorianCalendar calendar;

    /* Formateador de Fechas para transporte */
    private final SimpleDateFormat formateador;

    /**
     * Crea una nueva instancia de Dates
     * 
     * @param pDate Fecha base
     * @param pBase Base de Calculo;
     */
    public APPDates(String pDate, CalculationBase pBase) throws Exception {
        this(new Date(FormatDates.getInstance().getTransportDateFormat().parse(pDate).getTime()), pBase);
    }

    /**
     * Crea una nueva instancia de Dates
     * 
     * @param pDate Fecha base como string formato yyyy-MM-dd
     */
    public APPDates(String pDate) throws Exception {
        this(pDate, CalculationBase.B360365);
    }

    /**
     * Crea una nueva instancia de Dates
     * 
     * @param pDate Fecha base
     * @param pBase Base de Calculo;
     */
    public APPDates(Date pDate, CalculationBase pBase) throws Exception {
        this.date = pDate;
        this.base = pBase;
        this.calendar = new GregorianCalendar();
        this.calendar.setTime(this.date);
        this.formateador = FormatDates.getInstance().getTransportDateFormat();
    }

    /**
     * Crea una nueva instancia de Dates
     * 
     * @param pDate Fecha base
     * @param pBase Base de Calculo;
     */
    public APPDates(Date pDate, CalculationBase pBase, SimpleDateFormat sdf) throws Exception {
        this.date = pDate;
        this.base = pBase;
        this.calendar = new GregorianCalendar();
        this.calendar.setTime(this.date);
        this.formateador = sdf;
    }

    /**
     * Crea una nueva instancia de Dates con base 360/365
     * 
     * @param pDate Fecha base
     */
    public APPDates(Date pDate) throws Exception {
        this(pDate, CalculationBase.B360365);
    }

    /**
     * Entrega el Valor de date.
     * 
     * @return Date
     */
    public Date getDate() {
        return this.date;
    }

    /**
     * Entrega el Valor de date.
     * 
     * @return Timestamp
     */
    public Timestamp getTimestamp() {
        return new Timestamp(this.date.getTime());
    }

    /**
     * Metodoq ue resta un mes a una fecha y le pone el ultimo dia del mes en la misma.
     * 
     * @throws Exception
     */
    public void substractOneMonth365() throws Exception {
        this.calendar.add(Calendar.MONTH, -1);
        int max1 = this.calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
        this.calendar.set(Calendar.DAY_OF_MONTH, max1);
        this.date = new Date(this.calendar.getTimeInMillis());
    }

    /**
     * Clase que se encarga de restar dos fechas, a la fecha mayor se resta una fecha menor.
     * 
     * @param pInicialdate Feinicial a restar de una fecha final.
     * @return int
     * @throws Exception
     */
    public int substract(APPDates pInicialdate) throws Exception {
        int days = 0;
        if (pInicialdate.getDate().after(this.date)) {
            throw new CommondbException("COMMONDB-0001", "FECHA A RESTAR: {0,date,yyyy-MM-dd} ES MAYOR A: {1,date,yyyy-MM-dd}",
                    pInicialdate.getDate(), this.date);
        }
        if (pInicialdate.getDate().compareTo(this.date) == 0) {
            return 0;
        }
        if (this.base.getMonthBase() == 365) {
            days = this.substract365(pInicialdate);
        } else {
            days = this.substract360(pInicialdate);
        }
        return days;
    }

    /**
     * Metodo que resta fechas en base cuando la base es 365/xxx
     * 
     * @param pInicialdate Fecha inicia a restar.
     * @return int
     * @throws Exception
     */
    private int substract365(APPDates pInicialdate) throws Exception {
        int days = 0;
        GregorianCalendar cal1 = new GregorianCalendar();
        cal1.setTime(pInicialdate.getDate());
        do {
            cal1.add(Calendar.DATE, 1);
            days++;
        } while (this.date.compareTo(cal1.getTime()) != 0);
        return days;
    }

    /**
     * Metodo que se encarga de restar dos fehcas en base 360.
     * 
     * @param pInicialdate
     * @return
     * @throws Exception
     */
    private int substract360(APPDates pInicialdate) throws Exception {
        int days = 0;
        int nYears = this.getField(Calendar.YEAR) - pInicialdate.getField(Calendar.YEAR);
        int nMonths = this.getField(Calendar.MONTH) - pInicialdate.getField(Calendar.MONTH);
        int dayone = this.getField(Calendar.DAY_OF_MONTH);
        if (dayone == 31) {
            dayone = 30;
        }
        int daytwo = pInicialdate.getField(Calendar.DAY_OF_MONTH);
        if (daytwo == 31) {
            daytwo = 30;
        }
        int nMonth = this.getField(Calendar.MONTH);
        if (nMonth == 1) { // 0 enero, 1 febrero , 2 marzo
            // si el dia de la fecha es igual al maximo dia del mes
            if (this.getField(Calendar.DAY_OF_MONTH) == this.calendar.getActualMaximum(Calendar.DAY_OF_MONTH)) {
                int day = pInicialdate.getField(Calendar.DAY_OF_MONTH);
                if ((day == 30) || (day == 31)) {
                    dayone = 30;
                }
            }
        }
        nMonth = pInicialdate.getField(Calendar.MONTH);
        if (nMonth == 1) { // 0 enero, 1 febrero , 2 marzo
            // si el dia de la fecha es igual al maximo dia del mes
            if (pInicialdate.getField(Calendar.DAY_OF_MONTH) == pInicialdate.calendar.getActualMaximum(Calendar.DAY_OF_MONTH)) {
                int day = this.getField(Calendar.DAY_OF_MONTH);
                if ((day == 30) || (day == 31)) {
                    daytwo = 30;
                }
            }
        }
        nYears = nYears * 12 * 30;
        nMonths = nMonths * 30;
        days = dayone - daytwo;
        days = nYears + nMonths + days;
        /* v_dias_resultado := (coalesce(v_anios,0) * 12 * 30) + (coalesce(v_meses,0) * 30) + (coalesce(v_dia1,0) - coalesce(v_dia2,0)); */
        return days;
    }

    /**
     * Aniadir dias a una Fecha
     * 
     * @param days Numero de dias a aniadir
     */
    public void addYearBased(int days) throws Exception {
        if (this.base.getYearBase() == 365) {
            this.calendar.add(Calendar.DATE, days);
        } else {
            if (Math.abs(days) >= 30) {
                int month = (int) Math.floor(days / 30);
                this.calendar.add(Calendar.MONTH, month);
                this.calendar.add(Calendar.DATE, days % 30);
            } else {
                this.calendar.add(Calendar.DATE, days);
            }
        }
        this.date = new Date(this.calendar.getTime().getTime());
    }

    /**
     * Aniadir dias a una Fecha, amteniendo un dia fijo en la fecha.
     * 
     * @param days Numero de dias a adicionar.
     * @param payDay Fecha de pago.
     * @throws Exception
     */
    public void addDaysBased(int days, Integer payDay) throws Exception {
        if (this.base.getMonthBase() == 365) {
            /*
             * if( days == 31 || this.calendar.getActualMaximum(Calendar.DAY_OF_MONTH) >= days){
             * this.calendar.add(Calendar.MONTH,1); }else{ this.calendar.add(Calendar.DATE, days); }
             */
            this.calendar.add(Calendar.DATE, days);
        } else {
            if (Math.abs(days) >= 30) {
                int diff = 0;
                if (this.calendar.get(Calendar.MONTH) == Calendar.FEBRUARY) {
                    if (this.calendar.get(Calendar.DATE) >= 28) {
                        if ((payDay != null) && (this.calendar.getActualMaximum(Calendar.DAY_OF_MONTH) < payDay)) {
                            // diff= (this.calendar.getActualMaximum(Calendar.DAY_OF_MONTH)==29)?1:2;
                            diff = payDay - this.calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
                        }
                    }
                }
                int month = (int) Math.floor(days / 30);
                this.calendar.add(Calendar.MONTH, month);
                int d = days % 30;
                this.calendar.add(Calendar.DATE, d);
                if (diff > 0) {
                    this.calendar.add(Calendar.DATE, diff);
                }
            } else {
                this.calendar.add(Calendar.DATE, days);
            }
        }
        this.date = new Date(this.calendar.getTime().getTime());
    }

    /**
     * Aniadir dias a una Fecha
     * 
     * @param days Numero de dias a aniadir
     */
    public void addMonthBased(int days) throws Exception {
        if (this.base.getMonthBase() == 365) {
            this.calendar.add(Calendar.DATE, days);
        } else {
            if (Math.abs(days) >= 30) {
                int diff = 0;
                if (this.calendar.get(Calendar.MONTH) == Calendar.FEBRUARY) {
                    if (this.calendar.get(Calendar.DATE) >= 28) {
                        diff = (this.calendar.getActualMaximum(Calendar.DAY_OF_MONTH) == 29) ? 1 : 2;
                    }
                }
                int month = (int) Math.floor(days / 30);
                this.calendar.add(Calendar.MONTH, month);
                this.calendar.add(Calendar.DATE, days % 30);
                if (diff > 0) {
                    this.calendar.add(Calendar.DATE, diff);
                }
            } else {
                this.calendar.add(Calendar.DATE, days);
            }
        }
        this.date = new Date(this.calendar.getTime().getTime());
    }

    /**
     * Entrega el Valor de base de calculo.
     * 
     * @return CalculationBase.
     */
    public CalculationBase getBase() {
        return this.base;
    }

    /**
     * Fija el valor de base de calculo
     * 
     * @param base Nuevo valor de base.
     */
    public void setBase(CalculationBase base) {
        this.base = base;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return this.formateador.format(this.date).toString();
    }

    public int getField(int pField) throws Exception {
        return this.calendar.get(pField);
    }

    /**
     * Entrega el Valor de calendar.
     * 
     * @return GregorianCalendar.
     */
    public GregorianCalendar getGregorianCalendar() {
        return this.calendar;
    }

    public APPDates dateFrecuency(Frequency pFrecuency) throws Exception {
        this.calendar.add(Calendar.DATE, pFrecuency.getNumDays(this.getDate(), this.formateador));
        return new APPDates(new Date(this.calendar.getTime().getTime()));
    }

    public APPDates nextDateofSequence(Frequency pFrecuency, int pDay) throws Exception {
        int days = pFrecuency.getNumDays(this.getDate(), this.formateador);
        if (days > 31) {
            // TODO Incluiy las frecuanias superiores a mensual
            throw new CommondbException("COMMONDB-0002", "LA FRECUENCIA DEBE SER MENOR O IGUAL A MENSUAL");
        }
        if (pDay > days) {
            if ((pFrecuency == Frequency.MONTHLY) && (pDay == 31)) {
                pDay = days;
            } else {
                throw new CommondbException("COMMONDB-0003", "El DIA {0} NO PUEDE MAYOR AL NUMEOR DE DIAS DE LA FRECUENCIA{1}", pDay, days);
            }
        }
        GregorianCalendar cal = new GregorianCalendar();
        cal.setTime(this.date);
        if (pFrecuency == Frequency.WEEKLY) {
            while (cal.get(Calendar.DAY_OF_WEEK) != pDay) {
                cal.add(Calendar.DAY_OF_WEEK, 1);
            }
            return new APPDates(new Date(cal.getTime().getTime()));
        }
        cal.set(Calendar.DATE, pDay);
        APPLogger.getLogger().debug("Calendario base ==> " + cal.getTime());
        APPLogger.getLogger().debug("Maximo dia del mes ==> " + cal.getActualMaximum(Calendar.DAY_OF_MONTH));
        while (cal.getTime().before(this.date)) {
            cal.add(Calendar.DATE, days);
        }
        while ((cal.get(Calendar.MONTH) != this.getField(Calendar.MONTH)) && (pDay != cal.get(Calendar.DATE))) {
            cal.add(Calendar.DATE, -1);
        }
        if ((cal.get(Calendar.DATE) == 30) && (cal.getActualMaximum(Calendar.DAY_OF_MONTH) == 31)) {
            cal.add(Calendar.DATE, 1);
        }
        return new APPDates(new Date(cal.getTime().getTime()));
    }

    public int getJulianDate() throws Exception {
        GregorianCalendar c = new GregorianCalendar();
        c.setTime(this.date);
        return this.toJulian(c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DATE));
    }

    private int toJulian(int year, int month, int day) throws Exception {
        int julianYear = year;
        if (year < 0) {
            julianYear++;
        }
        int julianMonth = month;
        if (month > 2) {
            julianMonth++;
        } else {
            julianYear--;
            julianMonth += 13;
        }

        double julian = (java.lang.Math.floor(365.25 * julianYear) + java.lang.Math.floor(30.6001 * julianMonth) + day + 1720995.0);

        if ((day + (31 * (month + (12 * year)))) >= JGREG) {
            int ja = (int) (0.01 * julianYear);
            julian += (2 - ja) + (0.25 * ja);
        }
        return (int) Math.floor(julian);
    }

    public static final int JGREG = 15 + (31 * (10 + (12 * 1582)));

    /**
     * Adiciona anios, meses, dias, semanas ..Etc a una fecha.
     * 
     * @param field Constante de anio, mes, dia...Etc
     * @param value Numero de anios, dias, meses a adicionar puese der un valor negativo.
     * @throws Exception
     */
    public void addField(int field, int value) throws Exception {
        getGregorianCalendar().add(field, value);
        this.date = new Date(getGregorianCalendar().getTimeInMillis());
    }

    /**
     * Adiciona anios, meses, dias, semanas ..Etc a una fecha.
     * 
     * @param field Constante de anio, mes, dia...Etc
     * @param value Numero de anios, dias, meses a adicionar puese der un valor negativo.
     * @throws Exception
     */
    public void setField(int field, int value) throws Exception {
        if (field == Calendar.DAY_OF_MONTH) {
            int maxday = getGregorianCalendar().getActualMaximum(Calendar.DAY_OF_MONTH);
            if (value > maxday) {
                value = maxday;
            }
        }
        getGregorianCalendar().set(field, value);
        this.date = new Date(getGregorianCalendar().getTimeInMillis());
    }

    /**
     * Compara dos Dates.
     * 
     * @param o
     * @return
     */
    @Override
    public int compareTo(Object o) {
        if (!(o instanceof APPDates)) {
            return -1;
        }
        APPDates d = (APPDates) o;
        return this.date.compareTo(d.date);
    }

    /**
     * Entrega el dia del anio al correspondiente a la Fecha
     * 
     * @return Dia del Anio
     * @throws Exception
     */
    public int getDayOfYear() throws Exception {
        GregorianCalendar gcal = new GregorianCalendar();
        gcal.setTime(this.date);
        return gcal.get(Calendar.DAY_OF_YEAR);
    }

    /**
     * Entrega lso dias del mes correspondiente a la fecha.
     * 
     * @return int
     * @throws Exception
     */
    public int getDaysOfMonth() throws Exception {
        if (this.base.getMonthBase() == 360) {
            return 30;
        }
        GregorianCalendar gcal = new GregorianCalendar();
        gcal.setTime(this.date);
        return gcal.getActualMaximum(Calendar.DAY_OF_MONTH);
    }

    /**
     * Fija el ultimo dia del mes corespondiente a la fecha.
     * 
     * @return
     * @throws Exception
     */
    public void setLastDateOfMonth() throws Exception {
        Calendar gcal = new GregorianCalendar();
        gcal.setTime(this.date);
        gcal.set(Calendar.DAY_OF_MONTH, this.getDaysOfMonth());
        this.date = new Date(gcal.getTimeInMillis());
    }

    /**
     * Entrega el mes del aņo correspondiente a la Fecha
     * 
     * @return mes del aņo
     * @throws Exception
     */
    public String getMonthOfYear() throws Exception {
        int month = this.getField(Calendar.MONTH) + 1;
        if (month < 10) {
            return "0" + month;
        } else {
            return "" + month;
        }
    }
}
