package de.duehl.basics.datetime;

/*
 * Copyright 2024 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.util.Calendar;

import de.duehl.basics.datetime.date.DateHelper;
import de.duehl.basics.datetime.date.ImmutualDate;
import de.duehl.basics.datetime.time.ImmutualTime;
import de.duehl.basics.datetime.time.TimeHelper;

/**
 * Diese Klasse stellt ein Datum und eine Uhrzeit dar.
 *
 * @version 1.01     2024-04-26
 * @author Christian Dühl
 */

public class DateAndTime {

    static final long SECONDS_OF_ONE_DAY = 60 * 60 * 24;

    /** Datum. */
    private final ImmutualDate date;

    /** Uhrzeit. */
    private final ImmutualTime time;

    /**
     * Konstruktor.
     *
     * @param date
     *            Datum.
     * @param time
     *            Uhrzeit.
     */
    public DateAndTime(ImmutualDate date, ImmutualTime time) {
        this.date = date;
        this.time = time;
    }

    /** Konstruktor mit dem aktuellen Datum und der aktuellen Uhrzeit. */
    public DateAndTime() {
        Calendar cal = Calendar.getInstance();
        date = DateHelper.calendarToDate(cal);
        time = TimeHelper.calendarToTime(cal);
        /*
         * Anmerkungen:
         *
         * 1) Das aktuelle Datum bekommt man auch mit
         *        date = new ImmutualDate();
         *    , aber mit
         *        time = new ImmutualTime();
         *    nicht die aktuelle Zeit, denn new ImmutualTime() liefert Mitternacht zurück.
         *
         * 2) Außerdem vermeidet man mit dem einen Kalender-Objekt Probleme beim Tageswechsel um
         *    Mitternacht, wenn das Datum zum einen und die Uhrzeit zum anderen Tag gehört.
         */
    }

    /**
     * Konstruktor.
     *
     * @param timeMillis
     *            the new time in UTC milliseconds from the epoch.
     */
    public DateAndTime(long timeMillis) {
        Calendar cal = Calendar.getInstance();
        cal.setTimeInMillis(timeMillis);
        date = DateHelper.calendarToDate(cal);
        time = TimeHelper.calendarToTime(cal);
    }

    /** Getter für das Datum. */
    public ImmutualDate getDate() {
        return date;
    }

    /** Getter für die Uhrzeit. */
    public ImmutualTime getTime() {
        return time;
    }

    /** Addiert eine positive oder negative Anzahl an Sekunden. */
    public DateAndTime addSeconds(int seconds) {
        time.checkNoDayCarryOver();
        ImmutualTime newTime = time.addSeconds(seconds);
        int dayCarryOver = newTime.getDayCarryOver();
        newTime = newTime.forgetDayCarryOver();

        ImmutualDate newDate = date.addDays(dayCarryOver);

        return new DateAndTime(newDate, newTime);
    }

    /** Addiert eine positive oder negative Anzahl an Minuten. */
    public DateAndTime addMinutes(int minutes) {
        ImmutualTime newTime = time.addMinutes(minutes);
        int dayCarryOver = newTime.getDayCarryOver();
        newTime = newTime.forgetDayCarryOver();

        ImmutualDate newDate = date.addDays(dayCarryOver);

        return new DateAndTime(newDate, newTime);
    }

    /** Addiert eine positive oder negative Anzahl an Minuten. */
    public DateAndTime addHours(int hours) {
        ImmutualTime newTime = time.addHours(hours);
        int dayCarryOver = newTime.getDayCarryOver();
        newTime = newTime.forgetDayCarryOver();

        ImmutualDate newDate = date.addDays(dayCarryOver);

        return new DateAndTime(newDate, newTime);
    }

    /**
     * Addiert (oder subtrahiert bei negativen Zahlen) die angegebene Anzahl Tage zum Datum.
     *
     * @param delta
     *            Anzahl Tage, die addiert (oder bei negativer Anzahl subtrahiert) werden sollen.
     * @return Nach Addition oder Subtraktion normalisiertes Datum mit der gleichen Uhrzeit.
     */
    public DateAndTime addDays(int days) {
        ImmutualDate newDate = date.addDays(days);
        return new DateAndTime(newDate, time);
    }

    /**
     * Addiert (oder subtrahiert bei negativen Zahlen) die angegebene Anzahl Monate zum Datum.
     *
     * @param numberOfMonths
     *            Anzahl Monate, die addiert (oder bei negativer Anzahl subtrahiert) werden sollen.
     * @return Nach Addition oder Subtraktion normalisiertes Datum.
     */
    public DateAndTime addMonths(int numberOfMonhs) {
        ImmutualDate newDate = date.addMonths(numberOfMonhs);
        return new DateAndTime(newDate, time);
    }

    /**
     * Addiert (oder subtrahiert bei negativen Zahlen) die angegebene Anzahl Jahre zum Datum.
     *
     * @param numberOfYears
     *            Anzahl Jahre, die addiert (oder bei negativer Anzahl subtrahiert) werden sollen.
     * @return Nach Addition oder Subtraktion normalisiertes Datum.
     */
    public DateAndTime addYears(int years) {
        ImmutualDate newDate = date.addYears(years);
        return new DateAndTime(newDate, time);
    }

    /**
     * Ermittelt, ob der Zeitpunkt vor dem angegebenen Zeitpunkt liegt.
     *
     * @param that
     *            Vergleichsdatum
     * @return Wahrheitswert. Sind beide Datumswerte gleich, wird false zurückgegeben.
     */
    public boolean before(DateAndTime that) {
        if (this.date.before(that.date)) {
            return true;
        }
        if (that.date.before(this.date)) {
            return false;
        }
        return this.time.before(that.time);
    }

    /**
     * Ermittelt, ob der Zeitpunkt nach dem angegebenen Zeitpunkt liegt.
     *
     * @param that
     *            Vergleichsdatum
     * @return Wahrheitswert. Sind beide Datumswerte gleich, wird false zurückgegeben.
     */
    public boolean after(DateAndTime that) {
        return that.before(this);
    }


    /**
     * Ermittelt, ob der Zeitpunkt im Bereich der beiden angegebenen Zeitpunkte liegt.
     *
     * @param start
     *            Erster erlaubter Zeitpunkt.
     * @param end
     *            Letzter erlaubter Zeitpunkt.
     * @return Wahrheitswert.
     */
    public boolean liesBetween(DateAndTime start, DateAndTime end) {
        return start.equals(this) || end.equals(this) ||
                start.before(this) && this.before(end);
    }

    /**
     * Berechnet den Abstand in Sekunden zu dem gegebenen Datum. Ist es später als dieses, ist der
     * Wert positiv.
     */
    public long difference(DateAndTime that) {
        int dayDifference = this.date.difference(that.date);
        int secondsDifference = this.time.difference(that.time);
        long difference = SECONDS_OF_ONE_DAY * dayDifference + secondsDifference;
        return difference;
    }

    /**
     * Setzt die Sekunden auf den Anfang der Minute zurück, der Rest der Uhrzeit bleibt
     * unverändert.
     */
    public DateAndTime setSecondsToZero() {
        return new DateAndTime(date, time.setSecondsToZero());
    }

    /**
     * Gibt den Zeitpunkt aus Datum und Uhrzeit als Millisekunden seit Epoch (01.01.1970) zurück.
     */
    public long toEpoch() {
        String dateString = date.toString(); // Format "DD.MM.YYYY"
        String timeString = time.toString(); // Format "hh:mm:ss"

        return DateAndTimeHelper.toEpoch(dateString, timeString);
    }

    /** Stringrepräsentation im Format "20130722" + delimiter + "HHMMSS". */
    public String asYyyyMmDdDelimiterHhMmSs(String delimiter) {
        return date.asYyyyMmDd() + delimiter + time.asHhMmSs();
    }

    /**
     * Gibt einen String mit dem Datum im Format "TT.MM.YYYY" gefolgt von dem übergebenen Trenner
     * und der Uhrzeit im Format "hh:mm:ss" zurück.
     */
    public String toString(String delimiter) {
        return date.toString() + delimiter + time.toString();
    }

    /** Gibt einen String im Format "TT.MM.YYYY, hh:mm:ss" zurück. */
    @Override
    public String toString() {
        return toString(", ");
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((date == null) ? 0 : date.hashCode());
        result = prime * result + ((time == null) ? 0 : time.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        DateAndTime other = (DateAndTime) obj;
        if (date == null) {
            if (other.date != null) {
                return false;
            }
        }
        else if (!date.equals(other.date)) {
            return false;
        }
        if (time == null) {
            if (other.time != null) {
                return false;
            }
        }
        else if (!time.equals(other.time)) {
            return false;
        }
        return true;
    }

}
