using Semver;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

namespace Katteker.Types
{
    /// <summary>
    /// Entry of a Release.
    /// </summary>
    public readonly struct ReleaseEntry : IComparable<ReleaseEntry>, IComparable
    {
        private const string FilenamePattern = @"(^.*)-((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?).*-(full|delta)";
        private const char Separator = '|';
        private static readonly Regex FilenameRegex = new(FilenamePattern, RegexOptions.Compiled);

        /// <summary>
        /// Creates an instance of <see cref="ReleaseEntry"/>.
        /// </summary>
        public ReleaseEntry(string filename, SemVersion version, long fileSize, bool isDelta, string sha1)
        {
            Filename = filename;
            Version = version;
            FileSize = fileSize;
            IsDelta = isDelta;
            Sha1 = sha1;
            ApplicationName = string.Empty;
        }

        /// <summary>
        /// Create an instance of <see cref="ReleaseEntry"/> from string.
        /// </summary>
        /// <exception cref="ArgumentOutOfRangeException">
        /// Argument 'line' has not the right format.
        /// </exception>
        /// <exception cref="ArgumentNullException">Argument 'line' is null.</exception>
        /// <exception cref="FileLoadException">Filename is not compliant.</exception>
        public ReleaseEntry(string line)
        {
            if (line is null) throw new ArgumentNullException(nameof(line));
            var elements = line.Split(Separator);
            if (elements.Length != 3)
            {
                throw new ArgumentOutOfRangeException(nameof(line), elements.Length, "The release entry has not the right format.");
            }

            Sha1 = elements[0];
            Filename = elements[1];
            FileSize = long.Parse(elements[2]);
            var fileSegments = FilenameRegex.Match(Filename);
            if (fileSegments.Groups.Count < 3) throw new FileLoadException("Filename is not compliant.");
            ApplicationName = fileSegments.Groups[1].Value;
            Version = SemVersion.Parse(fileSegments.Groups[2].Value);
            IsDelta = fileSegments.Groups[3].Value != "full";
        }

        /// <summary>
        /// Construct release entry from application name and version.
        /// </summary>
        /// <param name="applicationName"></param>
        /// <param name="version"></param>
        public ReleaseEntry(string applicationName, SemVersion version)
        {
            ApplicationName = applicationName;
            Version = version;
            Sha1 = string.Empty;
            Filename = string.Empty;
            FileSize = default;
            IsDelta = false;
        }

        /// <summary>
        /// Name of the application.
        /// </summary>
        public string ApplicationName { get; }

        /// <summary>
        /// Format the release entry as line for release file.
        /// </summary>
        public string EntryAsString => $"{Sha1}{Separator}{Filename}{Separator}{FileSize}";

        /// <summary>
        /// filename
        /// </summary>
        public string Filename { get; }

        /// <summary>
        /// Size of the file.
        /// </summary>
        public long FileSize { get; }

        /// <summary>
        /// Is true if the update is delta file. False otherwise.
        /// </summary>
        public bool IsDelta { get; }

        /// <summary>
        /// Hash-sum of the file.
        /// </summary>
        public string Sha1 { get; }

        /// <summary>
        /// Version
        /// </summary>
        public SemVersion Version { get; }

        /// <summary>
        /// The override of the un-equal operator.
        /// </summary>
        /// <param name="left">The left value.</param>
        /// <param name="right">The right value.</param>
        /// <returns>If left is not equal to right <c>true</c>, else <c>false</c>.</returns>
        public static bool operator !=(ReleaseEntry left, ReleaseEntry right) => !Equals(left, right);

        /// <summary>
        /// The override of the less operator.
        /// </summary>
        /// <param name="left">The left value.</param>
        /// <param name="right">The right value.</param>
        /// <returns>If left is less than right <c>true</c>, else <c>false</c>.</returns>
        public static bool operator <(ReleaseEntry left, ReleaseEntry right) => left.CompareTo(right) < 0;

        /// <summary>
        /// The override of the less than or equal operator.
        /// </summary>
        /// <param name="left">The left value.</param>
        /// <param name="right">The right value.</param>
        /// <returns>If left is less than or equal to right <c>true</c>, else <c>false</c>.</returns>
        public static bool operator <=(ReleaseEntry left, ReleaseEntry right) => left == right || left < right;

        /// <summary>
        /// The override of the equals operator.
        /// </summary>
        /// <param name="left">The left value.</param>
        /// <param name="right">The right value.</param>
        /// <returns>If left is equal to right <c>true</c>, else <c>false</c>.</returns>
        public static bool operator ==(ReleaseEntry left, ReleaseEntry right) => Equals(left, right);

        /// <summary>
        /// The override of the greater operator.
        /// </summary>
        /// <param name="left">The left value.</param>
        /// <param name="right">The right value.</param>
        /// <returns>If left is greater than right <c>true</c>, else <c>false</c>.</returns>
        public static bool operator >(ReleaseEntry left, ReleaseEntry right) => left.CompareTo(right) > 0;

        /// <summary>
        /// The override of the greater than or equal operator.
        /// </summary>
        /// <param name="left">The left value.</param>
        /// <param name="right">The right value.</param>
        /// <returns>If left is greater than or equal to right <c>true</c>, else <c>false</c>.</returns>
        public static bool operator >=(ReleaseEntry left, ReleaseEntry right) => left == right || left > right;

        /// <inheritdoc/>
        public int CompareTo(object obj) => obj is ReleaseEntry entry ? CompareTo(entry) : 1;

        /// <inheritdoc/>
        public int CompareTo(ReleaseEntry other)
        {
            var versionComparison = Comparer<SemVersion>.Default.Compare(Version, other.Version);
            if (versionComparison != 0) return versionComparison;
            return string.Compare(Filename, other.Filename, StringComparison.Ordinal);
        }

        /// <inheritdoc/>
        public override bool Equals(object obj) => obj is ReleaseEntry entry && Equals(entry);

        /// <inheritdoc/>
        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = Version.GetHashCode();
                hashCode = (hashCode * 397) ^ Sha1.GetHashCode();
                hashCode = (hashCode * 397) ^ Filename.GetHashCode();
                hashCode = (hashCode * 397) ^ FileSize.GetHashCode();
                hashCode = (hashCode * 397) ^ IsDelta.GetHashCode();
                return hashCode;
            }
        }

        /// <inheritdoc/>
        public override string ToString() => $"{ApplicationName} {Version}";

        /// <summary>
        /// Returns true if the instances are equal. False otherwise.
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        private bool Equals(ReleaseEntry other)
        {
            return Equals(Version, other.Version)
                   && string.Equals(Sha1, other.Sha1)
                   && string.Equals(Filename, other.Filename)
                   && FileSize == other.FileSize
                   && IsDelta == other.IsDelta;
        }
    }
}