package org.jabref.logic.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import kong.unirest.core.json.JSONArray;
import kong.unirest.core.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents the Application Version with the major and minor number, the full Version String and if it's a developer version
 */
public class Version {

    public static final String JABREF_DOWNLOAD_URL = "https://downloads.jabref.org";

    private static final Version UNKNOWN_VERSION = new Version();

    private final static Pattern VERSION_PATTERN = Pattern.compile("(?<major>\\d+)(\\.(?<minor>\\d+))?(\\.(?<patch>\\d+))?(?<stage>-alpha|-beta)?(?<num>\\d+)?(?<dev>-?dev)?.*");
    private final static Pattern CI_SUFFIX_PATTERN = Pattern.compile("-ci\\.\\d+");

    private static final String JABREF_GITHUB_RELEASES = "https://api.github.com/repos/JabRef/JabRef/releases";

    private String fullVersion = BuildInfo.UNKNOWN_VERSION;
    private int major = -1;
    private int minor = -1;
    private int patch = -1;
    private DevelopmentStage developmentStage = DevelopmentStage.UNKNOWN;
    private int developmentNum = -1;
    private boolean isDevelopmentVersion;

    /**
     * Dummy constructor to create a local object (and  {@link Version#UNKNOWN_VERSION})
     */
    private Version() {
    }

    /**
     * Tinylog does not allow for altering existing loging configuraitons after the logger was initialized .
     * Lazy initialization to enable tinylog writing to a file (and also still enabling loggin in this class)
     */
    private static Logger getLogger() {
        return LoggerFactory.getLogger(Version.class);
    }

    /**
     * @param version must be in form of following pattern: {@code (\d+)(\.(\d+))?(\.(\d+))?(-alpha|-beta)?(-?dev)?} (e.g., 3.3; 3.4-dev)
     * @return the parsed version or {@link Version#UNKNOWN_VERSION} if an error occurred
     */
    public static Version parse(String version) {
        if ((version == null) || "".equals(version) || version.equals(BuildInfo.UNKNOWN_VERSION)
                || "${version}".equals(version)) {
            return UNKNOWN_VERSION;
        }

        Version parsedVersion = new Version();

        // remove "-ci.1" suffix
        Matcher ciSuffixMatcher = CI_SUFFIX_PATTERN.matcher(version);
        version = ciSuffixMatcher.replaceAll("");

        parsedVersion.fullVersion = version;
        Matcher matcher = VERSION_PATTERN.matcher(version);
        if (matcher.find()) {
            try {
                parsedVersion.major = Integer.parseInt(matcher.group("major"));

                String minorString = matcher.group("minor");
                parsedVersion.minor = minorString == null ? 0 : Integer.parseInt(minorString);

                String patchString = matcher.group("patch");
                parsedVersion.patch = patchString == null ? 0 : Integer.parseInt(patchString);

                String versionStageString = matcher.group("stage");
                parsedVersion.developmentStage = versionStageString == null ? DevelopmentStage.STABLE : DevelopmentStage.parse(versionStageString);

                String stageNumString = matcher.group("num");
                parsedVersion.developmentNum = stageNumString == null ? 0 : Integer.parseInt(stageNumString);

                parsedVersion.isDevelopmentVersion = matcher.group("dev") != null;
            } catch (NumberFormatException e) {
                getLogger().warn("Invalid version string used: {}", version, e);
                return UNKNOWN_VERSION;
            } catch (IllegalArgumentException e) {
                getLogger().warn("Invalid version pattern is used", e);
                return UNKNOWN_VERSION;
            }
        } else {
            getLogger().warn("Version could not be recognized by the pattern");
            return UNKNOWN_VERSION;
        }
        return parsedVersion;
    }

    /**
     * Grabs all the available releases from the GitHub repository
     */
    public static List<Version> getAllAvailableVersions() throws IOException {
        HttpURLConnection connection = (HttpURLConnection) new URL(JABREF_GITHUB_RELEASES).openConnection();
        connection.setRequestProperty("Accept-Charset", "UTF-8");
        try (BufferedReader rd = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
            JSONArray objects = new JSONArray(rd.readLine());
            List<Version> versions = new ArrayList<>(objects.length());
            for (int i = 0; i < objects.length(); i++) {
                JSONObject jsonObject = objects.getJSONObject(i);
                Version version = Version.parse(jsonObject.getString("tag_name").replaceFirst("v", ""));
                versions.add(version);
            }
            connection.disconnect();
            return versions;
        }
    }

    /**
     * @return true if this version is newer than the passed one
     */
    public boolean isNewerThan(Version otherVersion) {
        Objects.requireNonNull(otherVersion);
        if (Objects.equals(this, otherVersion)) {
            return false;
        } else if (this.getFullVersion().equals(BuildInfo.UNKNOWN_VERSION)) {
            return false;
        } else if (otherVersion.getFullVersion().equals(BuildInfo.UNKNOWN_VERSION)) {
            return false;
        }

        // compare the majors
        if (this.getMajor() > otherVersion.getMajor()) {
            return true;
        } else if (this.getMajor() == otherVersion.getMajor()) {
            // if the majors are equal compare the minors
            if (this.getMinor() > otherVersion.getMinor()) {
                return true;
            } else if (this.getMinor() == otherVersion.getMinor()) {
                // if the minors are equal compare the patch numbers
                if (this.getPatch() > otherVersion.getPatch()) {
                    return true;
                } else if (this.getPatch() == otherVersion.getPatch()) {
                    // if the patch numbers are equal compare the development stages
                    if (this.developmentStage.isMoreStableThan(otherVersion.developmentStage)) {
                        return true;
                    } else if (this.developmentStage == otherVersion.developmentStage) {
                        // if the development stage are equal compare the development number
                        if (this.getDevelopmentNum() > otherVersion.getDevelopmentNum()) {
                            return true;
                        } else if (this.getDevelopmentNum() == otherVersion.getDevelopmentNum()) {
                            // if the stage is equal check if this version is in development and the other is not
                            return !this.isDevelopmentVersion && otherVersion.isDevelopmentVersion;
                        }
                    }
                }
            }
        }
        return false;
    }

    /**
     * Checks if this version should be updated to one of the given ones. Ignoring the other Version if this one is Stable and the other one is not.
     *
     * @return The version this one should be updated to, or an empty Optional
     */
    public Optional<Version> shouldBeUpdatedTo(List<Version> availableVersions) {
        Optional<Version> newerVersion = Optional.empty();
        for (Version version : availableVersions) {
            if (this.shouldBeUpdatedTo(version)
                    && (!newerVersion.isPresent() || version.isNewerThan(newerVersion.get()))) {
                newerVersion = Optional.of(version);
            }
        }
        return newerVersion;
    }

    /**
     * Checks if this version should be updated to the given one. Ignoring the other Version if this one is Stable and the other one is not.
     *
     * @return True if this version should be updated to the given one
     */
    public boolean shouldBeUpdatedTo(Version otherVersion) {
        // ignoring the other version if it is not stable, except if this version itself is not stable
        if (developmentStage == Version.DevelopmentStage.STABLE
                && otherVersion.developmentStage != Version.DevelopmentStage.STABLE) {
            return false;
        }

        // check if the other version is newer than given one
        return otherVersion.isNewerThan(this);
    }

    public String getFullVersion() {
        return fullVersion;
    }

    public int getMajor() {
        return major;
    }

    public int getMinor() {
        return minor;
    }

    public int getPatch() {
        return patch;
    }

    public int getDevelopmentNum() {
        return developmentNum;
    }

    public boolean isDevelopmentVersion() {
        return isDevelopmentVersion;
    }

    /**
     * @return The link to the changelog on GitHub to this specific version (https://github.com/JabRef/jabref/blob/vX.X/CHANGELOG.md)
     */
    public String getChangelogUrl() {
        if (isDevelopmentVersion) {
            return "https://github.com/JabRef/jabref/blob/main/CHANGELOG.md#unreleased";
        } else {
            StringBuilder changelogLink = new StringBuilder()
                    .append("https://github.com/JabRef/jabref/blob/v")
                    .append(this.getMajor())
                    .append(".")
                    .append(this.getMinor());

            if (this.getPatch() != 0) {
                changelogLink
                        .append(".")
                        .append(this.getPatch());
            }

            changelogLink
                    .append(this.developmentStage.stage);

            if (this.getDevelopmentNum() != 0) {
                changelogLink
                        .append(this.getDevelopmentNum());
            }

            changelogLink.append("/CHANGELOG.md");

            return changelogLink.toString();
        }
    }

    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof Version)) {
            return false;
        }

        // till all the information are stripped from the fullversion this should suffice
        return this.getFullVersion().equals(((Version) other).getFullVersion());
    }

    @Override
    public int hashCode() {
        return getFullVersion().hashCode();
    }

    @Override
    public String toString() {
        return this.getFullVersion();
    }

    public enum DevelopmentStage {
        UNKNOWN("", 0),
        ALPHA("-alpha", 1),
        BETA("-beta", 2),
        STABLE("", 3);

        /**
         * describes how stable this stage is, the higher the better
         */
        private final int stability;
        private final String stage;

        DevelopmentStage(String stage, int stability) {
            this.stage = stage;
            this.stability = stability;
        }

        public static DevelopmentStage parse(String stage) {
            if (stage == null) {
                getLogger().warn("The stage cannot be null");
                return UNKNOWN;
            } else if (stage.equals(STABLE.stage)) {
                return STABLE;
            } else if (stage.equals(ALPHA.stage)) {
                return ALPHA;
            } else if (stage.equals(BETA.stage)) {
                return BETA;
            }
            getLogger().warn("Unknown development stage: {}", stage);
            return UNKNOWN;
        }

        /**
         * @return true if this stage is more stable than the {@code otherStage}
         */
        public boolean isMoreStableThan(DevelopmentStage otherStage) {
            return this.stability > otherStage.stability;
        }
    }
}
