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

namespace Katteker
{
    /// <inheritdoc cref="IComparable" />
    /// <summary>
    /// Entry of a Release.
    /// </summary>
    public class ReleaseEntry : IComparable<ReleaseEntry>, IComparable
    {
        private const string FilenameRegex = @"(^.*)-((?: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 = '|';

        /// <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;
        }

        /// <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 == 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 = Regex.Match(Filename, FilenameRegex);
            if (fileSegments.Groups.Count < 3) throw new FileLoadException("Filename is not compilant.");
            ApplicationName = fileSegments.Groups[1].Value;
            Version = SemVersion.Parse(fileSegments.Groups[2].Value);
            IsDelta = fileSegments.Groups[3].Value != "full";
        }

        /// <summary>
        /// cunstruct 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;
        }

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

        /// <summary>
        /// Hashsum of the file.
        /// </summary>
        public string SHA1 { get; }

        /// <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>
        /// 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}";

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

        /// <inheritdoc />
        public int CompareTo(object obj) => CompareTo(obj as ReleaseEntry);

        /// <inheritdoc />
        public int CompareTo(ReleaseEntry other)
        {
            if (ReferenceEquals(this, other)) return 0;
            if (other is null) return 1;
            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) => Equals(obj as ReleaseEntry);

        /// <summary>
        /// Returns true if the instances are equal. False otherwise.
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        protected 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;
        }

        /// <inheritdoc />
        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = Version != null ? Version.GetHashCode() : 0;
                hashCode = (hashCode * 397) ^ (SHA1 != null ? SHA1.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ (Filename != null ? Filename.GetHashCode() : 0);
                hashCode = (hashCode * 397) ^ FileSize.GetHashCode();
                hashCode = (hashCode * 397) ^ IsDelta.GetHashCode();
                return hashCode;
            }
        }

        /// <summary>
        /// Returns true if the left and right instances are equal.
        /// </summary>
        public static bool operator ==(ReleaseEntry left, ReleaseEntry right) => Equals(left, right);

        /// <summary>
        /// Returns true if the left and right instances are not equal.
        /// </summary>
        public static bool operator !=(ReleaseEntry left, ReleaseEntry right) => !Equals(left, right);
    }
}