﻿using System.Buffers.Binary;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using System.Buffers.Binary;//Unity 2021.2以降で使える　BinaryPrimitives.WriteInt32LittleEndian用

/*
int[,] array2D = {
    {1, 2, 3},
    {4, 5, 6}
};
上記で6の要素は、 2次元の添え字が 1 で、1次元目の添え字が2であるが、
int[] array1D = {1, 2, 3,4, 5, 6};の管理で、
array1D[ 1 * 1次元サイス + 2 ] により2次元操作ができ、
これ応用しして、1次元で、3次元もで配列を管理する考えが、このNumArrayクラス
 */

// Numpyの機能の限定した代替え用クラスで、 1～3次元の構造変更ができ、行列演算を行うクラス
//（利用したいNumpyの機能の絞って、似た能力のメソッドを実装している）
public class NumArray 
{
    public static System.Random random;
    static NumArray()  //C#の静的コンストラクター(Javaの静的イニシャライザ（staticブロック）相当)
    {
        random = new System.Random(123); //random.NextDouble();
    }

    // Box-Muller法:2つの独立な標準正規分布に従う乱数を生成するアルゴリズム
    public static double NormalDistribution(double mean=0, double stdDev=1)// 平均値と標準偏差を指定
    {
        double u1 = 1.0 - random.NextDouble();
        double u2 = random.NextDouble();
        double randStdNormal = System.Math.Sqrt(-2.0 * System.Math.Log(u1)) * System.Math.Sin(2.0 * System.Math.PI * u2);
        return mean + stdDev * randStdNormal;
    }

    public int []shapeR= { 1, 1, 1 };  // 下記arrayの1次元、2次元、3次元の要素数　(それぞれの次元のサイズ)
    // numpyのshapeは、次元の大きい方から１次元まで各次元のサイズがタプルで得られる情報で、その逆の並びになる
    // shapeR[2]が0の場合、次元の大きさが2で、shapeR[1]も0であれば一次元の配列として扱う。
    public float[] array;  // 多次元配列の要素は、全てこの一次配列に記憶

    // array[i3, i2, i1]のような操作対象要素の参照返す。
    public ref float FPAt(int i1, int i2 = 0, int i3 = 0)
    {
        int dim1 = shapeR[0];// 1次元サイズ
        int idx = i3 * (dim1 * shapeR[1]) + i2 * dim1 + i1;// 1次元の配列に相当する添え字を求める
        //Debug.Log($"array[{idx}]");
        return ref array[idx];
    }

    // 1次元、2次元、3次元のサイズだけを指定した初期化生成
    public static NumArray create(int dim1, int dim2 = -1, int dim3 = -1)
    {
        dim2 = dim2 < 0 ? 0 : dim2;
        dim3 = dim3 < 0 ? 0 : dim3;
        NumArray na = new NumArray();
        na.array = new float[dim1 * (dim2 == 0 ? 1 : dim2) * (dim3 == 0 ? 1 : dim3)];
        na.shapeR[0] = dim1; na.shapeR[1] = dim2; na.shapeR[2] = dim3;
        return na;
    }

    // 自身のNumArrayを複製
    public NumArray deepcopy()
    {
        NumArray naC = NumArray.create(this.shapeR[0], this.shapeR[1], this.shapeR[2]);
        for(int i=0; i < naC.array.Length; i++)
        {
            naC.array[i] = this.array[i];
        }
        return naC;
    }

    // bufの各要素を、floatに変換してarrayに設定し、引数で、次元の配列構造に設定する生成する特殊関数。
    // [0,1,2,3,4,5,6,7,8,9,10,11]のbuf 、dim1=3で、
    // dim2=0の実行すると、[[0,1,2],[3,4,5],[6,7,8],[9,10,11]]の4×3の配列イメージになり、
    // dim2=2の実行すると、[ [[0,1,2],[3,4,5]],  [[6,7,8],[9,10,11]] ]の2×2×3の配列イメージになる
    // dim1を指定しなければ1次元(dim1=0)、dim1だけ指定すると2次元、dim1とdim2を０以外で指定すると３次元になる
    public static NumArray createByBytes(byte[] buf, int dim1=0, int dim2 = 0)
    {
        dim2 = dim2 < 0 ? 0 : dim2;
        NumArray na = new NumArray();
        int n = buf.Length;
        na.array = new float[n];
        for (int i = 0; i < n; i++) na.array[i] = buf[i];
        if (dim1 == 0)
        {
            na.shapeR[0] = n; // １次元
            na.shapeR[1] = na.shapeR[2] = 0;
        }
        else // 2次元または３次元
        {
            na.shapeR[0] = dim1;
            if( dim2 == 0)
            {
                na.shapeR[1] = (int)(n / (dim1) + 0.5);
                na.shapeR[2] = 0; // 次元の個数が2
            }
            else
            {
                na.shapeR[1] = dim2;
                na.shapeR[2] = (int)(n / (dim1 * dim2) + 0.5);// 次元の個数が3
            }
        }
        return na;
    }

    // bufの各要素を、floatに変換してarrayに設定し、引数で、次元の配列構造に設定する生成する特殊関数。
    public static NumArray createByArray<T>(T[] buf, int dim1=0, int dim2 = 0)
    {
        dim2 = dim2 < 0 ? 0 : dim2;
        NumArray na = new NumArray();
        int n = buf.Length;
        na.array = new float[n];
        for (int i = 0; i < n; i++)
        {
            T t = buf[i];
            if (t is int)
                na.array[i] = (int)(object)t;
            else if (t is float)
                na.array[i] = (float)(object)t;
            else if (t is double)
                na.array[i] = (float)(double)(object)t;
            else throw new System.Exception("NumArray.createByArray:Unsupported type");
        }
        if (dim1 == 0)
        {
            na.shapeR[0] = n; // １次元
            na.shapeR[1] = na.shapeR[2] = 0;
        }
        else // 2次元または３次元
        {
            na.shapeR[0] = dim1;
            if (dim2 == 0)
            {
                na.shapeR[1] = (int)(n / (dim1) + 0.5);
                na.shapeR[2] = 0; // 次元の個数が2
            }
            else
            {
                na.shapeR[1] = dim2;
                na.shapeR[2] = (int)(n / (dim1 * dim2) + 0.5);// 次元の個数が3
            }
        }
        return na;
    }

    // paramの値で、1次元をdim1、2次元をdim2、3次元をdim3にして、NumArrayを生成
    public static NumArray createParam(float param, int dim1, int dim2=1, int dim3=0)
    {
        dim2 = dim2 < 0 ? 0 : dim2;
        dim3 = dim3 < 0 ? 0 : dim3;
        NumArray na = new NumArray();
        na.array = new float[dim1 * (dim2==0?1: dim2) * (dim3==0?1: dim3)];
        for (int i = 0; i < na.array.Length; i++) na.array[i] = param;
        na.shapeR[0] = dim1;
        na.shapeR[1] = dim2;
        na.shapeR[2] = dim3;
        return na;
    }

    // 正規分布乱数（ガウス分布乱数）で初期化したNumArrayの生成
    public static NumArray createGaussian(int dim1, int dim2 = 1, int dim3 = 1, double mean = 0, double stdDev = 1)
    {
        dim2 = dim2 < 0 ? 0 : dim2;
        dim3 = dim3 < 0 ? 0 : dim3;
        NumArray na = new NumArray();
        na.array = new float[dim1 * (dim2 == 0 ?1: dim2) * (dim3 == 0 ? 1 : dim3)];
        na.shapeR[0] = dim1; na.shapeR[1] = dim2; na.shapeR[2] = dim3;
        for(int i=0; i < na.array.Length; i++)
        {
            na.array[i] = (float)NormalDistribution(mean, stdDev);
        }
        return na;
    }

    // 自身が2次配列、または3次配列の場合に、idx行を抽出した１次または２次配列を返す。
    public NumArray createLineAt(int idx)
    {
        if (this.shapeR[1] == 0)
            throw new System.Exception("createLineAtは、１次配列で、NumArrayAtメソッドは使えません。");
        if (this.shapeR[1] <= idx)
            throw new System.Exception($"createLineAt({idx})が、shapeR[1]:{this.shapeR[0]}の添え字範囲を超えました");
        NumArray naA = new NumArray();
        { }
        naA.shapeR[0] = this.shapeR[0];
        naA.shapeR[1] = this.shapeR[2];
        naA.shapeR[2] = 0;
        int size = naA.shapeR[0] * (naA.shapeR[1] == 0 ? 1 : naA.shapeR[1]) * (naA.shapeR[2] == 0 ? 1 : naA.shapeR[2]);
        naA.array = new float[size];
        int iA = 0;
        int iS = idx * this.shapeR[0];
        while (iA < size)
        {
            int iE = iS + this.shapeR[0];
            for (int i = iS; i < iE; i++)
            {
                naA.array[iA] = this.array[i];
                iA += 1;
            }
            iS += this.shapeR[0] * this.shapeR[1];
        }
        return naA;
    }

    public void add_scalar(float v)//行列に対するスカラー加算した行列に変更
    {
        for (int i = 0; i < this.array.Length; i++) this.array[i] += v;
    }

    public void add_matrix(NumArray na)//行列の各要素を加算した要素に変更   
    {
        if(this.shapeR[0] != na.shapeR[0] || this.shapeR[0] != na.shapeR[0])
        {
            string s = $"add_matrix:{this.shapeR[0]}!={na.shapeR[0]} || {this.shapeR[0]}! {na.shapeR[0]}";
            throw new System.Exception(s);
        }
        for (int i = 0; i < this.array.Length; i++) this.array[i] += na.array[i];
    }

    public void mul_scalar(float v)//行列に対するスカラー乗算した行列に変更
    {
        for (int i = 0; i < this.array.Length; i++) this.array[i] *= v;
    }

    public void mul_matrix(NumArray na)//行列の各要素を乗算した要素に変更  
    {
        for (int i = 0; i < this.array.Length; i++) this.array[i] *= na.array[i];
    }

    // a行列のk行とb行列のn列において、各要素を積の総和を求める（行列の積用の副関数）
    public static float sum_of_products(NumArray a, int k, NumArray b, int n)
    {
        float rtnv = 0;
        for(int i=0; i < a.shapeR[0]; i++)
        {
            //Debug.Log($"a.FPAt({i}, {k}, 0) * b.FPAt({n}, {i}, 0)");
            rtnv += a.FPAt(i, k, 0) * b.FPAt(n, i, 0);
        }
        return rtnv;
    }

    // 行列の積 戻り値： a × b
    public static NumArray dot(NumArray a, NumArray b)
    {
        if( a.shapeR[0] != b.shapeR[1])
        {
            string msg = $"aの1次元サイズ]{a.shapeR[0]}と、bの2次元サイズ]{b.shapeR[1]}が一致しません。";
            throw new System.Exception(msg);
        }
        NumArray c = NumArray.createParam(0, b.shapeR[0], a.shapeR[1]);
        int row_size = c.shapeR[1] == 0 ? 1 : c.shapeR[1];
        for (int row = 0; row < row_size; row++)// 行の繰り返し
        {
            for (int column = 0; column < c.shapeR[0]; column++)// 列の繰り返し
            {
                c.FPAt(column, row, 0) = sum_of_products(a, row, b, column);
            }
        }
        return c;
    }

    // このクラスのオブジェクトをファイルに書き込むシリアライズメソッド
    // this.shapeの３要素（int型）と、この要素数のthis.array内容のデータ(float)の並びをストリームに書き込みます。
    public void write(FileStream fileStream)
    {
        // リトルエンディアンで byte 配列に変換
        byte[] littleEndianBytes = new byte[sizeof(int)];
        BinaryPrimitives.WriteInt32LittleEndian(littleEndianBytes, (int)this.shapeR[0]);// 1次元サイズ
        fileStream.Write(littleEndianBytes);
        BinaryPrimitives.WriteInt32LittleEndian(littleEndianBytes, (int)this.shapeR[1]);// 2次元サイズ
        fileStream.Write(littleEndianBytes);
        BinaryPrimitives.WriteInt32LittleEndian(littleEndianBytes, (int)this.shapeR[2]);// 3次元サイズ
        fileStream.Write(littleEndianBytes);
        for (int i = 0; i < this.array.Length; i++)
        {
            byte[] buf = System.BitConverter.GetBytes((float)this.array[i]);
            if (System.BitConverter.IsLittleEndian == false)
            {   // BinaryPrimitives.WriteSingleLittleEndianが使えなかったので代替えコード
                System.Array.Reverse(buf);// リトルエンディアンへ反転
            }
            fileStream.Write(buf);
        }
    }

    // writeメソッドで書き込んだシリアライズデータから このクラスのオブジェクトを復元する。
    public void read(FileStream fileStream)
    {
        byte[] buf = new byte[sizeof(int)];
        for(int dim=0; dim<this.shapeR.Length; dim++)
        {
            for (int i = 0; i < buf.Length;) {
                int size = fileStream.Read(buf, i, buf.Length - i);//読み込み
                if (size <= 0) break;
                i += size;
            }
            try
            {
                this.shapeR[dim] = BinaryPrimitives.ReadInt32LittleEndian(buf);
            }
            catch (System.Exception e)
            {
                throw new System.Exception($"NumArray Read fail:{e}");
            }
        }
        this.array = new float[this.shapeR[0]* (this.shapeR[1]==0?1: this.shapeR[1]) * (this.shapeR[2]==0?1: this.shapeR[2])];
        buf = new byte[sizeof(float)];
        for (int idx = 0; idx < this.array.Length; idx++)
        {
            for (int i = 0; i < buf.Length;)
            {
                int size = fileStream.Read(buf, i, buf.Length - i);//読み込み
                if (size <= 0) break;
                i += size;
            }
            if (System.BitConverter.IsLittleEndian == false)
            {   // BinaryPrimitives.WriteSingleLittleEndianが使えなかったので代替えコード
                System.Array.Reverse(buf);// リトルエンディアンへ反転
            }
            this.array[idx]=System.BitConverter.ToSingle(buf, 0);// floatを復元
        }
    }


    // idx位置のグレー手書き画像をRawImageに描画する時に使うColor32[]取得用
    public Color32[] getPixels(int width, int height, int idx)
    {
        // 画素配列を初期化
        idx *= (width * height);
        Color32 []pixels = new Color32[width * height];
        for (int y = height-1; y >= 0; y--)
        {
            for (int x = 0; x < width; x++)
            {
                byte v = (byte)(this.array[idx++] * 255);
                pixels[y * width + x] = new Color32(v, v, v, 255);// グレー表示
                pixels[y * width + x] = new Color32(v, v, v, 255);// グレー表示
            }
        }
        return pixels;
    }


    // シグモイド関数　(arrayの全要素を0～1.0に変換したNumArrayの生成)
    public NumArray sigmoid()
    {
        NumArray na = NumArray.create(this.shapeR[0], this.shapeR[1], this.shapeR[2]);
        for (int i = 0; i < this.array.Length; i++) {
            na.array[i] = 1/(1+Mathf.Exp(-this.array[i]));
        }
        return na;
    }

    // 転置行列 行列の行と列を入れ替えた行列を生成して返す。（2次元に対応する）
    // （numpyの場合、各要素は参照を維持するが、このメソッドは要素を複製する。）
    public NumArray T()
    {
        NumArray y = new NumArray();
        y.shapeR[0] = this.shapeR[1];
        y.shapeR[1] = this.shapeR[0];
        y.shapeR[2] = 0;
        int nRow = this.shapeR[1] == 0 ? 1 : this.shapeR[1];
        int nColumn = this.shapeR[0] == 0 ? 1 : this.shapeR[0];
        int idx = 0;
        y.array = new float[nRow * nColumn];
        for (int column = 0; column < nColumn; column++) 
        {
            for (int row = 0; row < nRow; row++)
            {
                y.array[idx++] = this.FPAt(column, row);
            }
        }
        return y;
    }

    // 要素全てで、ネイピア数eの累乗を求める 
    public static NumArray exp(NumArray x)
    {
        NumArray y = NumArray.create(x.shapeR[0], x.shapeR[1], x.shapeR[2]);
        for (int i = 0; i < y.array.Length; i++) y.array[i] = Mathf.Exp(x.array[i]);
        return y;
    }

    // 集計する。axis=-1の場合、y.array[0] に合計が設定される。
    public static NumArray sum(NumArray x, int axis= -1)
    {
        int nRow = x.shapeR[1] == 0 ? 1 : x.shapeR[1];
        int nColumn = x.shapeR[0] == 0 ? 1 : x.shapeR[0];
        NumArray y = new NumArray();
        float sumV = 0;
        if(axis == -1)
        {
            for (int i = 0; i < x.array.Length; i++) sumV += x.array[i];
            y.shapeR[0] = y.shapeR[1] = y.shapeR[2] = 0;
            y.array = new float[1] { sumV };
        }
        else if (axis == 0)//同列の各要素を集計 ★★
        {
            y.shapeR[0] = nColumn;
            y.shapeR[1] = y.shapeR[2] = 0;
            y.array = new float[nColumn];
            for (int column = 0; column < nColumn; column++)
            {
                for (int row = 0; row < nRow; row++)
                {
                    y.array[column] += x.FPAt(column, row);
                }
            }
        }
        else if (axis == 1)//同行の各要素の集計
        {
            y.shapeR[0] = y.shapeR[1] = y.shapeR[2] = 0;
            y.array = new float[1] { sumV };
            y.shapeR[0] = x.shapeR[1];
            y.shapeR[1] = y.shapeR[2] = 0;
            y.array = new float[nRow];
            for (int row = 0; row < nRow; row++) 
            {
                for (int column = 0; column < nColumn; column++)
                {
                    y.array[row] += x.FPAt(column, row);
                }
            }
        }
        return y;
    }

    // 集計する。axis=-1の場合、y.array[0] に最大値が設定される。
    public static NumArray max(NumArray x, int axis = -1)
    {
        int nRow = x.shapeR[1] == 0 ? 1 : x.shapeR[1];
        int nColumn = x.shapeR[0] == 0 ? 1 : x.shapeR[0];
        NumArray y = new NumArray();
        float maxV = -3.402823e+38f;
        if (axis == -1)
        {
            for (int i = 0; i < x.array.Length; i++) if(x.array[i]> maxV) maxV= x.array[i];
            y.shapeR[0] = y.shapeR[1] = y.shapeR[2] = 0;
            y.array = new float[1] { maxV };
        }
        else if (axis == 0)//同列の各要素で求める
        {
            y.shapeR[0] = x.shapeR[1];
            y.shapeR[1] = y.shapeR[2] = 0;
            y.array = new float[nColumn];
            for (int column = 0; column < nColumn; column++)
            {
                for (int row = 1; row < nRow; row++)
                {
                    if(x.FPAt(column, row) > y.array[column]) y.array[column] = x.FPAt(column, row);
                }
            }
        }
        else if (axis == 1)//同行の各要素で求める
        {
            y.shapeR[0] = y.shapeR[1] = y.shapeR[2] = 0;
            y.shapeR[0] = x.shapeR[1];
            y.shapeR[1] = y.shapeR[2] = 0;
            y.array = new float[nRow];
            for (int row = 0; row < nRow; row++)
            {
                for (int column = 1; column < nColumn; column++)
                {
                    if(x.FPAt(column, row) > y.array[row]) y.array[row]  = x.FPAt(column, row);
                }
            }
        }
        return y;
    }


    // ソフトマックス関数：xを確率的な値に変換する。
    // x要素群のごちゃごちゃした数字を、最終総和が100%(1.0)になる比率に変えてくれる関数
    public static NumArray softmax(NumArray x)
    {
        NumArray maxX = NumArray.max(x); // maxX.array[0] に最大値
        x =x.deepcopy();
        x.add_scalar(- maxX.array[0]);//# オーバーフロー対策
        //Debug.Log($"- maxX.array[0]:{-maxX.array[0]}, x:{x}");
        NumArray naY = NumArray.exp(x);
        float na_sum = NumArray.sum(naY).array[0];
        naY.mul_scalar(1 / na_sum);
        return naY;
    }

    // 2次元をワンホットの表現の文字列として表現
    public string getOneOfKstring(int i2)
    {
        string s = "";
        for (int i1 = 0; i1 < this.shapeR[0]; i1++)
        {
            s += $"  {this.FPAt(i1, i2),5:F3}";
        }
        return s;
    }

    //   交差エントロピー誤差取得
    public static float cross_entropy_error(NumArray y, NumArray t)
    {
        //Debug.Log($"y:{y}\nt:{t}");
        float rtnV = 0;
        for(int i=0; i < y.array.Length; i++)
        {
            rtnV += -Mathf.Log(y.array[i]) * t.array[i];
        }
        return rtnV;
    }

    // 確認用の文字列化
    public override string ToString()
    {
        if (this.shapeR[0] == 0)
        {
            return $" {this.array[0],8:F4}";
        }
        bool brackets0 = false;// 始まりのカッコ出力済みでtrue
        bool brackets1 = false;
        bool brackets2 = false;
        bool flagLF = false;
        int dim1 = this.shapeR[1] == 0 ? 1 : this.shapeR[1];
        int dim2 = this.shapeR[2] == 0 ? 1 : this.shapeR[2];
        string bS1 = this.shapeR[1] == 0 ? "" : "[";
        string bS2 = this.shapeR[2] == 0 ? "" : "[";
        string bE1 = this.shapeR[1] == 0 ? "" : "]";
        string bE2 = this.shapeR[2] == 0 ? "" : "]";

        string s = "";
        s += $"{this.shapeR[0]}, {this.shapeR[1]}, {this.shapeR[2]}\n";
        for (int i = 0; i < this.array.Length + 1; i++)
        {
            bool s0 = i % this.shapeR[0] == 0;
            bool s1 = i % (this.shapeR[0]* dim1) == 0;
            bool s2 = i % (this.shapeR[0] * dim1 * dim2) == 0;
            if (s0 && brackets0) { s += "]"; brackets0 = false; flagLF = true; }
            if (s1 && brackets1) { s += bE1; brackets1 = false; }
            if (s2 && brackets2) { s += bE2; brackets2 = false; }
            if (i == this.array.Length) break;
            if (flagLF) { s += $"\n"; flagLF = false; };
            if (s2 && !brackets2) { s += bS2; brackets2 = true; }
            if (s1 && !brackets1) { s += bS1; brackets1 = true; }
            if (s0 && !brackets0) { s += "["; brackets0 = true; }

            s += $" {this.array[i],8:F4}"; // 要素の表示
        }
        int countLF = System.Text.RegularExpressions.Regex.Matches(s,"\n").Count;
        if(countLF > 30 ) //&& false) // 条件行数を超えた場合の一部省略
        {
            int i = 0;
            for(int count = 0; count++ < 10; ) i = s.IndexOf("\n", i)+1;
            string s1 = s.Substring(0, i);
            i = s.Length - 1;
            for (int count = 0; count++ < 10;) i = s.LastIndexOf("\n", i)-1;
            string s2 = s.Substring(i);
            s = s1 + "...................." + s2; // 一部を省略した文字列
        }
        return s;
    }
}
