using System;
using System.Xml.Serialization;
using Newtonsoft.Json;
using SabreTools.Data.Extensions;
using SabreTools.Hashing;
using SabreTools.IO.Extensions;
using SabreTools.Metadata.Filter;
using SabreTools.Text.Extensions;

// TODO: Add item mappings for all fields
// TODO: Investigate whether this is the "File" from NIDB
namespace SabreTools.Metadata.DatItems.Formats
{
    /// <summary>
    /// Represents a single file item
    /// </summary>
    [JsonObject("file"), XmlRoot("file")]
    public sealed class File : DatItem
    {
        #region Private instance variables

        private byte[]? _crc; // 8 bytes
        private byte[]? _md5; // 16 bytes
        private byte[]? _sha1; // 20 bytes
        private byte[]? _sha256; // 32 bytes

        #endregion

        #region Properties

        /// <inheritdoc>/>
        public override Data.Models.Metadata.ItemType ItemType => Data.Models.Metadata.ItemType.File;

        /// <summary>
        /// ID value
        /// </summary>
        [JsonProperty("id", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("id")]
        public string? Id { get; set; }

        /// <summary>
        /// Extension value
        /// </summary>
        [JsonProperty("extension", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("extension")]
        public string? Extension { get; set; }

        /// <summary>
        /// Byte size of the rom
        /// </summary>
        [JsonProperty("size", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("size")]
        public long? Size { get; set; } = null;

        /// <summary>
        /// File CRC32 hash
        /// </summary>
        [JsonProperty("crc", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("crc")]
        public string? CRC
        {
            get { return _crc.ToHexString(); }
            set { _crc = value == "null" ? HashType.CRC32.ZeroBytes : TextHelper.NormalizeCRC32(value).FromHexString(); }
        }

        /// <summary>
        /// File MD5 hash
        /// </summary>
        [JsonProperty("md5", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("md5")]
        public string? MD5
        {
            get { return _md5.ToHexString(); }
            set { _md5 = TextHelper.NormalizeMD5(value).FromHexString(); }
        }

        /// <summary>
        /// File SHA-1 hash
        /// </summary>
        [JsonProperty("sha1", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("sha1")]
        public string? SHA1
        {
            get { return _sha1.ToHexString(); }
            set { _sha1 = TextHelper.NormalizeSHA1(value).FromHexString(); }
        }

        /// <summary>
        /// File SHA-256 hash
        /// </summary>
        [JsonProperty("sha256", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("sha256")]
        public string? SHA256
        {
            get { return _sha256.ToHexString(); }
            set { _sha256 = TextHelper.NormalizeSHA256(value).FromHexString(); }
        }

        /// <summary>
        /// Format value
        /// </summary>
        [JsonProperty("format", DefaultValueHandling = DefaultValueHandling.Ignore), XmlElement("format")]
        public string? Format { get; set; }

        #endregion

        #region Constructors

        /// <summary>
        /// Create a default, empty File object
        /// </summary>
        public File() { }

        #endregion

        #region Accessors

        /// <inheritdoc/>
        public override string? GetName() => null;

        /// <inheritdoc/>
        public override void SetName(string? name) { }

        #endregion

        #region Cloning Methods

        /// <inheritdoc/>
        public override object Clone()
        {
            var file = new File()
            {
                Id = this.Id,
                Extension = this.Extension,
                Size = this.Size,
                _crc = this._crc,
                _md5 = this._md5,
                _sha1 = this._sha1,
                _sha256 = this._sha256,
                Format = this.Format,
            };
            file.DupeType = DupeType;
            file.Machine = Machine!.Clone() as Machine ?? new Machine();
            file.RemoveFlag = RemoveFlag;
            file.Source = Source?.Clone() as Source;

            return file;
        }

        /// <summary>
        /// Convert a disk to the closest Rom approximation
        /// </summary>
        /// <returns></returns>
        public Rom ConvertToRom()
        {
            var rom = new Rom();

            rom.Name = $"{Id}.{Extension}";
            rom.Size = Size;
            rom.CRC32 = CRC;
            rom.MD5 = MD5;
            rom.SHA1 = SHA1;
            rom.SHA256 = SHA256;

            rom.DupeType = DupeType;
            rom.Machine = Machine?.Clone() as Machine;
            rom.MachineIndex = MachineIndex;
            rom.RemoveFlag = RemoveFlag;
            rom.Source = Source?.Clone() as Source;
            rom.SourceIndex = SourceIndex;

            return rom;
        }

        #endregion

        #region Comparision Methods

        /// <inheritdoc/>
        public override bool Equals(DatItem? other)
        {
            bool dupefound = false;

            // If we don't have a file, return false
            if (ItemType != other?.ItemType)
                return dupefound;

            // Otherwise, treat it as a File
            File? newOther = other as File;

            // If all hashes are empty, then they're dupes
            if (!HasHashes() && !newOther!.HasHashes())
            {
                dupefound = true;
            }

            // If we have a file that has no known size, rely on the hashes only
            else if (Size is null && HashMatch(newOther!))
            {
                dupefound = true;
            }

            // Otherwise if we get a partial match
            else if (Size == newOther!.Size && HashMatch(newOther))
            {
                dupefound = true;
            }

            return dupefound;
        }

        /// <summary>
        /// Fill any missing size and hash information from another Rom
        /// </summary>
        /// <param name="other">File to fill information from</param>
        public void FillMissingInformation(File other)
        {
            if (Size is null && other.Size is not null)
                Size = other.Size;

            if (_crc.IsNullOrEmpty() && !other._crc.IsNullOrEmpty())
                _crc = other._crc;

            if (_md5.IsNullOrEmpty() && !other._md5.IsNullOrEmpty())
                _md5 = other._md5;

            if (_sha1.IsNullOrEmpty() && !other._sha1.IsNullOrEmpty())
                _sha1 = other._sha1;

            if (_sha256.IsNullOrEmpty() && !other._sha256.IsNullOrEmpty())
                _sha256 = other._sha256;
        }

        /// <summary>
        /// Returns if the File contains any hashes
        /// </summary>
        /// <returns>True if any hash exists, false otherwise</returns>
        public bool HasHashes()
        {
            return !_crc.IsNullOrEmpty()
                || !_md5.IsNullOrEmpty()
                || !_sha1.IsNullOrEmpty()
                || !_sha256.IsNullOrEmpty();
        }

        /// <summary>
        /// Returns if all of the hashes are set to their 0-byte values
        /// </summary>
        /// <returns>True if any hash matches the 0-byte value, false otherwise</returns>
        public bool HasZeroHash()
        {
            bool crcNull = string.IsNullOrEmpty(CRC) || string.Equals(CRC, HashType.CRC32.ZeroString, StringComparison.OrdinalIgnoreCase);
            bool md5Null = string.IsNullOrEmpty(MD5) || string.Equals(MD5, HashType.MD5.ZeroString, StringComparison.OrdinalIgnoreCase);
            bool sha1Null = string.IsNullOrEmpty(SHA1) || string.Equals(SHA1, HashType.SHA1.ZeroString, StringComparison.OrdinalIgnoreCase);
            bool sha256Null = string.IsNullOrEmpty(SHA256) || string.Equals(SHA256, HashType.SHA256.ZeroString, StringComparison.OrdinalIgnoreCase);

            return crcNull && md5Null && sha1Null && sha256Null;
        }

        /// <summary>
        /// Returns if there are no, non-empty hashes in common with another File
        /// </summary>
        /// <param name="other">File to compare against</param>
        /// <returns>True if at least one hash is not mutually exclusive, false otherwise</returns>
        private bool HasCommonHash(File other)
        {
            return !(_crc.IsNullOrEmpty() ^ other._crc.IsNullOrEmpty())
                || !(_md5.IsNullOrEmpty() ^ other._md5.IsNullOrEmpty())
                || !(_sha1.IsNullOrEmpty() ^ other._sha1.IsNullOrEmpty())
                || !(_sha256.IsNullOrEmpty() ^ other._sha256.IsNullOrEmpty());
        }

        /// <summary>
        /// Returns if any hashes are common with another File
        /// </summary>
        /// <param name="other">File to compare against</param>
        /// <returns>True if any hashes are in common, false otherwise</returns>
        private bool HashMatch(File other)
        {
            // If either have no hashes, we return false, otherwise this would be a false positive
            if (!HasHashes() || !other.HasHashes())
                return false;

            // If neither have hashes in common, we return false, otherwise this would be a false positive
            if (!HasCommonHash(other))
                return false;

            // Return if all hashes match according to merge rules
            return MetadataExtensions.ConditionalHashEquals(_crc, other._crc)
                && MetadataExtensions.ConditionalHashEquals(_md5, other._md5)
                && MetadataExtensions.ConditionalHashEquals(_sha1, other._sha1)
                && MetadataExtensions.ConditionalHashEquals(_sha256, other._sha256);
        }

        #endregion

        #region Sorting and Merging

        /// <inheritdoc/>
        public override string GetKey(ItemKey bucketedBy, Machine? machine, Source? source, bool lower = true, bool norename = true)
        {
            // Set the output key as the default blank string
            string? key;

#pragma warning disable IDE0010
            // Now determine what the key should be based on the bucketedBy value
            switch (bucketedBy)
            {
                case ItemKey.CRC32:
                    key = CRC;
                    break;

                case ItemKey.MD5:
                    key = MD5;
                    break;

                case ItemKey.SHA1:
                    key = SHA1;
                    break;

                case ItemKey.SHA256:
                    key = SHA256;
                    break;

                // Let the base handle generic stuff
                default:
                    return base.GetKey(bucketedBy, machine, source, lower, norename);
            }
#pragma warning restore IDE0010

            // Double and triple check the key for corner cases
            key ??= string.Empty;
            if (lower)
                key = key.ToLowerInvariant();

            return key;
        }

        #endregion

        #region Manipulation

        /// <inheritdoc/>
        /// <remarks>Not implemented</remarks>
        public override bool PassesFilter(FilterRunner filterRunner) => false;

        /// <inheritdoc/>
        /// <remarks>Not implemented</remarks>
        public override bool PassesFilterDB(FilterRunner filterRunner) => false;

        #endregion
    }
}
