namespace StardewModdingAPI.Toolkit.Framework
{
    /// <summary>Reads strings into a semantic version.</summary>
    internal static class SemanticVersionReader
    {
        /*********
        ** Public methods
        *********/
        /// <summary>Parse a semantic version string.</summary>
        /// <param name="versionStr">The version string to parse.</param>
        /// <param name="allowNonStandard">Whether to recognize non-standard semver extensions.</param>
        /// <param name="major">The major version incremented for major API changes.</param>
        /// <param name="minor">The minor version incremented for backwards-compatible changes.</param>
        /// <param name="patch">The patch version for backwards-compatible fixes.</param>
        /// <param name="platformRelease">The platform-specific version (if applicable).</param>
        /// <param name="prereleaseTag">An optional prerelease tag.</param>
        /// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param>
        /// <returns>Returns whether the version was successfully parsed.</returns>
        public static bool TryParse(string versionStr, bool allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata)
        {
            // init
            major = 0;
            minor = 0;
            patch = 0;
            platformRelease = 0;
            prereleaseTag = null;
            buildMetadata = null;

            // normalize
            versionStr = versionStr?.Trim();
            if (string.IsNullOrWhiteSpace(versionStr))
                return false;
            char[] raw = versionStr.ToCharArray();

            // read major/minor version
            int i = 0;
            if (!TryParseVersionPart(raw, ref i, out major) || !TryParseLiteral(raw, ref i, '.') || !TryParseVersionPart(raw, ref i, out minor))
                return false;

            // read optional patch version
            if (TryParseLiteral(raw, ref i, '.') && !TryParseVersionPart(raw, ref i, out patch))
                return false;

            // read optional non-standard platform release version
            if (allowNonStandard && TryParseLiteral(raw, ref i, '.') && !TryParseVersionPart(raw, ref i, out platformRelease))
                return false;

            // read optional prerelease tag
            if (TryParseLiteral(raw, ref i, '-') && !TryParseTag(raw, ref i, out prereleaseTag))
                return false;

            // read optional build tag
            if (TryParseLiteral(raw, ref i, '+') && !TryParseTag(raw, ref i, out buildMetadata))
                return false;

            // validate
            return i == versionStr.Length; // valid if we're at the end
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Try to parse the next characters in a queue as a numeric part.</summary>
        /// <param name="raw">The raw characters to parse.</param>
        /// <param name="index">The index of the next character to read.</param>
        /// <param name="part">The parsed part.</param>
        private static bool TryParseVersionPart(char[] raw, ref int index, out int part)
        {
            part = 0;

            // take digits
            string str = "";
            for (int i = index; i < raw.Length && char.IsDigit(raw[i]); i++)
                str += raw[i];

            // validate
            if (str.Length == 0)
                return false;
            if (str.Length > 1 && str[0] == '0')
                return false; // can't have leading zeros

            // parse
            part = int.Parse(str);
            index += str.Length;
            return true;
        }

        /// <summary>Try to parse a literal character.</summary>
        /// <param name="raw">The raw characters to parse.</param>
        /// <param name="index">The index of the next character to read.</param>
        /// <param name="ch">The expected character.</param>
        private static bool TryParseLiteral(char[] raw, ref int index, char ch)
        {
            if (index >= raw.Length || raw[index] != ch)
                return false;

            index++;
            return true;
        }

        /// <summary>Try to parse a tag.</summary>
        /// <param name="raw">The raw characters to parse.</param>
        /// <param name="index">The index of the next character to read.</param>
        /// <param name="tag">The parsed tag.</param>
        private static bool TryParseTag(char[] raw, ref int index, out string tag)
        {
            // read tag length
            int length = 0;
            for (int i = index; i < raw.Length && (char.IsLetterOrDigit(raw[i]) || raw[i] == '-' || raw[i] == '.'); i++)
                length++;

            // validate
            if (length == 0)
            {
                tag = null;
                return false;
            }

            // parse
            tag = new string(raw, index, length);
            index += length;
            return true;
        }
    }
}