using UnityEngine;
using UnityEngine.Events;
using System;
using System.Collections;
using System.Net;
using System.Net.Sockets;
using System.Threading;

// NTP同期時刻を返却するクラス
public class NtpRequest {
    
    private DateTime ntpDateTime;
    private Thread thread;
    private Socket socket;
    private volatile bool threadRunning = false;
    private int timeout = 3000; // ms

    private int retryCount = 0;

    private string[] NTP_SERVER = {
         "time.google.com",
         "cn.pool.ntp.org",
    };

    void OnApplicationQuit() {
        CloseRequest ();
    }

    public Coroutine GetNetworkTime(MonoBehaviour behaviour, Action<DateTime> succeededCallback, Action failedCallback, int timeout = 3000)
    {
        if (threadRunning) return null;

        // リクエスト実行
        retryCount = 0;
        this.timeout = timeout;
        ntpDateTime = DateTime.MinValue;
        _threadStart();
        return behaviour.StartCoroutine(WaitForRequest(succeededCallback, failedCallback));
    }
    private void _threadStart()
    {
        threadRunning = true;
        thread = new Thread(new ThreadStart(NetworkRequest));
        thread.Start(); 
    }
        
    private IEnumerator WaitForRequest(Action<DateTime> succeededCallback, Action failedCallback)
    {
        int startTickCount = Environment.TickCount;
        bool isTimeouted = false;
        while ( threadRunning ) {            
            if (ntpDateTime > DateTime.MinValue){
                break;
            }
            // タイムアウト
            else if (Environment.TickCount - startTickCount > timeout) {
                isTimeouted = true;
                break;
            }
            yield return 0;
        }

        CloseRequest ();
        threadRunning = false;

        if(!isTimeouted){
            if(succeededCallback != null)
                succeededCallback (ntpDateTime);
        // リトライ
        }else if(retryCount < NTP_SERVER.Length-1){
            retryCount++;
            isTimeouted = false;
            _threadStart();
            yield return WaitForRequest(succeededCallback, failedCallback);
        }else{
            failedCallback();
        }
    }

    private void NetworkRequest()
    {
        try{
            string ntpServer = NTP_SERVER[retryCount];

            var ntpData = new byte[48];
            //Setting the Leap Indicator, Version Number and Mode values
            ntpData[0] = 0x1B; //LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)

            var addresses = Dns.GetHostEntry(ntpServer).AddressList;
            var ipEndPoint = new IPEndPoint(addresses[0], 123);

            socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

            // ソケット通信開始
            socket.Connect(ipEndPoint);
            socket.Send(ntpData);
            socket.Receive(ntpData);
            socket.Close();

            ulong intPart = (ulong)ntpData[40] << 24 | (ulong)ntpData[41] << 16 | (ulong)ntpData[42] << 8 | (ulong)ntpData[43];
            ulong fractPart = (ulong)ntpData[44] << 24 | (ulong)ntpData[45] << 16 | (ulong)ntpData[46] << 8 | (ulong)ntpData[47];

            var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L);
            var networkDateTime = (new DateTime(1900, 1, 1)).AddMilliseconds((long)milliseconds);

            ntpDateTime = networkDateTime;

        }catch(SocketException){
            // TODO 例外フィルタでSocketException: Access deniedのみ取りたいが、C#のバージョンが足りない
            // https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/try-catch
        }catch(ThreadAbortException){
        }
    }

    private void CloseRequest(){
        if ( thread != null ) {
            thread.Abort();
        }
        if ( socket != null ) {
            socket.Close();
        }
    }
}



