///////////////////////////////////////////////////////////////////////////////
//
//  Microsoft Research Netstack / Singularity
//
//  Copyright (c) Microsoft Corporation.  All rights reserved.
//
//  File:   DhcpClientFSM.cs
//
//  Description:   DHCP Client Finite State machine.
//
//  Note: This file does not yet implement the RENEWING or
//        REBINDING states.

using Microsoft.Singularity;
using System;
using System.Diagnostics;
using System.Collections;

using System.Net.IP;
using Drivers.Net;

namespace NetStack.Runtime
{
    using Protocols;

    class DhcpClientState
    {
        protected DhcpClient client;
        protected string     stateName;

        internal DhcpClientState(DhcpClient client, string stateName)
        {
            this.client    = client;
            this.stateName = stateName;
        }

        internal string Name
        {
            get { return stateName; }
        }

        /// <summary>
        /// State should perform processing as DhcpClient recognizes it
        /// as the currently active event.
        /// </summary>
        internal virtual void EnterEvent()
        {
            Core.Log("Entering State {0}\n", stateName);
        }

        /// <summary>
        /// State should process that has arrived.
        /// </summary>
        internal virtual void ReceiveEvent(DhcpFormat! df)
        {
            DhcpClient.DebugPrint("FSM Ignored DHCP packet: {0}\n", stateName);
        }

        /// <summary>
        /// Renewal timer expiry event.
        /// </summary>
        internal virtual void RenewalTimeoutEvent()
        {
            DhcpClient.DebugPrint("FSM Renewal Timeout: {0}\n", stateName);
        }

        /// <summary>
        /// Rebind timer expiry event.
        /// </summary>
        internal virtual void RebindTimeoutEvent()
        {
            DhcpClient.DebugPrint("FSM Rebind Timeout: {0}\n", stateName);
        }

        /// <summary>
        /// General purpose timer expiry event.
        /// </summary>
        internal virtual void StateTimeoutEvent()
        {
            DhcpClient.DebugPrint("FSM State Specific Timeout: {0}\n",
                                  stateName);
        }
    };

    internal class DhcpClientStateInitialize : DhcpClientState
    {
        internal DhcpClientStateInitialize(DhcpClient client)
            : base(client, "Initialize")
        {
        }

        internal override void EnterEvent()
        {
            client.StartNewTransaction();

            DhcpFormat dhcp =
                new DhcpFormat(DhcpFormat.MessageType.Discover);

            dhcp.TransactionID = client.TransactionID;
            dhcp.SetHardwareAddress(client.MacAddress);

#if ADVERTISE_CLIENT_ID
            // Add Client Identifier for self
            //
            // This is disabled because the Windows
            // DHCP server allocates us a different address with the
            // client id present if we networked booted.  Thus having the
            // identifier breaks static DHCP entries which we use
            // for test machines.
            EthernetAddress macAddress = client.MacAddress;
            dhcp.AddOption(DhcpClientID.Create(macAddress.GetAddressBytes()));
#endif

            // Add parameters we'd like to know about
            dhcp.AddOption(DhcpParameterRequest.Create(
                               DhcpClient.StandardRequestParameters
                               )
                           );
            // dhcp.AddOption(DhcpAutoConfigure.Create(0));
            client.Send(EthernetAddress.Broadcast, dhcp);
            client.ChangeState(new DhcpClientStateSelecting(client));
        }
    }

    internal class DhcpClientStateSelecting : DhcpClientState
    {
        private readonly TimeSpan StateTimeout = TimeSpan.FromSeconds(5);

        internal DhcpClientStateSelecting(DhcpClient client)
            : base(client, "Selecting")
        {
        }

        internal override void EnterEvent()
        {
            client.SetStateTimeout(DateTime.Now + StateTimeout);
        }

        internal override void StateTimeoutEvent()
        {
            // Received no valid offers
            client.ChangeState(new DhcpClientStateInitialize(client));
        }

        internal override void ReceiveEvent(DhcpFormat! dhcp)
        {
            // Check if message is in response to our request
            if (dhcp.BootMessageType != DhcpFormat.BootType.Reply   ||
                dhcp.TransactionID != client.TransactionID          ||
                dhcp.GetHardwareAddress() != client.MacAddress)
            {
                return;
            }

            IPv4 serverAddress = dhcp.NextServerIPAddress;

            // Check if offered address is valid (ie not zero
            // and below class E)
            IPv4 offeredAddress = dhcp.YourIPAddress;
            if (offeredAddress == IPv4.Any || offeredAddress.IsMulticast()) {
                return;
            }

            // Check if message is an offer
            SortedList offeredOptions = dhcp.GetOptions();
            DhcpByteOption messageType
                = offeredOptions[DhcpMessageType.OptionCode] as DhcpByteOption;
            if (messageType == null ||
                messageType.Value != (byte)DhcpFormat.MessageType.Offer)
            {
                return;
            }

            // Must have parameters
            byte [] parameters = new byte [] {
                DhcpSubnetMask.OptionCode,
                DhcpRouter.OptionCode,
                DhcpDomainNameServer.OptionCode
            };
            foreach (byte p in parameters) {
                IDhcpOption ido = offeredOptions[p] as IDhcpOption;
                if (ido == null) {
                    return;
                }
            }

            client.CancelStateTimeout();
            client.ChangeState(new DhcpClientStateRequesting(client,
                                                             serverAddress,
                                                             offeredAddress,
                                                             offeredOptions));
        }
    }

    internal class DhcpClientStateRequesting : DhcpClientState
    {
        IPv4       serverAddress;
        IPv4       offeredAddress;
        SortedList offeredOptions;

        private readonly TimeSpan StateTimeout = TimeSpan.FromSeconds(5);

        internal DhcpClientStateRequesting(DhcpClient client,
                                           IPv4       serverAddress,
                                           IPv4       offeredAddress,
                                           SortedList offeredOptions)
            : base(client, "Requesting")
        {
            this.serverAddress  = serverAddress;
            this.offeredAddress = offeredAddress;
            this.offeredOptions = offeredOptions;
        }

        internal override void EnterEvent()
        {
            client.SetStateTimeout(DateTime.Now + StateTimeout);

            DhcpFormat dhcp =
                new DhcpFormat(DhcpFormat.MessageType.Request);

            dhcp.TransactionID      = client.TransactionID;
            dhcp.TransactionSeconds = client.TransactionSeconds;
            dhcp.SetHardwareAddress(client.MacAddress);
#if ADVERTISE_CLIENT_ID
            dhcp.AddOption(
                DhcpClientID.Create(client.MacAddress.GetAddressBytes())
                );
#endif
            dhcp.AddOption(
                DhcpRequestedIPAddress.Create(offeredAddress)
                );

            // Add parameters we'd like to know about
            dhcp.AddOption(DhcpParameterRequest.Create(
                               DhcpClient.StandardRequestParameters
                               )
                           );

            client.Send(EthernetAddress.Broadcast, dhcp);
        }

        private static void TakeOption(SortedList! offeredOptions,
                                       byte        optionCode,
                                       DhcpFormat! dhcpFormat)
        {
            IDhcpOption option = offeredOptions[optionCode] as IDhcpOption;
            if (option != null) {
                dhcpFormat.AddOption(option);
            }
        }

        internal override void StateTimeoutEvent()
        {
            client.ChangeState(new DhcpClientStateInitialize(client));
        }

        internal override void ReceiveEvent(DhcpFormat! dhcp)
        {
            // Check if message is in response to our request
            if (dhcp.BootMessageType != DhcpFormat.BootType.Reply   ||
                dhcp.TransactionID != client.TransactionID          ||
                dhcp.GetHardwareAddress() != client.MacAddress)
            {
                return;
            }

            IPv4 serverAddress = dhcp.NextServerIPAddress;

            // Check if offered address is valid (ie not zero
            // and below class E)
            IPv4 offeredAddress = dhcp.YourIPAddress;
            if (offeredAddress == IPv4.Any || offeredAddress.IsMulticast()) {
                return;
            }

            // Check if message is an ack
            SortedList offeredOptions = dhcp.GetOptions();
            DhcpByteOption messageType
                = offeredOptions[DhcpMessageType.OptionCode] as DhcpByteOption;
            if (messageType == null) {
                return;
            }

            switch (messageType.Value) {
                case (byte) DhcpFormat.MessageType.Ack:
                    break;
                case (byte) DhcpFormat.MessageType.Nak:
                    client.ChangeState(new DhcpClientStateInitialize(client));
                    return;
                default:
                    return;
            }

            // Must have parameters
            byte [] parameters = new byte [] {
                DhcpSubnetMask.OptionCode,
                DhcpRouter.OptionCode,
                DhcpDomainNameServer.OptionCode
            };

            foreach (byte p in parameters) {
                IDhcpOption ido = offeredOptions[p] as IDhcpOption;
                if (ido == null) {
                    return;
                }
            }

            client.CancelStateTimeout();
            client.ChangeState(new DhcpClientStateBound(client,
                                                        serverAddress,
                                                        offeredAddress,
                                                        offeredOptions));
        }
    }

    internal class DhcpClientStateBound : DhcpClientState
    {
        private IPv4       serverAddress;
        private IPv4       hostAddress;
        private SortedList hostOptions;

        internal DhcpClientStateBound(DhcpClient client,
                                      IPv4       serverAddress,
                                      IPv4       hostAddress,
                                      SortedList hostOptions)
            : base(client, "Bound")
        {
            this.serverAddress = serverAddress;
            this.hostAddress   = hostAddress;
            this.hostOptions   = hostOptions;
        }

        internal override void EnterEvent()
        {
            // Yay! We got DHCP configuration information
            DateTime now = DateTime.Now;

            // Set up renewal timer
            DhcpDWordOption renewalOption =
                hostOptions[DhcpRenewalTime.OptionCode] as DhcpDWordOption;

            uint renewalSeconds = 3600;
            if (renewalOption != null) {
                renewalSeconds = renewalOption.Value;
            }
            client.SetRenewalTimeout(now + TimeSpan.FromSeconds(renewalSeconds));

            // Set up rebinding timer
            DhcpDWordOption rebindOption =
                hostOptions[DhcpRebindingTime.OptionCode] as DhcpDWordOption;
            uint rebindSeconds = renewalSeconds + 60;
            if (rebindOption != null) {
                rebindSeconds = Math.Max(rebindOption.Value,
                                         renewalSeconds + 1);
            }
            client.SetRebindTimeout(now + TimeSpan.FromSeconds(rebindSeconds));

            // Add host address as a dhcp option and then get client
            // to install settings.
            hostOptions[DhcpRequestedIPAddress.OptionCode] =
                DhcpRequestedIPAddress.Create(hostAddress);

            if (client.InstallDhcpOptions(hostOptions) == false) {
                client.ChangeState(new DhcpClientStateInitialize(client));
            }
            DebugStub.WriteLine("\nDHCP client acquired lease of {0} for {1} seconds.",
                __arglist(hostAddress.ToString(), rebindSeconds));
        }

        internal override void ReceiveEvent(DhcpFormat! dhcp)
        {
            // ignore
        }

        internal override void RenewalTimeoutEvent()
        {
#if NOTYET
            client.ChangeState(new DhcpClientStateRenewing(client,
                                                           hostOptions));
#endif
        }
    }

    internal class DhcpClientStateRenewing : DhcpClientState
    {
        SortedList hostOptions;

        internal DhcpClientStateRenewing(DhcpClient client,
                                         SortedList hostOptions)
            : base(client, "Renewing")
        {
            this.hostOptions = hostOptions;
        }

        internal override void EnterEvent()
        {
        }

        internal override void ReceiveEvent(DhcpFormat! dhcp)
        {
            // ignore
        }

        internal override void RebindTimeoutEvent()
        {
            // NB We use Debug.Assert here rather than Debug.Fail
            // as Singularity doesn't support the latter.
            Debug.Assert(false, "Rebind timeout in renewal state");
        }
    }

    internal class DhcpClientStateRebinding : DhcpClientState
    {
        internal DhcpClientStateRebinding(DhcpClient client)
            : base(client, "Rebinding")
        {
        }

        internal override void EnterEvent()
        {

        }

        internal override void ReceiveEvent(DhcpFormat! dhcp)
        {
        }

        internal override void RebindTimeoutEvent()
        {
        }
    }
}
