/* UtilLib - Copyright (c) 2005-2025 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package net.mapeadores.util.primitives;

import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import net.mapeadores.util.text.DateFormatBundle;


/**
 * La classe FuzzyDateInteger sert à stocker sur un entier des informations de
 * date. La date utilise un entier sur 28 bits La valeur de date est entier,
 * soit une FFFF FFFF Année : 1111 1111 1111 1100 0000 0000 0000 Semestre : 0000
 * 0000 0000 0011 0000 0000 0000 Trimestre : 0000 0000 0000 0000 1110 0000 0000
 * Mois : 0000 0000 0000 0000 0001 1110 0000 Jour : 0000 0000 0000 0000 0000
 * 0001 1111
 *
 * @author Vincent Calame
 */
final class FuzzyDateInteger {

    private static final SimpleDateFormat ISO_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

    private FuzzyDateInteger() {
    }

    static short getType(int dateInteger) {
        if (getDay(dateInteger) != 0) {
            return FuzzyDate.DAY_TYPE;
        } else if (getMonth(dateInteger) != 0) {
            return FuzzyDate.MONTH_TYPE;
        } else if (getQuarter(dateInteger) != 0) {
            return FuzzyDate.QUARTER_TYPE;
        } else if (getHalfYear(dateInteger) != 0) {
            return FuzzyDate.HALFYEAR_TYPE;
        }
        return FuzzyDate.YEAR_TYPE;
    }

    static int getYear(int dateInteger) {
        return (dateInteger & 0xFFFC000) >>> 14;
    }

    static int getHalfYear(int dateInteger) {
        return (dateInteger & 0x3000) >>> 12;
    }

    static int getQuarter(int dateInteger) {
        return (dateInteger & 0xE00) >>> 9;
    }

    static int getMonth(int dateInteger) {
        return (dateInteger & 0x1E0) >>> 5;
    }

    static int getDay(int dateInteger) {
        return dateInteger & 0x1F;
    }

    static int truncate(int dateInteger, short dateType) {
        switch (dateType) {
            case FuzzyDate.YEAR_TYPE:
                return (dateInteger & 0xFFFC000);
            case FuzzyDate.HALFYEAR_TYPE:
                return (dateInteger & 0xFFFF000);
            case FuzzyDate.QUARTER_TYPE:
                return (dateInteger & 0xFFFFE00);
            case FuzzyDate.MONTH_TYPE:
                return (dateInteger & 0xFFFFFE0);
            default:
                return dateInteger;
        }
    }

    static boolean check(int dateInteger) {
        if (dateInteger == 0) {
            return false;
        }
        if ((dateInteger & 0xF0000000) != 0) {
            return false;
        }
        int year = getYear(dateInteger);
        if ((year < 1000) && (year > 9999)) {
            return false;
        }
        if ((dateInteger & 0xFFFF) == 0) {
            return true;
        }
        int halfyear = getHalfYear(dateInteger);
        if ((halfyear == 0) || (halfyear == 3)) {
            return false;
        }
        int quarter = getQuarter(dateInteger);
        if ((quarter == 0) || (quarter > 4)) {
            return false;
        }
        if ((halfyear == 1) && (quarter > 2)) {
            return false;
        }
        if ((halfyear == 2) && (quarter < 3)) {
            return false;
        }
        int month = getMonth(dateInteger);
        if ((month == 0) || (month > 12)) {
            return false;
        }
        if ((quarter == 1) && (month > 3)) {
            return false;
        }
        if ((quarter == 2) && ((month < 4) || (month > 6))) {
            return false;
        }
        if ((quarter == 3) && ((month < 7) || (month > 9))) {
            return false;
        }
        if ((quarter == 4) && (month < 10)) {
            return false;
        }
        if (getDay(dateInteger) == 0) {
            return false;
        }
        return true;
    }

    static String toString(int dateInteger) {
        StringBuilder buf = new StringBuilder();
        try {
            appendString(buf, dateInteger);
        } catch (IOException ioe) {
        }
        return buf.toString();
    }

    static void appendString(Appendable buf, int dateInteger) throws IOException {
        if (dateInteger < 1) {
            return;
        }
        int year = getYear(dateInteger);
        buf.append(String.valueOf(year));
        int halfyear = getHalfYear(dateInteger);
        if (halfyear == 0) {
            return;
        }
        int quarter = getQuarter(dateInteger);
        if (quarter == 0) {
            buf.append("-S");
            buf.append(String.valueOf(halfyear));
            return;
        }
        int month = getMonth(dateInteger);
        if (month == 0) {
            buf.append("-T");
            buf.append(String.valueOf(quarter));
            return;
        }
        buf.append('-');
        if (month < 10) {
            buf.append('0');
        }
        buf.append(String.valueOf(month));
        int day = getDay(dateInteger);
        if (day == 0) {
            return;
        }
        buf.append('-');
        if (day < 10) {
            buf.append('0');
        }
        buf.append(String.valueOf(day));
    }

    /**
     * renvoie une chaîne permettant le classement
     */
    static String toSortString(int dateInteger) {
        if (dateInteger < 1) {
            return "";
        }
        StringBuilder buf = new StringBuilder();
        try {
            appendSortString(dateInteger, buf);
        } catch (IOException ioe) {
        }
        return buf.toString();
    }

    private static void appendSortString(int dateInteger, Appendable buf) throws IOException {
        int year = getYear(dateInteger);
        buf.append(String.valueOf(year));
        int halfyear = getHalfYear(dateInteger);
        if (halfyear == 0) {
            return;
        }
        buf.append('S');
        buf.append(String.valueOf(halfyear));
        int quarter = getQuarter(dateInteger);
        if (quarter == 0) {
            return;
        }
        buf.append('Q');
        buf.append(String.valueOf(quarter));
        int month = getMonth(dateInteger);
        if (month == 0) {
            return;
        }
        if (month < 10) {
            buf.append('0');
        }
        buf.append(String.valueOf(month));
        int day = getDay(dateInteger);
        if (day == 0) {
            return;
        }
        if (day < 10) {
            buf.append('0');
        }
        buf.append(String.valueOf(day));
    }

    static int parse(String dateString) throws ParseException {
        if (dateString == null) {
            throw new ParseException("dateString is null", 0);
        }
        int dateLength = dateString.length();
        if (dateLength == 4) {
            int year = parseYear(dateString);
            return (year << 14);
        } else if (dateLength == 7) {
            if (dateString.charAt(4) != '-') {
                throw new ParseException("mauvais separateur apres l'annee", 4);
            }
            int year = parseYear(dateString);
            int result = (year << 14);
            char carac1 = dateString.charAt(5);
            char carac2 = dateString.charAt(6);
            if (carac1 == 'S') {
                if ((carac2 == '1') || (carac2 == '2')) {
                    return result + ((carac2 - 48) << 12);
                } else {
                    throw new ParseException("mauvaise valeur de semestre", 6);
                }
            } else if (carac1 == 'T') {
                return result + parseQuarter(carac2);
            } else if (carac1 == '0') {
                return result + parseMonth(carac2, false);
            } else if (carac1 == '1') {
                return result + parseMonth(carac2, true);
            } else {
                throw new ParseException("mauvaise valeur de mois", 5);
            }

        } else if (dateLength == 10) {
            if (dateString.charAt(4) != '-') {
                throw new ParseException("mauvais separateur apres l'annee", 4);
            }
            if (dateString.charAt(7) != '-') {
                throw new ParseException("mauvais separateur apres le mois", 7);
            }
            int year = parseYear(dateString);
            int result = (year << 14);
            char carac1 = dateString.charAt(5);
            char carac2 = dateString.charAt(6);
            if (carac1 == '0') {
                result = result + parseMonth(carac2, false);
            } else if (carac1 == '1') {
                result = result + parseMonth(carac2, true);
            } else {
                throw new ParseException("mauvaise valeur de mois", 5);
            }
            return result + parseDay(dateString.charAt(8), dateString.charAt(9));
        }
        throw new ParseException("taille de la chaine incorrecte", dateLength - 1);
    }

    private static int parseQuarter(char carac) throws ParseException {
        int result;
        int numero = carac - 48;
        if ((numero < 1) || (numero > 4)) {
            throw new ParseException("mauvaise valeur de trimestre", 6);
        }
        result = (numero << 9);
        if (numero < 3) {
            result = result + (1 << 12);
        } else {
            result = result + (2 << 12);
        }
        return result;
    }

    private static int parseMonth(char carac, boolean avecdizaine) throws ParseException {
        int result;
        if (avecdizaine) {
            if ((carac < '0') || (carac > '2')) {
                throw new ParseException("mauvaise valeur de mois", 5);
            }
        } else if ((carac < '1') || (carac > '9')) {
            throw new ParseException("mauvaise valeur de mois", 6);
        }
        int month = carac - 48;
        if (avecdizaine) {
            month = month + 10;
            result = (month << 5) + (2 << 12) + (4 << 9);
        } else {
            result = (month << 5);
            if (month < 4) {
                result = result + (1 << 12) + (1 << 9);
            } else if (month < 7) {
                result = result + (1 << 12) + (2 << 9);
            } else {
                result = result + (2 << 12) + (3 << 9);
            }
        }
        return result;
    }

    private static int parseDay(char carac1, char carac2) throws ParseException {
        if ((carac1 < '0') || (carac1 > '3')) {
            throw new ParseException("mauvaise valeur de jour", 8);
        }
        if ((carac2 < '0') || (carac2 > '9')) {
            throw new ParseException("mauvaise valeur de jour", 9);
        }
        return (carac2 - 48) + ((carac1 - 48) * 10);
    }

    private static int parseYear(String chaine) throws ParseException {
        int year = 0;
        for (int i = 0; i < 4; i++) {
            char carac = chaine.charAt(i);
            if ((carac < '0') || (carac > '9')) {
                throw new ParseException("mauvais caractere dans l'annee", i);
            }
            if ((i == 0) && (carac == '0')) {
                throw new ParseException("annee inferieure a 1000", i);
            }
            year = year + ((int) Math.pow(10, (3 - i))) * (carac - 48);
        }
        return year;
    }

    static int fromYear(int year) {
        if ((year < 1000) || (year > 9999)) {
            throw new IllegalArgumentException("wrong year");
        }
        return (year << 14);
    }

    static int fromHalfYear(int year, int halfyear) {
        if ((year < 1000) || (year > 9999)) {
            throw new IllegalArgumentException("wrong year");
        }
        if ((halfyear < 1) || (halfyear > 2)) {
            throw new IllegalArgumentException("wrong half year");
        }
        int result = (year << 14) + (halfyear << 12);
        return result;
    }

    static int fromQuarter(int year, int quarter) {
        if ((year < 1000) || (year > 9999)) {
            throw new IllegalArgumentException("wrong year");
        }
        if ((quarter < 1) || (quarter > 4)) {
            throw new IllegalArgumentException("wrong quarter");
        }
        int result = (year << 14) + (quarter << 9);
        if (quarter < 3) {
            result = result + (1 << 12);
        } else {
            result = result + (2 << 12);
        }
        return result;
    }

    static int fromMonth(int year, int month) {
        if ((year < 1000) || (year > 9999)) {
            throw new IllegalArgumentException("wrong year");
        }
        if ((month < 1) || (month > 12)) {
            throw new IllegalArgumentException("wrong month");
        }
        return fromInt(year, month, -1);
    }

    static int fromDay(int year, int month, int day) {
        if ((year < 1000) || (year > 9999)) {
            throw new IllegalArgumentException("wrong year");
        }
        if ((month < 1) || (month > 12)) {
            throw new IllegalArgumentException("wrong month");
        }
        if ((day < 1) || (day > 31)) {
            throw new IllegalArgumentException("wrong day");
        }
        return fromInt(year, month, day);
    }

    private static int fromInt(int year, int month, int day) {
        int result = (year << 14);
        result = result + (month << 5);
        if (day != -1) {
            result = result + day;
        }
        if (month < 4) {
            result = result + (1 << 12) + (1 << 9);
        } else if (month < 7) {
            result = result + (1 << 12) + (2 << 9);
        } else if (month < 10) {
            result = result + (2 << 12) + (3 << 9);
        } else {
            result = result + (2 << 12) + (4 << 9);
        }
        return result;
    }

    static GregorianCalendar toGregorianCalendar(int dateInteger) {
        GregorianCalendar gc = new GregorianCalendar();
        int year = getYear(dateInteger);
        short type = getType(dateInteger);
        int month = 1;
        int day = 1;
        switch (type) {
            case FuzzyDate.HALFYEAR_TYPE:
                int halfyear = getHalfYear(dateInteger);
                if (halfyear == 2) {
                    month = 7;
                }
                break;
            case FuzzyDate.QUARTER_TYPE:
                int quarter = getQuarter(dateInteger);
                if (quarter == 2) {
                    month = 4;
                } else if (quarter == 3) {
                    month = 7;
                } else if (quarter == 4) {
                    month = 10;
                }
                break;
            case FuzzyDate.MONTH_TYPE:
                month = getMonth(dateInteger);
                break;
            case FuzzyDate.DAY_TYPE:
                month = getMonth(dateInteger);
                day = getDay(dateInteger);
                break;
        }
        gc.set(year, month - 1, day);
        return gc;
    }

    static String toISOString(int dateInteger, boolean lastDate) {
        StringBuilder buf = new StringBuilder();
        try {
            appendCompleteString(buf, dateInteger, true, lastDate);
        } catch (IOException ioe) {
        }
        return buf.toString();
    }

    /**
     * Retourne sous la forme année-month en forçant le month. Si dernierMois
     * est égal à true, c'est le dernier month de la période considérée qui est
     * pris en compte.
     */
    static String toMonthString(int dateInteger, boolean lastMonth) {
        StringBuilder buf = new StringBuilder();
        try {
            appendCompleteString(buf, dateInteger, false, lastMonth);
        } catch (IOException ioe) {
        }
        return buf.toString();
    }

    static void appendCompleteString(Appendable buf, int dateInteger, boolean withDay, boolean lastDate) throws IOException {
        int year = getYear(dateInteger);
        buf.append(String.valueOf(year));
        buf.append('-');
        short type = getType(dateInteger);
        int month = (lastDate) ? 12 : 1;
        int day = -1;
        switch (type) {
            case FuzzyDate.HALFYEAR_TYPE:
                int halfyear = getHalfYear(dateInteger);
                if (lastDate) {
                    if (halfyear == 1) {
                        month = 6;
                    }
                } else {
                    if (halfyear == 2) {
                        month = 7;
                    }
                }
                break;
            case FuzzyDate.QUARTER_TYPE:
                int quarter = getQuarter(dateInteger);
                if (lastDate) {
                    if (quarter == 1) {
                        month = 3;
                    } else if (quarter == 2) {
                        month = 6;
                    } else if (quarter == 3) {
                        month = 9;
                    }
                } else {
                    if (quarter == 2) {
                        month = 4;
                    } else if (quarter == 3) {
                        month = 7;
                    } else if (quarter == 4) {
                        month = 10;
                    }
                }
                break;
            case FuzzyDate.MONTH_TYPE:
                month = getMonth(dateInteger);
                break;
            case FuzzyDate.DAY_TYPE:
                month = getMonth(dateInteger);
                if (withDay) {
                    day = getDay(dateInteger);
                }
                break;
        }
        if (month < 10) {
            buf.append("0");
        }
        buf.append(String.valueOf(month));
        if (withDay) {
            if (day == -1) {
                if (lastDate) {
                    day = DateUtils.getLastDayOfMonth(month, year);
                } else {
                    day = 1;
                }
            }
            buf.append("-");
            if (day < 10) {
                buf.append("0");
            }
            buf.append(String.valueOf(day));
        }
    }

    static LocalDate toLocaleDate(int dateInteger, boolean lastDate) {
        int year = getYear(dateInteger);
        short type = getType(dateInteger);
        int month = (lastDate) ? 12 : 1;
        int day = -1;
        switch (type) {
            case FuzzyDate.HALFYEAR_TYPE:
                int halfyear = getHalfYear(dateInteger);
                if (lastDate) {
                    if (halfyear == 1) {
                        month = 6;
                    }
                } else {
                    if (halfyear == 2) {
                        month = 7;
                    }
                }
                break;
            case FuzzyDate.QUARTER_TYPE:
                int quarter = getQuarter(dateInteger);
                if (lastDate) {
                    if (quarter == 1) {
                        month = 3;
                    } else if (quarter == 2) {
                        month = 6;
                    } else if (quarter == 3) {
                        month = 9;
                    }
                } else {
                    if (quarter == 2) {
                        month = 4;
                    } else if (quarter == 3) {
                        month = 7;
                    } else if (quarter == 4) {
                        month = 10;
                    }
                }
                break;
            case FuzzyDate.MONTH_TYPE:
                month = getMonth(dateInteger);
                break;
            case FuzzyDate.DAY_TYPE:
                month = getMonth(dateInteger);
                day = getDay(dateInteger);
                break;
        }
        if (day == -1) {
            if (lastDate) {
                day = DateUtils.getLastDayOfMonth(month, year);
            } else {
                day = 1;
            }
        }
        return LocalDate.of(year, month, day);
    }

    static int fromGregorianCalendar(GregorianCalendar gregcalendar) {
        return fromInt(gregcalendar.get(Calendar.YEAR), gregcalendar.get(Calendar.MONTH) + 1, gregcalendar.get(Calendar.DAY_OF_MONTH));
    }

    static int fromDate(Date date) {
        GregorianCalendar gc = new GregorianCalendar();
        gc.setTime(date);
        return fromGregorianCalendar(gc);
    }

    /**
     * Raccourci pour toGregorianCalendar().getTime()
     */
    static Date toDate(int dateInteger) {
        return toGregorianCalendar(dateInteger).getTime();
    }

    static int current() {
        return fromGregorianCalendar(new GregorianCalendar());
    }

    static String getDateLitteral(int dateInteger, DateFormatBundle dateFormatBundle) {
        short type = getType(dateInteger);
        DateFormat dateformat = null;
        switch (type) {
            case FuzzyDate.DAY_TYPE:
                dateformat = dateFormatBundle.getFormatJour();
                break;
            case FuzzyDate.MONTH_TYPE:
                dateformat = dateFormatBundle.getFormatMois();
                break;
            case FuzzyDate.YEAR_TYPE:
                dateformat = dateFormatBundle.getFormatAnnee();
                break;
            case FuzzyDate.HALFYEAR_TYPE:
                int halfyear = getHalfYear(dateInteger);
                return dateFormatBundle.getLocalizedString("s" + String.valueOf(halfyear)) + " " + String.valueOf(getYear(dateInteger));
            case FuzzyDate.QUARTER_TYPE:
                int quarter = getQuarter(dateInteger);
                return dateFormatBundle.getLocalizedString("t" + String.valueOf(quarter)) + " " + String.valueOf(getYear(dateInteger));
        }
        if (dateformat != null) {
            return dateformat.format(toDate(dateInteger));
        }
        return toString(dateInteger);
    }

    /**
     *
     * @throws IllegalArgumentException si s est nul ou vide
     */
    static int parse(String s, DateFormatBundle dateFormatBundle) throws ParseException {
        if ((s == null) || (s.length() == 0)) {
            throw new IllegalArgumentException("datation string is null");
        }
        try {
            int dateInteger = parse(s);
            return dateInteger;
        } catch (ParseException dee) {
            if (dateFormatBundle != null) {
                try {
                    Date date = dateFormatBundle.getParseFormat().parse(s);
                    return fromDate(date);
                } catch (ParseException e) {
                }
            }
            int idx = s.indexOf('/');
            if (idx > 0) {
                Date date = ISO_FORMAT.parse(s.substring(idx + 1) + "-" + s.substring(0, idx) + "-01");
                int dateInteger = fromDate(date);
                return truncate(dateInteger, FuzzyDate.MONTH_TYPE);
            } else {
                throw new ParseException("unable to parse", 0);
            }
        }

    }

    static int roll(int dateInteger, int dayAmount) {
        if (dayAmount == 0) {
            return dateInteger;
        } else if (dayAmount < 0) {
            return rollToPast(dateInteger, -dayAmount);
        } else {
            return rollToFuture(dateInteger, dayAmount);
        }
    }

    private static int rollToPast(int dateInteger, int amount) {
        GregorianCalendar cal = toGregorianCalendar(dateInteger);
        int dayOfYear = cal.get(Calendar.DAY_OF_YEAR);
        if (dayOfYear - amount > 0) {
            cal.roll(Calendar.DAY_OF_YEAR, -amount);
            return fromGregorianCalendar(cal);
        } else {
            amount = amount - dayOfYear;
            int newDateInteger = fromInt(getYear(dateInteger) - 1, 12, 31);
            if (amount == 0) {
                return newDateInteger;
            } else {
                return rollToPast(newDateInteger, amount);
            }
        }
    }

    private static int rollToFuture(int dateInteger, int amount) {
        GregorianCalendar cal = toGregorianCalendar(dateInteger);
        int dayOfYear = cal.get(Calendar.DAY_OF_YEAR);
        int yearLength = 365;
        if (cal.isLeapYear(cal.get(Calendar.YEAR))) {
            yearLength = 366;
        }
        if (amount + dayOfYear <= yearLength) {
            cal.roll(Calendar.DAY_OF_YEAR, amount);
            return fromGregorianCalendar(cal);
        } else {
            amount = amount + dayOfYear - yearLength - 1;
            int newDateInteger = fromInt(getYear(dateInteger) + 1, 1, 1);
            if (amount == 0) {
                return newDateInteger;
            } else {
                return rollToFuture(newDateInteger, amount);
            }
        }
    }


}
