package de.duehl.basics.datetime.time;

/*
 * Copyright 2021 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.List;

import de.duehl.basics.text.NumberString;
import de.duehl.basics.text.Text;

/**
 * Eine Klasse zur Darstellung einer Uhrzeit.
 *
 * @version 1.01     2021-03-17
 * @author Christian Dühl
 */

public class ImmutualTime {

    /** Stunden (0-23) */
    private final int hour;

    /** Minuten (0-59) */
    private final int minute;

    /** Sekunden (0-59) */
    private final int second;

    /** Übertrag in Tagen bei Rechnungen (darf positiv, Null und negativ sein). */
    private final int dayCarryOver;

    /**
     * Konstruktor, erzeugt den Zeitpunkt Mitternacht (00:00:00).                           <br><br>
     *
     * Wenn man die aktuelle Uhrzeit haben möchte, sollte man
     * TimeHelper.actualTimeAsString() verwenden.                                           <br><br>
     *
     * Aber Achtung, will man die aktuelle Uhrzeit zusammen mit dem
     * aktuellen Datum, dann sollte man sich diese nicht einzeln
     * Beschaffen, wegen eventuellen Problemen um Mitternacht, wenn
     * das Datum zum einen und die Uhrzeit zum anderen tag gehört,
     * sondern den Konstruktor DateAndTime() nutzen.
     */
    public ImmutualTime() {
        this(0, 0, 0, 0);
    }

    /**
     * Konstruktor.
     *
     * @param hour
     *            Stunde (0-23)
     * @param minute
     *            Minute (0-59)
     * @param second
     *            Sekunde (0-59)
     * @throws IllegalArgumentException
     *             Falls die Stunden, Minuten oder Sekunden zu klein oder zu groß sind.
     */
    public ImmutualTime(int hour, int minute, int second) {
        this(hour, minute, second, 0);
    }

    /**
     * Konstruktor.
     *
     * @param hour
     *            Stunde (0-23)
     * @param minute
     *            Minute (0-59)
     * @param second
     *            Sekunde (0-59)
     * @param dayCarryOver
     *            Übertrag in Tagen bei Rechnungen (darf positiv, Null und negativ sein).
     * @throws IllegalArgumentException
     *             Falls die Stunden, Minuten oder Sekunden zu klein oder zu groß sind.
     */
    ImmutualTime(int hour, int minute, int second, int dayCarryOver) {
        checkTime(hour, minute, second);
        this.hour = hour;
        this.minute = minute;
        this.second = second;
        this.dayCarryOver = dayCarryOver;
    }

    /**
     * Konstruktor für das Format hh:mm:ss und hh:mm.
     *
     * @param time
     *            Zeitangabe im Format hh:mm:ss oder hh:mm.
     * @throws IllegalArgumentException
     *             Falls die Stunden, Minuten oder Sekunden zu klein oder zu groß sind.
     */
    public ImmutualTime(String time) {
        List<String> parts = Text.splitByColon(time);

        for (String part : parts) {
            if (!NumberString.isDigitSequence(part)) {
                throw new IllegalArgumentException("Unverständliche Zeitangabe '" + time + "'.");
            }
        }

        if (parts.size() == 2) {
            parts.add("0");
        }

        if (parts.size() != 3) {
            throw new IllegalArgumentException("Unverständliche Zeitangabe '" + time + "'.");
        }

        int hour = NumberString.parseInt(parts.get(0),
                "Die Stunde in '" + time + "' lässt sich nicht parsen!");
        int minute = NumberString.parseInt(parts.get(1),
                "Die Minute in '" + time + "' lässt sich nicht parsen!");
        int second = NumberString.parseInt(parts.get(2),
                "Die Sekunde in '" + time + "' lässt sich nicht parsen!");
        checkTime(hour, minute, second);
        this.hour = hour;
        this.minute = minute;
        this.second = second;
        dayCarryOver = 0;
    }

    private void checkTime(int hour, int minute, int second) {
        if (hour < 0) {
            throw new IllegalArgumentException("Die Anzahl der Stunden darf nicht negativ sein.");
        }
        if (hour > 23) {
            throw new IllegalArgumentException("Die Anzahl der Stunden darf höchstens 23 sein.");
        }
        if (minute < 0) {
            throw new IllegalArgumentException("Die Anzahl der Minuten darf nicht negativ sein.");
        }
        if (minute > 59) {
            throw new IllegalArgumentException("Die Anzahl der Minuten darf höchstens 59 sein.");
        }
        if (second < 0) {
            throw new IllegalArgumentException("Die Anzahl der Sekunden darf nicht negativ sein.");
        }
        if (second > 59) {
            throw new IllegalArgumentException("Die Anzahl der Sekunden darf höchstens 59 sein.");
        }
    }

    /** Getter für die Stunde (0-23) */
    public int getHour() {
        return hour;
    }

    /** Getter für die Minute (0-59) */
    public int getMinute() {
        return minute;
    }

    /** Getter für die Sekunde (0-59) */
    public int getSecond() {
        return second;
    }

    /** Getter für den Übertrag in Tagen bei Rechnungen (darf positiv, Null und negativ sein). */
    public int getDayCarryOver() {
        return dayCarryOver;
    }

    /** Getter für die Stunde in zweistelliger Darstellung ("00"-"23") */
    public String getHourAsTwoDigitString() {
        String hoursString = Integer.toString(hour);
        if (hoursString.length() < 2) {
            hoursString = "0" + hoursString;
        }
        return hoursString;
    }

    /** Getter für die Minute in zweistelliger Darstellung ("00"-"59") */
    public String getMinuteAsTwoDigitString() {
        String minutesString = Integer.toString(minute);
        if (minutesString.length() < 2) {
            minutesString = "0" + minutesString;
        }
        return minutesString;
    }

    /** Getter für die Sekunde in zweistelliger Darstellung ("00"-"59") */
    public String getSecondAsTwoDigitString() {
        String secondsString = Integer.toString(second);
        if (secondsString.length() < 2) {
            secondsString = "0" + secondsString;
        }
        return secondsString;
    }

    /**
     * Erzeugt eine Darstellung der gesamten Zeit im Format "hh:mm:ss". Falls ein Übertrag aus
     * Rechnungen vorliegt, so wird dieser in Klammern dahinter angezeigt.
     */
    @Override
    public String toString() {
        String timePart = getHourAsTwoDigitString() + ":" + getMinuteAsTwoDigitString() + ":"
                + getSecondAsTwoDigitString();
        return timePart + createCarryOverExtension();
    }

    private String createCarryOverExtension() {
        if (dayCarryOver == 0) {
            return "";
        }
        else if (dayCarryOver == 1) {
            return " (+1 Tag)";
        }
        else if (dayCarryOver > 1) {
            return " (+" + dayCarryOver + " Tage)";
        }
        else if (dayCarryOver == -1) {
            return " (-1 Tag)";
        }
        else if (dayCarryOver < -1) {
            return " (" + dayCarryOver + " Tage)";
        }
        else {
            throw new RuntimeException("Darf nicht auftreten!");
        }
    }

    /**
     * Erzeugt eine Darstellung der gesamten Zeit im Format "hh:mm". Falls ein Übertrag aus
     * Rechnungen vorliegt, so wird dieser in Klammern dahinter angezeigt.
     */
    public String toStringWithoutSeconds() {
        String timePart = getHourAsTwoDigitString() + ":" + getMinuteAsTwoDigitString();
        return timePart + createCarryOverExtension();
    }

    /** Addiert eine positive oder negative Anzahl an Stunden. */
    public ImmutualTime addHours(int hours) {
        int newHour = hour + hours;
        int newDayCarryOver = dayCarryOver;
        while (newHour > 23) {
            newHour -= 24;
            ++newDayCarryOver;
        }
        while (newHour < 0) {
            newHour += 24;
            --newDayCarryOver;
        }
        return new ImmutualTime(newHour, minute, second, newDayCarryOver);
    }

    /** Addiert eine positive oder negative Anzahl an Minuten. */
    public ImmutualTime addMinutes(int minutes) {
        int newMinute = minute + minutes;
        int addHours = 0;
        while (newMinute > 59) {
            newMinute -= 60;
            ++addHours;
        }
        while (newMinute < 0) {
            newMinute += 60;
            --addHours;
        }
        ImmutualTime tempTime = new ImmutualTime(hour, newMinute, second, dayCarryOver);
        return tempTime.addHours(addHours);
    }

    /** Addiert eine positive oder negative Anzahl an Sekunden. */
    public ImmutualTime addSeconds(int seconds) {
        int newSecond = second + seconds;
        int addMinutes = 0;
        while (newSecond > 59) {
            newSecond -= 60;
            ++addMinutes;
        }
        while (newSecond < 0) {
            newSecond += 60;
            --addMinutes;
        }
        ImmutualTime tempTime = new ImmutualTime(hour, minute, newSecond, dayCarryOver);
        return tempTime.addMinutes(addMinutes);
    }

    /** "Vergisst" den Übertrag in Tagen bei Rechnungen. */
    public ImmutualTime forgetDayCarryOver() {
        return new ImmutualTime(hour, minute, second);
    }

    /**
     * Berechnet die Differenz zum übergebenen Zeitpunkt in Sekunden. Wenn der andere Zeitpunkt
     * später ist, ist die Differenz positiv, ist der andere Zeitpunkt früher, ist sie negativ.
     *
     * @throws RuntimeException
     *             Falls der Aufrufer (dieses Objekt) einen Übertrag aufweist.
     * @throws IllegalArgumentException
     *             Falls das Argument einen Übertrag aufweist.
     */
    public int difference(ImmutualTime that) {
        if (this.dayCarryOver != 0) {
            throw new RuntimeException("Keine Differenz bei vorhandendem Übertrag im Aufrufer!");
        }
        if (that.dayCarryOver != 0) {
            throw new IllegalArgumentException("Keine Differenz bei vorhandendem Übertrag im "
                    + "Argument!");
        }

        int secDiff = that.second - this.second;
        int minDiff = 60 * (that.minute - this.minute);
        int hourDiff = 60 * 60 * (that.hour - this.hour);

        return hourDiff + minDiff + secDiff;
    }

    /** Gibt an, ob diese Zeit einen früheren Zeitpunkt meint als die übergebene Zeit. */
    public boolean before(ImmutualTime that) {
        int difference = difference(that);
        return difference > 0;
    }

    /** Gibt an, ob diese Zeit einen späteren Zeitpunkt meint als die übergebene Zeit. */
    public boolean after(ImmutualTime that) {
        int difference = difference(that);
        return difference < 0;
    }

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

    /**
     * Stellt sicher, dass kein Übertrag in Tagen von Rechnungen vorliegt.
     *
     * @throws RuntimeException
     *             falls der Übertrag nicht null ist.
     */
    public void checkNoDayCarryOver() {
        if (dayCarryOver != 0) {
            throw new RuntimeException("Keine Differenz erlaubt!");
        }
    }

    /** Stringrepräsentation im Format HHMMSS. */
    public String asHhMmSs() {
        return String.format("%02d%02d%02d", hour, minute, second);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + dayCarryOver;
        result = prime * result + hour;
        result = prime * result + minute;
        result = prime * result + second;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        ImmutualTime other = (ImmutualTime) obj;
        if (dayCarryOver != other.dayCarryOver) {
            return false;
        }
        if (hour != other.hour) {
            return false;
        }
        if (minute != other.minute) {
            return false;
        }
        if (second != other.second) {
            return false;
        }
        return true;
    }

}
