using Swan.Formatters;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;

namespace Swan.Net.Dns {
  /// <summary>
  /// DnsClient public methods.
  /// </summary>
  internal partial class DnsClient {
    public abstract class DnsResourceRecordBase : IDnsResourceRecord {
      private readonly IDnsResourceRecord _record;

      protected DnsResourceRecordBase(IDnsResourceRecord record) => this._record = record;

      public DnsDomain Name => this._record.Name;

      public DnsRecordType Type => this._record.Type;

      public DnsRecordClass Class => this._record.Class;

      public TimeSpan TimeToLive => this._record.TimeToLive;

      public Int32 DataLength => this._record.DataLength;

      public Byte[] Data => this._record.Data;

      public Int32 Size => this._record.Size;

      protected virtual String[] IncludedProperties => new[] { nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength) };

      public Byte[] ToArray() => this._record.ToArray();

      public override String ToString() => Json.SerializeOnly(this, true, this.IncludedProperties);
    }

    public class DnsResourceRecord : IDnsResourceRecord {
      public DnsResourceRecord(DnsDomain domain, Byte[] data, DnsRecordType type, DnsRecordClass klass = DnsRecordClass.IN, TimeSpan ttl = default) {
        this.Name = domain;
        this.Type = type;
        this.Class = klass;
        this.TimeToLive = ttl;
        this.Data = data;
      }

      public DnsDomain Name {
        get;
      }

      public DnsRecordType Type {
        get;
      }

      public DnsRecordClass Class {
        get;
      }

      public TimeSpan TimeToLive {
        get;
      }

      public Int32 DataLength => this.Data.Length;

      public Byte[] Data {
        get;
      }

      public Int32 Size => this.Name.Size + Tail.SIZE + this.Data.Length;

      public static DnsResourceRecord FromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
        DnsDomain domain = DnsDomain.FromArray(message, offset, out offset);
        Tail tail = message.ToStruct<Tail>(offset, Tail.SIZE);

        Byte[] data = new Byte[tail.DataLength];

        offset += Tail.SIZE;
        Array.Copy(message, offset, data, 0, data.Length);

        endOffset = offset + data.Length;

        return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive);
      }

      public Byte[] ToArray() => new MemoryStream(this.Size).Append(this.Name.ToArray()).Append(new Tail() { Type = Type, Class = Class, TimeToLive = TimeToLive, DataLength = this.Data.Length, }.ToBytes()).Append(this.Data).ToArray();

      public override String ToString() => Json.SerializeOnly(this, true, nameof(this.Name), nameof(this.Type), nameof(this.Class), nameof(this.TimeToLive), nameof(this.DataLength));

      [StructEndianness(Endianness.Big)]
      [StructLayout(LayoutKind.Sequential, Pack = 2)]
      private struct Tail {
        public const Int32 SIZE = 10;

        private UInt16 type;
        private UInt16 klass;
        private UInt32 ttl;
        private UInt16 dataLength;

        public DnsRecordType Type {
          get => (DnsRecordType)this.type;
          set => this.type = (UInt16)value;
        }

        public DnsRecordClass Class {
          get => (DnsRecordClass)this.klass;
          set => this.klass = (UInt16)value;
        }

        public TimeSpan TimeToLive {
          get => TimeSpan.FromSeconds(this.ttl);
          set => this.ttl = (UInt32)value.TotalSeconds;
        }

        public Int32 DataLength {
          get => this.dataLength;
          set => this.dataLength = (UInt16)value;
        }
      }
    }

    public class DnsPointerResourceRecord : DnsResourceRecordBase {
      public DnsPointerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.PointerDomainName = DnsDomain.FromArray(message, dataOffset);

      public DnsDomain PointerDomainName {
        get;
      }

      protected override String[] IncludedProperties {
        get {
          List<String> temp = new List<String>(base.IncludedProperties) { nameof(this.PointerDomainName) };
          return temp.ToArray();
        }
      }
    }

    public class DnsIPAddressResourceRecord : DnsResourceRecordBase {
      public DnsIPAddressResourceRecord(IDnsResourceRecord record) : base(record) => this.IPAddress = new IPAddress(this.Data);

      public IPAddress IPAddress {
        get;
      }

      protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.IPAddress) }.ToArray();
    }

    public class DnsNameServerResourceRecord : DnsResourceRecordBase {
      public DnsNameServerResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.NSDomainName = DnsDomain.FromArray(message, dataOffset);

      public DnsDomain NSDomainName {
        get;
      }

      protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.NSDomainName) }.ToArray();
    }

    public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase {
      public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) => this.CanonicalDomainName = DnsDomain.FromArray(message, dataOffset);

      public DnsDomain CanonicalDomainName {
        get;
      }

      protected override String[] IncludedProperties => new List<String>(base.IncludedProperties) { nameof(this.CanonicalDomainName) }.ToArray();
    }

    public class DnsMailExchangeResourceRecord : DnsResourceRecordBase {
      private const Int32 PreferenceSize = 2;

      public DnsMailExchangeResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset)
          : base(record) {
        Byte[] preference = new Byte[PreferenceSize];
        Array.Copy(message, dataOffset, preference, 0, preference.Length);

        if(BitConverter.IsLittleEndian) {
          Array.Reverse(preference);
        }

        dataOffset += PreferenceSize;

        this.Preference = BitConverter.ToUInt16(preference, 0);
        this.ExchangeDomainName = DnsDomain.FromArray(message, dataOffset);
      }

      public Int32 Preference {
        get;
      }

      public DnsDomain ExchangeDomainName {
        get;
      }

      protected override String[] IncludedProperties => new List<String>(base.IncludedProperties)
      {
                nameof(this.Preference),
                nameof(this.ExchangeDomainName),
            }.ToArray();
    }

    public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase {
      public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, Byte[] message, Int32 dataOffset) : base(record) {
        this.MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);
        this.ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset);

        Options tail = message.ToStruct<Options>(dataOffset, Options.SIZE);

        this.SerialNumber = tail.SerialNumber;
        this.RefreshInterval = tail.RefreshInterval;
        this.RetryInterval = tail.RetryInterval;
        this.ExpireInterval = tail.ExpireInterval;
        this.MinimumTimeToLive = tail.MinimumTimeToLive;
      }

      public DnsStartOfAuthorityResourceRecord(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl = default)
          : base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl)) {
        this.MasterDomainName = master;
        this.ResponsibleDomainName = responsible;

        this.SerialNumber = serial;
        this.RefreshInterval = refresh;
        this.RetryInterval = retry;
        this.ExpireInterval = expire;
        this.MinimumTimeToLive = minTtl;
      }

      public DnsDomain MasterDomainName {
        get;
      }

      public DnsDomain ResponsibleDomainName {
        get;
      }

      public Int64 SerialNumber {
        get;
      }

      public TimeSpan RefreshInterval {
        get;
      }

      public TimeSpan RetryInterval {
        get;
      }

      public TimeSpan ExpireInterval {
        get;
      }

      public TimeSpan MinimumTimeToLive {
        get;
      }

      protected override String[] IncludedProperties => new List<String>(base.IncludedProperties)
      {
                nameof(this.MasterDomainName),
                nameof(this.ResponsibleDomainName),
                nameof(this.SerialNumber),
            }.ToArray();

      private static IDnsResourceRecord Create(DnsDomain domain, DnsDomain master, DnsDomain responsible, Int64 serial, TimeSpan refresh, TimeSpan retry, TimeSpan expire, TimeSpan minTtl, TimeSpan ttl) {
        MemoryStream data = new MemoryStream(Options.SIZE + master.Size + responsible.Size);
        Options tail = new Options {
          SerialNumber = serial,
          RefreshInterval = refresh,
          RetryInterval = retry,
          ExpireInterval = expire,
          MinimumTimeToLive = minTtl,
        };

        _ = data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes());

        return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl);
      }

      [StructEndianness(Endianness.Big)]
      [StructLayout(LayoutKind.Sequential, Pack = 4)]
      public struct Options {
        public const Int32 SIZE = 20;

        private UInt32 serialNumber;
        private UInt32 refreshInterval;
        private UInt32 retryInterval;
        private UInt32 expireInterval;
        private UInt32 ttl;

        public Int64 SerialNumber {
          get => this.serialNumber;
          set => this.serialNumber = (UInt32)value;
        }

        public TimeSpan RefreshInterval {
          get => TimeSpan.FromSeconds(this.refreshInterval);
          set => this.refreshInterval = (UInt32)value.TotalSeconds;
        }

        public TimeSpan RetryInterval {
          get => TimeSpan.FromSeconds(this.retryInterval);
          set => this.retryInterval = (UInt32)value.TotalSeconds;
        }

        public TimeSpan ExpireInterval {
          get => TimeSpan.FromSeconds(this.expireInterval);
          set => this.expireInterval = (UInt32)value.TotalSeconds;
        }

        public TimeSpan MinimumTimeToLive {
          get => TimeSpan.FromSeconds(this.ttl);
          set => this.ttl = (UInt32)value.TotalSeconds;
        }
      }
    }

    private static class DnsResourceRecordFactory {
      public static IList<IDnsResourceRecord> GetAllFromArray(Byte[] message, Int32 offset, Int32 count, out Int32 endOffset) {
        List<IDnsResourceRecord> result = new List<IDnsResourceRecord>(count);

        for(Int32 i = 0; i < count; i++) {
          result.Add(GetFromArray(message, offset, out offset));
        }

        endOffset = offset;
        return result;
      }

      private static IDnsResourceRecord GetFromArray(Byte[] message, Int32 offset, out Int32 endOffset) {
        DnsResourceRecord record = DnsResourceRecord.FromArray(message, offset, out endOffset);
        Int32 dataOffset = endOffset - record.DataLength;

        return record.Type switch
        {
          DnsRecordType.A => (new DnsIPAddressResourceRecord(record)),
          DnsRecordType.AAAA => new DnsIPAddressResourceRecord(record),
          DnsRecordType.NS => new DnsNameServerResourceRecord(record, message, dataOffset),
          DnsRecordType.CNAME => new DnsCanonicalNameResourceRecord(record, message, dataOffset),
          DnsRecordType.SOA => new DnsStartOfAuthorityResourceRecord(record, message, dataOffset),
          DnsRecordType.PTR => new DnsPointerResourceRecord(record, message, dataOffset),
          DnsRecordType.MX => new DnsMailExchangeResourceRecord(record, message, dataOffset),
          _ => record
        };
      }
    }
  }
}