// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Threading;

namespace NuGet.CommandLine.Test
{
    /// <summary>
    /// This class allocates ports while ensuring that:
    /// 1. Ports that are permanently taken (or taken for the duration of the test) are not being attempted to be used.
    /// 2. Ports are not shared across different tests (but you can allocate two different ports in the same test).
    /// 
    /// Gotcha: If another application grabs a port during the test, we have a race condition.
    /// </summary>
    [DebuggerDisplay("Port: {PortNumber}, Port count for this app domain: {_appDomainOwnedPorts.Count}")]
    public class PortReserver : IDisposable
    {
        private Mutex _portMutex;

        // We use this list to hold on to all the ports used because the Mutex will be blown through on the same thread.
        // Theoretically we can do a thread local hashset, but that makes dispose thread dependant, or requires more complicated concurrency checks.
        // Since practically there is no perf issue or concern here, this keeps the code the simplest possible.
        private static HashSet<int> _appDomainOwnedPorts = new HashSet<int>();

        public int PortNumber
        {
            get;
            private set;
        }

        public PortReserver(int basePort = 50231)
        {
            if (basePort <= 0)
            {
                throw new InvalidOperationException();
            }

            // Grab a cross appdomain/cross process/cross thread lock, to ensure only one port is reserved at a time.
            using (Mutex mutex = GetGlobalMutex())
            {
                try
                {
                    int port = basePort - 1;

                    while (true)
                    {
                        port++;

                        if (port > 65535)
                        {
                            throw new InvalidOperationException("Exceeded port range");
                        }

                        // AppDomainOwnedPorts check enables reserving two ports from the same thread in sequence.
                        // ListUsedTCPPort prevents port contention with other apps.
                        if (_appDomainOwnedPorts.Contains(port) ||
                            ListUsedTCPPort().Any(endPoint => endPoint.Port == port))
                        {
                            continue;
                        }

                        string mutexName = "NuGet-Port-" + port.ToString(CultureInfo.InvariantCulture); // Create a well known mutex
                        _portMutex = new Mutex(initiallyOwned: false, name: mutexName);

                        // If no one else is using this port grab it.
                        if (_portMutex.WaitOne(millisecondsTimeout: 0))
                        {
                            break;
                        }

                        // dispose this Mutex since the port it represents is not available.
                        _portMutex.Dispose();
                        _portMutex = null;
                    }

                    PortNumber = port;
                    _appDomainOwnedPorts.Add(port);
                }
                finally
                {
                    mutex.ReleaseMutex();
                }
            }
        }

        public string BaseUri
        {
            get
            {
                return String.Format(CultureInfo.InvariantCulture, "http://localhost:{0}/", PortNumber);
            }
        }

        public void Dispose()
        {
            if (PortNumber == -1)
            {
                // Object already disposed
                return;
            }

            using (Mutex mutex = GetGlobalMutex())
            {
                _portMutex.Dispose();
                _appDomainOwnedPorts.Remove(PortNumber);
                PortNumber = -1;
            }
        }

        private static Mutex GetGlobalMutex()
        {
            Mutex mutex = new Mutex(initiallyOwned: false, name: "NuGet-RandomPortAcquisition");
            if (!mutex.WaitOne(30000))
            {
                throw new InvalidOperationException();
            }

            return mutex;
        }

        private static IPEndPoint[] ListUsedTCPPort()
        {
            var usedPort = new HashSet<int>();
            IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();

            return ipGlobalProperties.GetActiveTcpListeners();
        }
    }
}
