/*
Technitium DNS Server
Copyright (C) 2022  Shreyas Zare (shreyas@technitium.com)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/

using DnsServerCore.Dhcp.Options;
using System;
using System.Globalization;
using System.IO;
using TechnitiumLibrary.IO;

namespace DnsServerCore.Dhcp
{
    public enum DhcpOptionCode : byte
    {
        Pad = 0,
        SubnetMask = 1,
        TimeOffset = 2,
        Router = 3,
        TimeServer = 4,
        NameServer = 5,
        DomainNameServer = 6,
        LogServer = 7,
        CookieServer = 8,
        LprServer = 9,
        ImpressServer = 10,
        ResourceLocationServer = 11,
        HostName = 12,
        BootFileSize = 13,
        MeritDump = 14,
        DomainName = 15,
        SwapServer = 16,
        RootPath = 17,
        ExtensionPath = 18,
        IpForwarding = 19,
        NonLocalSourceRouting = 20,
        PolicyFilter = 21,
        MaximumDatagramReassemblySize = 22,
        DefaultIpTtl = 23,
        PathMtuAgingTimeout = 24,
        PathMtuPlateauTable = 25,
        InterfaceMtu = 26,
        AllSubnetAreLocal = 27,
        BroadcastAddress = 28,
        PerformMaskDiscovery = 29,
        MaskSupplier = 30,
        PerformRouterDiscovery = 31,
        RouterSolicitationAddress = 32,
        StaticRoute = 33,
        TrailerEncapsulation = 34,
        ArpCacheTimeout = 35,
        EthernetEncapsulation = 36,
        TcpDefaultTtl = 37,
        TcpKeepAliveInterval = 38,
        TcpKeepAliveGarbage = 39,
        NetworkInformationServiceDomain = 40,
        NetworkInformationServers = 41,
        NetworkTimeProtocolServers = 42,
        VendorSpecificInformation = 43,
        NetBiosOverTcpIpNameServer = 44,
        NetBiosOverTcpIpDatagramDistributionServer = 45,
        NetBiosOverTcpIpNodeType = 46,
        NetBiosOverTcpIpScope = 47,
        XWindowSystemFontServer = 48,
        XWindowSystemDisplayManager = 49,
        RequestedIpAddress = 50,
        IpAddressLeaseTime = 51,
        OptionOverload = 52,
        DhcpMessageType = 53,
        ServerIdentifier = 54,
        ParameterRequestList = 55,
        Message = 56,
        MaximumDhcpMessageSize = 57,
        RenewalTimeValue = 58,
        RebindingTimeValue = 59,
        VendorClassIdentifier = 60,
        ClientIdentifier = 61,
        NetworkInformationServicePlusDomain = 64,
        NetworkInformationServicePlusServers = 65,
        TftpServerName = 66,
        BootfileName = 67,
        MobileIpHomeAgent = 68,
        SmtpServer = 69,
        Pop3Server = 70,
        NntpServer = 71,
        DefaultWwwServer = 72,
        DefaultFingerServer = 73,
        DefaultIrc = 74,
        StreetTalkServer = 75,
        StreetTalkDirectoryAssistance = 76,
        ClientFullyQualifiedDomainName = 81,
        DomainSearch = 119,
        ClasslessStaticRoute = 121,
        CAPWAPAccessControllerAddresses = 138,
        TftpServerAddress = 150,
        End = 255
    }

    public class DhcpOption
    {
        #region variables

        readonly DhcpOptionCode _code;
        byte[] _value;

        #endregion

        #region constructor

        public DhcpOption(DhcpOptionCode code, string hexValue)
        {
            if (hexValue is null)
                throw new ArgumentNullException(nameof(hexValue));

            _code = code;

            if (hexValue.Contains(':'))
                _value = ParseColonHexString(hexValue);
            else
                _value = Convert.FromHexString(hexValue);
        }

        public DhcpOption(DhcpOptionCode code, byte[] value)
        {
            if (value is null)
                throw new ArgumentNullException(nameof(value));

            _code = code;
            _value = value;
        }

        protected DhcpOption(DhcpOptionCode code, Stream s)
        {
            _code = code;

            int len = s.ReadByte();
            if (len < 0)
                throw new EndOfStreamException();

            _value = s.ReadBytes(len);
        }

        protected DhcpOption(DhcpOptionCode code)
        {
            _code = code;
        }

        #endregion

        #region static

        public static DhcpOption CreateEndOption()
        {
            return new DhcpOption(DhcpOptionCode.End);
        }

        public static DhcpOption Parse(Stream s)
        {
            int code = s.ReadByte();
            if (code < 0)
                throw new EndOfStreamException();

            DhcpOptionCode optionCode = (DhcpOptionCode)code;

            switch (optionCode)
            {
                case DhcpOptionCode.SubnetMask:
                    return new SubnetMaskOption(s);

                case DhcpOptionCode.Router:
                    return new RouterOption(s);

                case DhcpOptionCode.DomainNameServer:
                    return new DomainNameServerOption(s);

                case DhcpOptionCode.HostName:
                    return new HostNameOption(s);

                case DhcpOptionCode.DomainName:
                    return new DomainNameOption(s);

                case DhcpOptionCode.BroadcastAddress:
                    return new BroadcastAddressOption(s);

                case DhcpOptionCode.VendorSpecificInformation:
                    return new VendorSpecificInformationOption(s);

                case DhcpOptionCode.NetBiosOverTcpIpNameServer:
                    return new NetBiosNameServerOption(s);

                case DhcpOptionCode.RequestedIpAddress:
                    return new RequestedIpAddressOption(s);

                case DhcpOptionCode.IpAddressLeaseTime:
                    return new IpAddressLeaseTimeOption(s);

                case DhcpOptionCode.OptionOverload:
                    return new OptionOverloadOption(s);

                case DhcpOptionCode.DhcpMessageType:
                    return new DhcpMessageTypeOption(s);

                case DhcpOptionCode.ServerIdentifier:
                    return new ServerIdentifierOption(s);

                case DhcpOptionCode.ParameterRequestList:
                    return new ParameterRequestListOption(s);

                case DhcpOptionCode.MaximumDhcpMessageSize:
                    return new MaximumDhcpMessageSizeOption(s);

                case DhcpOptionCode.RenewalTimeValue:
                    return new RenewalTimeValueOption(s);

                case DhcpOptionCode.RebindingTimeValue:
                    return new RebindingTimeValueOption(s);

                case DhcpOptionCode.VendorClassIdentifier:
                    return new VendorClassIdentifierOption(s);

                case DhcpOptionCode.ClientIdentifier:
                    return new ClientIdentifierOption(s);

                case DhcpOptionCode.ClientFullyQualifiedDomainName:
                    return new ClientFullyQualifiedDomainNameOption(s);

                case DhcpOptionCode.DomainSearch:
                    return new DomainSearchOption(s);

                case DhcpOptionCode.ClasslessStaticRoute:
                    return new ClasslessStaticRouteOption(s);

                case DhcpOptionCode.CAPWAPAccessControllerAddresses:
                    return new CAPWAPAccessControllerOption(s);

                case DhcpOptionCode.TftpServerAddress:
                    return new TftpServerAddressOption(s);

                case DhcpOptionCode.Pad:
                case DhcpOptionCode.End:
                    return new DhcpOption(optionCode);

                default:
                    //unknown option
                    return new DhcpOption(optionCode, s);
            }
        }

        protected static byte[] ParseColonHexString(string value)
        {
            int i;
            int j = -1;
            string strHex;
            int b;

            using (MemoryStream mS = new MemoryStream())
            {
                while (true)
                {
                    i = value.IndexOf(':', j + 1);
                    if (i < 0)
                        i = value.Length;

                    strHex = value.Substring(j + 1, i - j - 1);

                    if (!int.TryParse(strHex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out b) || (b < byte.MinValue) || (b > byte.MaxValue))
                        throw new InvalidDataException("VendorSpecificInformation option data must be a colon (:) separated hex string.");

                    mS.WriteByte((byte)b);

                    if (i == value.Length)
                        break;

                    j = i;
                }

                return mS.ToArray();
            }
        }

        #endregion

        #region internal

        internal void AppendOptionValue(DhcpOption option)
        {
            byte[] value = new byte[_value.Length + option._value.Length];

            Buffer.BlockCopy(_value, 0, value, 0, _value.Length);
            Buffer.BlockCopy(option._value, 0, value, _value.Length, option._value.Length);

            _value = value;
        }

        internal void ParseOptionValue()
        {
            if (_value != null)
            {
                using (MemoryStream mS = new MemoryStream(_value))
                {
                    ParseOptionValue(mS);
                }
            }
        }

        #endregion

        #region protected

        protected virtual void ParseOptionValue(Stream s)
        { }

        protected virtual void WriteOptionValue(Stream s)
        {
            if (_value == null)
                throw new NotImplementedException();

            s.Write(_value);
        }

        #endregion

        #region public

        public void WriteTo(Stream s)
        {
            switch (_code)
            {
                case DhcpOptionCode.Pad:
                case DhcpOptionCode.End:
                    s.WriteByte((byte)_code);
                    break;

                default:
                    using (MemoryStream mS = new MemoryStream())
                    {
                        WriteOptionValue(mS);

                        int len = 255;
                        int valueLen = Convert.ToInt32(mS.Position);
                        mS.Position = 0;

                        do
                        {
                            if (valueLen < len)
                                len = valueLen;

                            //write option
                            s.WriteByte((byte)_code); //code
                            s.WriteByte((byte)len); //len
                            mS.CopyTo(s, len, len); //value

                            valueLen -= len;
                        }
                        while (valueLen > 0);
                    }

                    break;
            }
        }

        #endregion

        #region properties

        public DhcpOptionCode Code
        { get { return _code; } }

        public byte[] RawValue
        { get { return _value; } }

        #endregion
    }
}
