package de.duehl.basics.version;

/*
 * 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.ArrayList;
import java.util.List;

import de.duehl.basics.io.FileHelper;
import de.duehl.basics.text.Text;

/**
 * Diese Klasse dient Informationsquelle über Datum und Version des Programms.
 *
 * @version 1.03     2024-11-11
 * @author Christian Dühl
 */

public final class Version implements Comparable<Version> {

    private static final String SERIALIZATION_SEPARATOR = "#;#";

    /** Version als String. */
    private final String version;

    /** Datum als String. */
    private final String date;

    /** Interne Darstellung der Version als Liste von Integern. */
    private final List<Integer> numericVersion;

    /**
     * Konstruktor
     *
     * @param version
     *            Version als String.
     * @param date
     *            Datum als String.
     */
    public Version(String version, String date) {
        this.date = date;
        this.version = version;
        numericVersion = new ArrayList<>();
        initNumericVersion(version);
    }

    /**
     * Erzeugt aus der String-Version eine Liste ihrer interessanten Teile in
     * Integer-Representation.
     *
     * @param versionString
     *            Version als String
     */
    private void initNumericVersion(String versionString) {
        /* 1. Aufteilung an nicht-(Zahlen und Buchstaben): */
        String[] preParts = versionString.split("[^0-9a-zA-Z]+");

        /* 2. Teile in Zahlen und einzelne Buchstaben aufteilen: */
        for (String prePart : preParts) {
            initNumericVersionPart(prePart);
        }
    }

    private void initNumericVersionPart(String prePart) {
        /* 2.1 reine Ziffernfolge: */
        if (prePart.matches("[0-9]+")) {
            initNumericVersionNumericalPart(prePart);
        }
        /* 2.2 reine alphanumerische Zeichen: */
        else if (prePart.matches("[a-zA-Z]+")) {
            initNumericVersionAlphanumericalPart(prePart);
        }
        /* 2.3 gemischte Zeichen: */
        else {
             initNumericVersionMixedPart(prePart);
        }
    }

    private void initNumericVersionNumericalPart(String prePart) {
        int number = Integer.parseInt(prePart);
        numericVersion.add(number);
    }

    private void initNumericVersionAlphanumericalPart(String prePart) {
        char[] chars = prePart.toCharArray();
        for (char c : chars) {
            initNumericVersionAlphanumericalChar(c);
        }
    }

    private void initNumericVersionAlphanumericalChar(char c) {
        int i = c;
        numericVersion.add(i);
    }

    private void initNumericVersionMixedPart(String prePart) {
        boolean numerical = false;
        char[] chars = prePart.toCharArray();
        StringBuilder builder = new StringBuilder();
        for (char c : chars) {
            String str = String.valueOf(c);
            if (str.matches("[a-zA-Z]")) {
                if (numerical) {
                    numerical = false;
                    String numberString = builder.toString();
                    initNumericVersionNumericalPart(numberString);
                    builder.delete(0, builder.length());
                }
                initNumericVersionAlphanumericalChar(c);
            }
            else {
                numerical = true;
                builder.append(c);
            }
        }

        /* Falls am Ende noch eine ungesicherte Zahlenfolge da ist, diese speichern: */
        if (numerical) {
            String numberString = builder.toString();
            initNumericVersionNumericalPart(numberString);
        }
    }

    /** Getter für die Version als String. */
    public String getVersion() {
        return version;
    }

    /** Getter für den Test für die numerische Version. */
    protected List<Integer> getNumericVersion() {
        return numericVersion;
    }

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

    /** Getter für die Version und das Datum als zusammengesetzter String. */
    public String getVersionAndDate() {
        return version + " vom " + date;
    }

    /** Stringrepräsentation. */
    @Override
    public String toString() {
        return "Version " + version + " vom " + date;
    }

    /**
     * Vergleich mit einem zweiten Versions-Objekt.                               <br><br>
     *
     * Wichtig ist neben einer Implementierung von compareTo() auch die passende
     * Realisierung in equals(). Sie ist erst dann konsistent, wenn
     * e1.compareTo(e2) == 0 das gleiche Ergebnis wie e1.equals(e2) liefert,
     * wobei e1 und e2 den gleichen Typ besitzen. Ein Verstoß gegen diese Regel
     * kann bei sortierten Mengen schnell Probleme bereiten; ein Beispiel nennt
     * die API-Dokumentation. Auch sollte die hashCode()-Methode korrekt
     * realisiert sein, denn sind Objekte gleich, müssen auch die Hashcodes
     * gleich sein. Und die Gleichheit bestimmen eben equals()/compareTo().       <br><br>
     *
     * e.compareTo(null) sollte eine NullPointerException auslösen, auch wenn
     * e.equals(null) die Rückgabe false liefert.
     *
     * @param that
     *            Das mit diesem Objekt zu vergleichende andere Versions-Objekt.
     * @return <0 für this < that,                                                <br>
     *         =0 für this = that und                                             <br>
     *         >0 für this > that.                                                <br>
     *
     * @see equals
     * @see hashCode
     */
    @Override
    public int compareTo(Version that) {
        if (null == that) {
            throw new NullPointerException(
                    "Vergleich einer Version mit null ist unzulässig!");
        }

        /*
         * Zunächst testen wir, ob die String-Versionen gleich sind. In dem Fall
         * ist nichts weiter zu prüfen:
         */
        if (this.getVersion().equals(that.getVersion())) {
            return 0;
        }

        /*
         * Nun untersuchen wir die beiden Versionsnummerfolgen, bis zum Ende
         * des kürzeren:
         */
        int thisSize = this.numericVersion.size();
        int thatSize = that.numericVersion.size();
        int minSize = Math.min(thisSize, thatSize);
        for (int i = 0; i < minSize; ++i) {
            int thisNumber = this.numericVersion.get(i);
            int thatNumber = that.numericVersion.get(i);
            if (thisNumber != thatNumber) {
                return thisNumber - thatNumber;
            }
        }

        /*
         * Kommt man hier an, sind beide bis zur minSize gleich, also hier
         * schauen, wer länger ist. Wer länger ist, wird als größer angesehen,
         * da 0.17.1.a größer als 0.17.1 angesehen wird:
         */

        if (thisSize == thatSize) {
            return 0;  // beide gleich lang und vorne gleich
        }
        if (thisSize == minSize) {
            return -1; // that ist länger und vorne gleich
        }
        else {
            return 1;  // this ist länger und vorne gleich
        }
    }

    /** Stellt fest, ob diese Version neuer ist, als die übergebene Version. */
    public boolean isNewerThan(Version that) {
        return compareTo(that) > 0;
    }

    /** Ermittelt die Jahreszahl aus der Version. */
    public String getYear() {
        String date = getDate();
        List<String> parts = Text.splitByWhitespace(date);
        if (parts.size() != 2) {
            throw new RuntimeException("Das Datum der Version lässt sich nicht an Leerzeichen "
                    + "teilen.\n\tdate = " + date + "\n\tversion = " + version);
        }

        return parts.get(1);
    }

    /**
     * Erzeugt den Hashcode zu einem Versionsobjekt.
     *
     * @see equals
     * @see compareTo
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((numericVersion == null) ? 0 : numericVersion.hashCode());
        return result;
    }

    /**
     * Prüft auf die Gleichheit mit einem anderen Versionenobjekt. Dabei wird nur die numerische
     * Zahlenfolge der Version ausgewertet, nicht das Datum und nicht die Version als String.
     *
     * @see hashCode
     * @see compareTo
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        Version other = (Version) obj;
        if (numericVersion == null) {
            if (other.numericVersion != null) {
                return false;
            }
        }
        else if (!numericVersion.equals(other.numericVersion)) {
            return false;
        }
        return true;
    }

    /** Erzeugt eine serialisierbare Ausgabe der Version. */
    public String toSerializedString() {
        return version + SERIALIZATION_SEPARATOR + date;
    }

    /** Erzeugt aus einem serialisierten String wieder einem Version-Objekt. */
    public static Version createFromSerializedString(String serialized) {
        List<String> parts = Text.splitBy(serialized, SERIALIZATION_SEPARATOR);
        if (parts.size() != 2) {
            throw new IllegalArgumentException("Der serialisierte String '" + serialized
                    + "' lässt sich nicht in zwei Teile zerlgen.");
        }
        else {
            return new Version(parts.get(0), parts.get(1));
        }
    }

    /**
     * Ermittelt die Version aus dem Namen einer Jar-Datei, die von Jarify erstellt worden ist.
     *
     * @param jarFilename
     *            Der Dateiname der Jar-Datei mit Pfad.
     * @param jarNameStart
     *            Der Anfang des eigentlichen Dateinamens (also das Paket mit Unterstrich dahinter,
     *            z.B. "de.duehl.vocabulary.japanese_")
     * @return Die extrahierte Nummer.
     */
    public static String extractVersionFromJarFilename(String jarFilename, String jarNameStart) {
        String bareFilename = FileHelper.getBareName(jarFilename);
        String pureName = FileHelper.removeExtension(bareFilename);
        if (!pureName.startsWith(jarNameStart)) {
            throw new RuntimeException("Der extrahierte pure Name der Jar-Datei beginnt nicht mit "
                    + "dem übergebenen Anfang des Namesns der Jar-Datei!\n"
                    + "\t" + "jarFilename  = " + jarFilename + "\n"
                    + "\t" + "jarNameStart = " + jarNameStart + "\n"
                    + "\t" + "bareFilename = " + bareFilename + "\n"
                    + "\t" + "pureName     = " + pureName + "\n"
                    );
        }
        return pureName.substring(jarNameStart.length());
    }

}
