﻿using UnityEngine;
using System.Net.Sockets;
using System.Threading;
using System.Text;//Encoding
using System.IO;//StreamWriter,StreamReader
using System;
using System.Threading.Tasks;
using System.Buffers.Binary;

public class TcpCommunication 
{
    public delegate void TcpClientStartButtonDlegate();// TcpAppServerのOnGUIクライアント起動ボタンで実行するメソッド
    private static void dummyTcpClientStartButton() { Debug.Log($"dummy message Connect to {target_IP}:{tcp_port}"); }
    public static TcpClientStartButtonDlegate tcpClientButton = dummyTcpClientStartButton;

    public delegate void OnConnectDlegate();// サーバに接続できた時に実行するメソッド
    private static void dummyOnConnect() { Debug.Log($"Connected {target_IP}:{tcp_port}!"); }
    public OnConnectDlegate onConnect = dummyOnConnect;

    // 接続時の変数
    public static string target_IP = "";//接続先IP
    public static int tcp_port = 3000;    //接続先プログラムのポート番号
    public static string clientId = "ShipA";// クライアント識別ID

    public string netRemoteString = ""; // 送信先の先頭文字列(IPアドレス:ポート番号)
    public string netLocalString = ""; // 自身の先頭文字列(IPアドレス:ポート番号)

    public QueueBuffer queueBuffer = new QueueBuffer(1024);//メッセージ受信で使うキュー

    Thread mythread;//using System.Threading;が必要　（クライアントの受信スレッド）
    bool isConnectWaiting = false;// TCP接続の待機中だけTrue

    public static SynchronizationContext context; // 別スレッドからUnitのGUIやGameObjectを操作する時にこれを介す。
    Thread myRecthread;////受信処理スレッド
    public bool isRecRunning = false;// 受信スレッドが動いている間true

    public TcpClient tcpClient; //接続ソケット情報
    public Stream stream;// バイト列の送受信用

    public static Encoding encoding = new UTF8Encoding(false);

    public object messageObject = null;//このクラスを利用するアプリケーション側で、自由に利用して構わない変数

    public static string onGUI_message = "";// 状態通知（エラーなど）
    public static float onGUI_startY = 400;// OnGUI部品を並べ始める縦の基準座標
    public static float onGUI_height = 300;// OnGUI部品高さのパラメタ
    public static float onGUI_startX = 10;// OnGUI部品を並べ始める横の基準座標
    public static float onGUI_width = 700;// OnGUI部品幅のパラメタ
    public static Color onGUI_color = Color.black;// OnGUI部品幅のパラメタ
    public static int onGUI_LabelFontSize = 26;// OnGUI部品幅のパラメタ

    public TcpCommunication()// 接続用と、サーバ側の両方で使う
    {
        if (TcpCommunication.context == null && SynchronizationContext.Current != null)
        {   // 未設定で、有効な状態の場合だけ設定
            TcpCommunication.context = SynchronizationContext.Current;
        }
        if (TcpCommunication.context == null) Debug.Log("TcpCommunication.contextが未設定");
    }

    // Tcpサーバ側で使うコンストラクタ
    public TcpCommunication(TcpClient tcpClient) : base()
    {
        this.tcpClient = tcpClient;
        this.netRemoteString = tcpClient.Client.RemoteEndPoint.ToString();// GetHeaderString(tcpClient);
        this.netLocalString = tcpClient.Client.LocalEndPoint.ToString();
        this.stream = tcpClient.GetStream();
        Debug.Log("TcpCommunication サーバ用コンストラクタ実行");
        this.myRecthread = new Thread(new ThreadStart(MyRecThread));//受信処理スレッド
        this.myRecthread.Start();
    }

    public async Task ConnectTo()// staticの(string target_IP, int tcp_port)で接続するクライアント側用
    {
        if (isRecRunning)// 受信スレット動作中
        {
            return;
        }
        if (isConnectWaiting)// 接続を待つ
        {
            return;// 接続待機中であれば、何もしない。
        }

        ResetTcpClient();
        isConnectWaiting = true;// 接続中
        try
        {
            if (tcp_port > 65535) throw new Exception("ポート番号が大きい過ぎでエラー");

            // 接続処理
            await Task.Run(() =>
            {
                this.tcpClient = new TcpClient(target_IP, tcp_port); //サーバに接続する。
            });
            this.netRemoteString = tcpClient.Client.RemoteEndPoint.ToString();//GetHeaderString(this.tcpClient);
            this.netLocalString = tcpClient.Client.LocalEndPoint.ToString();
            context.Post((state) =>// GUI操作
            {
                onConnect();// 接続した時のdelegateメソッド
            }, null);
            mythread = new Thread(new ThreadStart(MyRecThread));//受信スレッドの生成
            mythread.Start();//受信スレッドのスタート
        }
        catch (Exception err)
        {
            onGUI_message += err.Message + "\r\n";

        }
        isConnectWaiting = false;// 接続終了
    }

    public void ResetTcpClient()
    {
        if (stream != null)
        {
            stream.Dispose();
            stream = null;
        }
        isRecRunning = false;
        if (this.tcpClient != null)
        {
            this.tcpClient.Dispose();
            this.tcpClient = null;
        }
    }

    // サーバー、クライン等検討の受信スレッド（受信したBinPacketをQueueBufferに記憶）
    private void MyRecThread()
    {
        isRecRunning = true;
        this.stream = tcpClient.GetStream();
        string partner = $"{tcpClient.Client.RemoteEndPoint}";
        StreamReader sr = new StreamReader(this.stream, encoding);
        Debug.Log("MyRecThread.実行開始");

        while (isRecRunning)
        {
            try
            {
                if (QueueBuffer.Is_Full(this.queueBuffer))
                {
                    System.Threading.Thread.Sleep(1);//1m秒待ち
                    onGUI_message = $"QueueBuffer　Full!";
                    continue;
                }

                BinPacket packet = BinPacket.getBinPacket(this.stream);

                QueueBuffer.SetBuff(queueBuffer, packet);
                TcpCommunication.onGUI_message = $"MyRecThread {packet.packet.Length}byte 受信";
                Debug.Log("MyRecThread {packet.packet.Length}byte（先頭{BinPacket.topSize}byteはサイズ） 受信");
            }
            catch (Exception err)
            {
                isRecRunning = false;
                onGUI_message = $"受信スレッド MyRecThreadを、不具合により終了！{err}";
                break;
            }
        }
        isRecRunning = false;
    }

    public void SendMessage(string msg)// 接続相手への送信メソッド
    {
        if (this.stream == null) return;
        byte[] packet = BinPacket.makePacketBinary(msg);// 送信データ取得
        this.stream.Write(packet, 0, packet.Length);//バイナリ送信
        TcpCommunication.onGUI_message = $"SendMessage: {packet.Length}byteを \"{msg}\"の文字列を送信";
        Debug.Log($"SendMessage: {packet.Length}byte（先頭{BinPacket.topSize}byteはサイズ）で \"{msg}\"を文字列を送信");
    }
    public void SendMessage(byte [] msg)// 接続相手への送信メソッド
    {
        if (this.stream == null) return;
        byte[] packet = BinPacket.makePacketBinary(msg);// 送信データ取得
        this.stream.Write(packet, 0, packet.Length);//バイナリ送信
        TcpCommunication.onGUI_message = $"SendMessage: {packet.Length}byteをbyte配列を送信";
        Debug.Log($"SendMessage: {packet.Length}byte（先頭{BinPacket.topSize}byteはサイズ）をbyte配列を送信");
    }
    public static void OnGUI_Sub()// TcpCommunication.onGUI_messageの文字列を表示する。（OnGUIで呼び出して利用する）
    {
        if (TcpAppServer.onGUI_flag == false) return;
        GUIStyle lableStyle = GUI.skin.GetStyle("label");   // ---Labelスタイル設定
        lableStyle.fontSize = onGUI_LabelFontSize;
        GUI.color = onGUI_color;//文字色設定
        GUI.Label(new Rect(onGUI_startX, onGUI_startY, onGUI_width, onGUI_height), $"{ TcpCommunication.onGUI_message }");
    }
}// TcpCommunication

public class BinPacket // TcpCommunicationの送受信で使うパケットの出し入れ用staticメソッド群などを定義
{
    public static int topSize = 4;// 先頭に、2であればUInt16、4であればUInt32 の送信サイスとする。
    internal byte[] packet;//QueueBufferに管理させる(先頭にサイズを埋め込んだuInt16の2byteあり)

    private BinPacket() { }
    public BinPacket(byte[] packet) { this.packet = packet; }

    // 送信用のバイト列取得用
    internal static byte[] makePacketBinary(byte[] data)// buffer先頭にサイズデータを埋め込んだbyte列を取得
    {
        // 先頭２または４byteにビックエンディアンビットサイズ(最大byte:65535または2147483647)があるバイト列
        byte[] packet = new byte[data.Length + topSize];
        if (topSize == 2 && packet.Length < 65535) BinaryPrimitives.WriteUInt16BigEndian(packet, (ushort)data.Length);
        else if (topSize == 4 && packet.Length < 2147483647) BinaryPrimitives.WriteUInt32BigEndian(packet, (uint)data.Length);
        else throw new Exception("makePacketBinary: bytes size exceeded.");
        Buffer.BlockCopy(data, 0, packet, topSize, data.Length);
        return packet;
    }
    // 送信時のバイト列取得用
    public static byte[] makePacketBinary(String data)// buffer先頭にサイズデータを埋め込んだbyte列を取得
    {
        byte[] sbuff = System.Text.Encoding.UTF8.GetBytes(data);
        // 先頭２または４byteにビックエンディアンビットサイズ(最大byte:65535または2147483648)があるバイト列
        byte[] packet = new byte[sbuff.Length + topSize];
        if (topSize == 2 && packet.Length < 65535) BinaryPrimitives.WriteUInt16BigEndian(packet, (ushort)sbuff.Length);
        else if (topSize == 4 && packet.Length < 2147483647) BinaryPrimitives.WriteUInt32BigEndian(packet, (uint)sbuff.Length);
        else throw new Exception("makePacketBinary: string size exceeded.");
        Buffer.BlockCopy(sbuff, 0, packet, topSize, sbuff.Length);
        return packet;
    }

    // 受信用　BinPacketインスタンスのpacketを引数で指定し、内容のbyte列を取り出す。
    public static byte[] getData(byte[] packet)
    {   // TcpCommunicationオブジェクトのSendMessage(byte [] msgg)で送ったパケットからの取り出し
        UInt32 count = 0;
        if (topSize == 2) count = BinaryPrimitives.ReadUInt16BigEndian(packet);//後ろに並ぶbyte数を取得
        else if (topSize == 4) count = BinaryPrimitives.ReadUInt32BigEndian(packet);//後ろに並ぶbyte数を取得
        byte[] data = new byte[count];
        Buffer.BlockCopy(packet, topSize, data, 0, (int)count);// 2147483648以下
        return data;
    }


    // 受信用　BinPacketインスタンスのpacketを引数で指定し、内容のstringを取り出す。
    internal static string getString(byte[] packet)
    {   // TcpCommunicationオブジェクトのSendMessage(string msg)で送ったパケットからの取り出し
        UInt32 count = 0;
        if (topSize == 2) count = BinaryPrimitives.ReadUInt16BigEndian(packet);//後ろに並ぶbyte数を取得
        else if (topSize == 4) count = BinaryPrimitives.ReadUInt32BigEndian(packet);//後ろに並ぶbyte数を取得
        byte[] sbuff = new byte[count];
        Buffer.BlockCopy(packet, topSize, sbuff, 0, (int)count);// 2147483648以下
        try
        {
            string s = System.Text.Encoding.UTF8.GetString(sbuff, 0, (int)count);// stringのobjectを復元
            return s;
        }
        catch (Exception e) {
            return $"{count}byteのBinPacket {e.ToString()}";
        }
    }

    public override string ToString()
    {
        return getString( this.packet );
    }

    // 受信streamからBinPacketを生成して返す
    internal static BinPacket getBinPacket(Stream stream)
    {
        // 受信バイトを取得
        byte[] aCount = new byte[topSize];
        int n = aCount.Length;
        int idx = 0;
        while(idx < n)
        {
            idx += stream.Read(aCount, idx, n - idx);
        }
        UInt32 count = 0;
        if (topSize == 2) count = BinaryPrimitives.ReadUInt16BigEndian(aCount);//後ろに並ぶbyte数を取得
        else if (topSize == 4) count = BinaryPrimitives.ReadUInt32BigEndian(aCount);//後ろに並ぶbyte数を取得
        byte[] packet = new byte[topSize + count];
        Buffer.BlockCopy(aCount, 0, packet, 0, topSize);
        n = packet.Length;
        while (idx < n)
        {
            idx += stream.Read(packet, idx, n - idx);
        }
        BinPacket binPacket = new BinPacket();
        binPacket.packet = packet;
        return binPacket;
    }

    public static void Test()// テストコード
    {
        BinPacket binPacket = new BinPacket();
        binPacket.packet = BinPacket.makePacketBinary("abcあいう");// パケットに入れる。
        Debug.Log(BinPacket.getString(binPacket.packet));// 上記が表示されればOK

        byte[] data = { 0, 1, 2, 3 };
        binPacket.packet = BinPacket.makePacketBinary(data);// パケットに入れる。
        byte [] data2 = BinPacket.getData(binPacket.packet);
        for(int i=0; i < data2.Length; i++)
        {
            Debug.Log(data2[i] == data[i]);// すべてtrueならOK
        }
    }
}// end BinPacket class


// QueueBufferを生成した後、これを引数するstaticメソッド群で操作するためのクラス
public class QueueBuffer
{
    private object lockObj = new object();
    bool mutex = false; // trueにした関数だけが、ringBufferを出し入れできる。
    BinPacket [] ringBuffer = null;
    int in_idx = -1;
    int outIdx = -1;
    public int number = 0;//上記ringBufferバッファに記憶される個数


    public QueueBuffer(int size, bool flagStringData = false)
    {
        ringBuffer = new BinPacket[size];
    }

    public static bool Is_Full(QueueBuffer buff)
    {
        return buff.number == buff.ringBuffer.Length;
    }

    // packetをqueueBuffer追加する。（上記Is_Full()がfalseの場合だけ実行する使い方をしなければならない）
    public static void SetBuff(QueueBuffer queueBuffer, BinPacket packet)// キューにpacketを入れる
    {
        while (true)
        {
            lock (queueBuffer.lockObj)// ロック
            {
                if (queueBuffer.mutex == false)
                {
                    queueBuffer.mutex = true;
                    break;
                }
            }
            System.Threading.Thread.Sleep(1);//1m秒待ち
        }
        queueBuffer.number++;
        if (queueBuffer.in_idx + 1 >= queueBuffer.ringBuffer.Length)
        {
            queueBuffer.in_idx = -1;
        }
        queueBuffer.ringBuffer[++queueBuffer.in_idx] = packet;
        queueBuffer.mutex = false;
    }

    // queueBufferが空（buff.numberが0）であれば、nullを返す。空でなければバッファから抜き出して返す。
    public static BinPacket GetBuff(QueueBuffer queueBuffer)// キューからの取り出し
    {
        if (queueBuffer.number == 0) return null;
        lock (queueBuffer.lockObj)// ロック
        {
            if (queueBuffer.mutex) return null;
            queueBuffer.mutex = true;
        }
        queueBuffer.number--;
        if (queueBuffer.outIdx + 1 >= queueBuffer.ringBuffer.Length)
        {
            queueBuffer.outIdx = -1;
        }
        BinPacket binPacket = queueBuffer.ringBuffer[++queueBuffer.outIdx];
        queueBuffer.mutex = false;
        return binPacket;
    }
}

