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.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 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 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 float[] resultArray;
    Thread savingThread;
    Task<string> task1;
    Task task2;

    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
    }

    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();

        if (Input.GetKeyDown(KeyCode.F))
        {
            SetFlag();
        }

        if (flag)
        {
            GenerateDepth();
        }

        UpdateShader(cameraTexture1, cameraTexture2);

        if(Input.GetKeyDown(KeyCode.S))
        {
            SaveTextureAsync();
        }

        if (Input.GetKeyDown(KeyCode.A))
        {
            SaveTexture();
        }
    }

    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 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 GenerateDepth()
    {
        //camera1.RenderWithShader(DepthShader, "RenderType");
        //camera2.RenderWithShader(DepthShader, "RenderType");

        Graphics.Blit(depth1, cameraTexture1); //Depth generated due to format of render texture
        Graphics.Blit(depth2, cameraTexture2);
    }

    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 void GetDistArray()
    {
        
    }

    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()
    {
        string result = string.Empty;
        Debug.Log("StartSerializing");
        for (int i = 0; i < resultArray.Length; i += currentRes)
        {
            result += string.Join(", ", resultArray[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 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;
    }

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