using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal.Internal;
using UnityEngine.UI;

public class BlendShaderController : MonoBehaviour
{
    private static int tex1Prop = Shader.PropertyToID("Camera1");
    private static int tex2Prop = Shader.PropertyToID("Camera2");
    private static int texResProp = Shader.PropertyToID("Result");
    private static int offsetProp = Shader.PropertyToID("Offset");
    private static int resXProp = Shader.PropertyToID("ResX");
    private static int farClipPlaneProp = Shader.PropertyToID("FarClipPlane");
    private static int monoChanelResProp = Shader.PropertyToID("MonoChannelResult");
    private static int Depths1Id = Shader.PropertyToID("Depths1");
    private static int Depths2Id = Shader.PropertyToID("Depths2");
    private static int ResDepthsId = Shader.PropertyToID("MergedDepths");

    private static string savePath = Application.streamingAssetsPath + '\\';
    private const string filename = "dsitMap";
    private const string ext = ".txt";

    private Vector3 center
    {
        get
        {
            if (camera1 && camera2)
                return camera1.transform.position + camera1.transform.right * camera2.transform.localPosition.x * 0.5f;
            else
                return Vector3.zero;
        }
    }

    [Range(0.0f, 2.0f)]
    public float testDistance = 0.08f;
    public Shader DepthShader;
    public Material depthMaterial;
    public ComputeShader BlendShader;
    public Camera camera1;
    public Camera camera2;
    public int QuadResolution = 256;
    public float Multiplier;
    public RawImage Reciever;
    [Range(0, 1000)] public float OffsetMultiplier = 1000;

    public RenderTexture test;

    private RenderTexture depth1;
    private RenderTexture depth2;
    private RenderTexture cameraTexture1;
    private RenderTexture cameraTexture2;
    private RenderTexture Result;
    private int currentRes;

    private bool flag = false;

    private ComputeBuffer computeBuffer;
    private ComputeBuffer depthBuffer;
    private float[] resultArray;
    Thread savingThread;
    Task<string> task1;
    Task task2;

    private ComputeBuffer depthsBuffer1;
    private ComputeBuffer depthsBuffer2;
    private ComputeBuffer resDepthsBuffer;

    private int uvOffset;

    // Start is called before the first frame update
    void Start()
    {
        InitializeTextures();
        InitializeComputeBuffer();
    }

    void InitializeComputeBuffer()
    {
        computeBuffer = new ComputeBuffer(currentRes * currentRes, 1 * sizeof(float));//2 dimensional array of 1 value type of float

        depthBuffer = new ComputeBuffer(currentRes * currentRes, 1 * sizeof(float));
        depthMaterial = CoreUtils.CreateEngineMaterial(DepthShader);
        depthMaterial.SetBuffer("distBuffer", depthBuffer);

        depthsBuffer1 = new ComputeBuffer(currentRes * currentRes, 1 * sizeof(float));
        depthsBuffer2 = new ComputeBuffer(currentRes * currentRes, 1 * sizeof(float));
        resDepthsBuffer = new ComputeBuffer(currentRes * currentRes, 1 * sizeof(float));
    }

    void InitializeTextures()
    {
        currentRes = QuadResolution;
        RenderTexture[] defaultTextures = CreateTextures(2, currentRes, currentRes);
        cameraTexture1 = defaultTextures[0];
        cameraTexture2 = defaultTextures[1];

        RenderTexture[] depthTextures = CreateTextures(2, currentRes, currentRes, true);
        depth1 = depthTextures[0];
        depth2 = depthTextures[1];

        Result = CreateTextures(1, currentRes, currentRes)[0];

        Reciever.texture = Result;

        camera1.targetTexture = cameraTexture1;
        camera2.targetTexture = cameraTexture2;
    }

    public static RenderTexture[] CreateTextures(int count, int resX, int resY, bool depth = false)
    {
        RenderTexture[] result = new RenderTexture[count];
        for (int i = 0; i < count; i++)
        {
            result[i] = new RenderTexture(resX, resY, 32, depth ? RenderTextureFormat.Depth : RenderTextureFormat.ARGBFloat);
            result[i].enableRandomWrite = !depth;
            result[i].Create();
        }
        return result;
    }

    // Update is called once per frame
    void Update()
    {
        SetCameraDistance(testDistance);
        ComputeUVOffset();

        UpdateShader(cameraTexture1, cameraTexture2);
    }

    void UpdateShader(RenderTexture tex1, RenderTexture tex2)
    {
        BlendShader.SetTexture(0, tex1Prop, tex1);
        BlendShader.SetTexture(0, tex2Prop, tex2);
        BlendShader.SetTexture(0, texResProp, Result);
        BlendShader.SetFloat(offsetProp, uvOffset);
        BlendShader.SetFloat(resXProp, Result.width);
        BlendShader.SetFloat(farClipPlaneProp, camera1.farClipPlane);
        BlendShader.SetBuffer(0, monoChanelResProp, computeBuffer);
        BlendShader.Dispatch(0, Result.width / 8, Result.height / 8, 1);
    }

    void UpdateShader(float[] depths1, float[] depths2)
    {
        depthsBuffer1.SetData(depths1);
        depthsBuffer2.SetData(depths2);

        BlendShader.SetFloat(offsetProp, uvOffset);
        BlendShader.SetFloat(resXProp, currentRes);

        BlendShader.SetBuffer(1, Depths1Id, depthsBuffer1);
        BlendShader.SetBuffer(1, Depths2Id, depthsBuffer2);
        BlendShader.SetBuffer(1, ResDepthsId, resDepthsBuffer);

        BlendShader.Dispatch(1, currentRes / 8, currentRes / 8, 1);
    }

    void ComputeUVOffset()
    {
        if (camera1 && camera2)
        {
            if (Physics.Raycast(center, camera1.transform.forward, out var hit, 1000))
            {
                Vector3 target = hit.point;
                Vector3 clipPoint1 = camera1.WorldToScreenPoint(target);
                Vector3 clipPoint2 = camera2.WorldToScreenPoint(target);
                float diff = clipPoint1.x - clipPoint2.x;
                uvOffset = (int)(diff * 0.5f);
                return;
            }
        }

        uvOffset = (int)(camera2.transform.localPosition.x * OffsetMultiplier);
    }

    void SwapRenderTargets()
    {
        if (flag)
        {
            camera1.targetTexture = depth1;
            camera2.targetTexture = depth2;
        }
        else
        {
            camera1.targetTexture = cameraTexture1;
            camera2.targetTexture = cameraTexture2;
        }
    }

    public void SetFlag()
    {
        flag = !flag;
        SwapRenderTargets();
    }

    public async void SaveTextureAsync(TaskCompletionSource<bool> tcs = null)
    {
        Debug.Log("Start Saving");
        resultArray = new float[currentRes * currentRes];
        computeBuffer.GetData(resultArray);
        Debug.Log("Data copied");
        task1 = Task.Run(() => SerializeArray());
        await task1;
        Debug.Log("After awaiting");
        string result = task1.Result;

        task2 = SavingProcessAsync(result);
        await task2;
        Debug.Log("After saving");

        if (tcs != null) tcs.SetResult(true);
        Debug.Log("Saving finished");
    }

    public void SaveTexture()
    {
        resultArray = new float[currentRes * currentRes];
        computeBuffer.GetData(resultArray);
        string result = SerializeArray();
        Save(result);
    }

    public async void GetDistArray(TaskCompletionSource<string> tcs)
    {
        int cameraId1 = camera1.GetInstanceID();
        int cameraId2 = camera2.GetInstanceID();
        float[] depths1 = await DepthRenderPassFeature.DepthRenderPass.RequestDepthDataAsync(cameraId1);
        float[] depths2 = await DepthRenderPassFeature.DepthRenderPass.RequestDepthDataAsync(cameraId2);
        UpdateShader(depths1, depths2);
        float[] depths = new float[currentRes * currentRes];
        resDepthsBuffer.GetData(depths);
        byte[] bytes = new byte[depths.Length * sizeof(float)];
        Buffer.BlockCopy(depths, 0, bytes, 0, bytes.Length);
        string base64 = Convert.ToBase64String(bytes);
        string result = "{\"width\":" + currentRes + ",\"height\":" + currentRes + ",\"data\":\"" + base64 + "\"}";
        tcs.SetResult(result);
    }

    private async Task SavingProcessAsync(string data)
    {
        int filesCount = Directory.EnumerateFiles(savePath, filename + '*' + ext).Count();
        Debug.Log("Start saving");
        await File.WriteAllTextAsync(savePath + filename + filesCount.ToString() + ext, data);
        Debug.Log("File saved");
    }

    private void Save(string data)
    {
        int filesCount = Directory.EnumerateFiles(savePath, filename + '*' + ext).Count();
        File.WriteAllText(savePath + filename + filesCount.ToString() + ext, data);
        Debug.Log("File saved");
    }

    private string SerializeArray()
    {
        return SerializeArray(resultArray);
    }

    private string SerializeArray(float[] arr)
    {
        string result = string.Empty;
        Debug.Log("StartSerializing");
        for (int i = 0; i < resultArray.Length; i += currentRes)
        {
            result += string.Join("; ", arr[i..(i + currentRes)]) + '\n';
        }
        Debug.Log("EndSerializing");
        return result;
    }

    private float GetDistanceByDepth()
    {
        resultArray = new float[currentRes * currentRes];
        computeBuffer.GetData(resultArray);
        int idx = (int)(currentRes * (0.5f + currentRes * 0.5f));
        float zDepth = resultArray[idx];
        float far = camera1.farClipPlane;
        return zDepth * far; 
    }

    private float GetDistanceByRayCast()
    {
        Vector3 position = camera1.transform.position + camera1.transform.right * camera2.transform.localPosition.x * 0.5f;
        if (Physics.Raycast(position, camera1.transform.forward, out var hit, 100))
        {
            return hit.distance;
        }

        return -1;
    }

    public double[] GetIntrinsicParameters(Camera camera)
    {
        int width = camera.targetTexture != null ? camera.targetTexture.width : camera.pixelWidth;
        int height = camera.targetTexture != null ? camera.targetTexture.height : camera.pixelHeight;
        double fovRad = camera.fieldOfView * Math.PI / 180.0;
        double fy = (height * 0.5) / Math.Tan(fovRad * 0.5);
        double aspect = (double)width / height;
        double fx = fy * aspect;
        double cx = width * 0.5;
        double cy = height * 0.5;
        return new double[] { fx, 0, cx, 0, fy, cy, 0, 0, 1 };
    }

    public string GetIntrinsicParametersAsBase64(Camera camera)
    {
        double[] intrinsics = GetIntrinsicParameters(camera);
        byte[] bytes = new byte[intrinsics.Length * sizeof(double)];
        Buffer.BlockCopy(intrinsics, 0, bytes, 0, bytes.Length);
        return Convert.ToBase64String(bytes);
    }

    public string GetLeftIntrinsicParametersAsBase64()
    {
        return GetIntrinsicParametersAsBase64(camera1);
    }

    public string GetRightIntrinsicParametersAsBase64()
    {
        return GetIntrinsicParametersAsBase64(camera2);
    }

    public double[] GetExtrinsicParameters(Camera camera)
    {
        Matrix4x4 m = camera.worldToCameraMatrix;
        return new double[]
        {
            m.m00, m.m01, m.m02, m.m03,
            m.m10, m.m11, m.m12, m.m13,
            m.m20, m.m21, m.m22, m.m23,
            m.m30, m.m31, m.m32, m.m33
        };
    }

    public string GetExtrinsicParametersAsBase64(Camera camera)
    {
        double[] extrinsics = GetExtrinsicParameters(camera);
        byte[] bytes = new byte[extrinsics.Length * sizeof(double)];
        Buffer.BlockCopy(extrinsics, 0, bytes, 0, bytes.Length);
        return Convert.ToBase64String(bytes);
    }

    public string GetLeftExtrinsicParametersAsBase64()
    {
        return GetExtrinsicParametersAsBase64(camera1);
    }

    public string GetRightExtrinsicParametersAsBase64()
    {
        return GetExtrinsicParametersAsBase64(camera2);
    }

    public float GetCameraDistance()
    {
        if (camera1 == null || camera2 == null)
            return -1f;
        return camera2.transform.localPosition.x;
    }

    public void SetCameraDistance(float distance)
    {
        if (camera1 == null || camera2 == null)
            return;

        Vector3 direction = camera1.transform.right;
        distance = Mathf.Max(0, distance);
        Vector3 camera2LocPos = camera2.transform.localPosition;
        camera2.transform.localPosition = new Vector3(distance, camera2LocPos.y, camera2LocPos.z);
        camera1.transform.position = center - direction * distance * 0.5f;
    }

    public RenderTexture CombineTexturesSideBySide(RenderTexture left, RenderTexture right)
    {
        if (left == null || right == null)
        {
            Debug.LogError("Both render textures must be provided");
            return null;
        }
        if (left.width != right.width || left.height != right.height)
        {
            Debug.LogError("Both render textures must have the same dimensions");
            return null;
        }

        int w = left.width;
        int h = left.height;

        RenderTexture combined = new RenderTexture(w * 2, h, 0, RenderTextureFormat.ARGBFloat);
        combined.Create();

        Graphics.CopyTexture(left, 0, 0, 0, 0, w, h, combined, 0, 0, 0, 0);
        Graphics.CopyTexture(right, 0, 0, 0, 0, w, h, combined, 0, 0, w, 0);

        return combined;
    }

    public string GetCombinedTexturesAsBase64Json()
    {
        RenderTexture combined = CombineTexturesSideBySide(cameraTexture1, cameraTexture2);
        if (combined == null) return null;

        int w = combined.width;
        int h = combined.height;

        RenderTexture activeRT = RenderTexture.active;
        RenderTexture.active = combined;

        Texture2D tex = new Texture2D(w, h, TextureFormat.RGBAFloat, false);
        tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
        tex.Apply();
        RenderTexture.active = activeRT;

        byte[] rawData = tex.GetRawTextureData();
        Destroy(tex);
        combined.Release();

        string base64 = Convert.ToBase64String(rawData);

        return "{\"width\":" + w + ",\"height\":" + h + ",\"data\":\"" + base64 + "\"}";
    }

    private void OnDisable()
    {
        computeBuffer.Release();
        computeBuffer = null;

        depthBuffer.Release();
        depthBuffer = null;

        depthsBuffer1.Release();
        depthsBuffer1 = null;
        depthsBuffer2.Release();
        depthsBuffer2 = null;
        resDepthsBuffer.Release();
        resDepthsBuffer = null;

        Destroy(depthMaterial);
        depthMaterial = null;
    }
}
