﻿// poorly-written 4chan browser
// t. frosty fridge

using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon;
using VRC.SDK3.Image;
using VRC.SDK3.StringLoading;
using VRC.Udon.Common.Interfaces;
using VRC.SDK3.Data;
using TMPro;

public class BrowerController : UdonSharpBehaviour
{

    const float minPostHeight = 300f;
    const int postSpaces = 1;
    const float imageMarginCutoff = 1.04f;
    const int textureTileSize = 16;
    const float imageSize = 120f;
    const float postBoxPadding = 6f;
    private Vector3 imageLocation { get { return new Vector3(16f, -36f, 0f); } }

    [SerializeField, Tooltip("Text API URL")]
    VRCUrl stringUrl;
    [SerializeField, Tooltip("Image API URL")]
    VRCUrl imageUrl;
    [SerializeField, Tooltip("Number of posts to display per page")]
    public int pageSize = 100;
    [Tooltip("Cooldown on triggering downloads")]
    public float downloadCooldown;
    [Tooltip("Whether or not to enable auto-refresh by default")]
    public bool autoRefresh = true;
    [SerializeField, Tooltip("Whether or not to enable auto-bottoming by default. Auto-bottoming, or following, will take the user to the most recent posts whenever the thread refreshes")]
    public bool autoBottom = true;

    // Reference TMP and canvas are for determining post height. They should match the horizontal dimensions of actual posts
    public TextMeshProUGUI referenceTMP;
    public RectTransform referenceCanvas;

    private IUdonEventReceiver _udonEventReceiver;
    private VRCImageDownloader _imageDownloader;
    private bool downloadingText;
    private bool downloadingImages;
    private float currentDownloadCooldown;
    private string downloadCooldownText;
    private DataDictionary postHeightCache;
    private string threadText;
    private string motdText;
    private string statusText;
    private Vector3[] vertices;
    private Vector2[] uv;
    private int[] triangles;
    public MeshFilter threadMesh;
    private Vector2 postColorUV;

    // pagination
    private DataList postList;
    private int currentPage;
    private int pageCount;

    void OnEnable()
    {
        Debug.Log("Starting Browser...");

        _imageDownloader = new VRCImageDownloader();
        _udonEventReceiver = (IUdonEventReceiver)this;

        postHeightCache = new DataDictionary();


        postColorUV = new Vector2(.97f, .01f);
        motdText = "/vrg/ - virtual reality general";
        statusText = "";
        StartLoad();
    }

    private void Update()
    {
        if (!downloadingImages && !downloadingText)
        {
            if (currentDownloadCooldown > 0)
            {
                downloadCooldownText = ((int)currentDownloadCooldown).ToString();
                currentDownloadCooldown -= Time.deltaTime;
            }
            else
            {
                downloadCooldownText = "0";
                if (autoRefresh)
                {
                    StartLoad();
                }
            }
        }

    }

    public bool StartLoad()
    {
        if (currentDownloadCooldown > 0 || downloadingImages || downloadingText)
        {
            return false;
        }
        currentDownloadCooldown = downloadCooldown;
        Debug.Log("Attempting download from " + stringUrl.ToString());
        VRCStringDownloader.LoadUrl(stringUrl, _udonEventReceiver);

        Debug.Log("Attempting to download images from " + imageUrl);
        var rgbInfo = new TextureInfo();
        rgbInfo.GenerateMipMaps = true;
        _imageDownloader.DownloadImage(imageUrl, threadMesh.GetComponent<MeshRenderer>().sharedMaterial, _udonEventReceiver, rgbInfo);

        downloadingText = true;
        downloadingImages = true;

        return true;
    }

    public Mesh GetThreadMesh()
    {
        return threadMesh.mesh;
    }

    public string GetThreadString()
    {
        return threadText;
    }

    public string GetCurrentCooldown()
    {
        return downloadCooldownText;
    }

    public string GetStatus()
    {
        return statusText;
    }

    public string GetMotd()
    {
        return motdText;
    }

    public bool GetAutoRefresh()
    {
        return autoRefresh;
    }

    public void SetAutoRefresh(bool toggle)
    {
        autoRefresh = toggle;
    }

    public bool GetAutoBottom()
    {
        return autoBottom;
    }

    public void SetAutoBottom(bool toggle)
    {
        autoBottom = toggle;
    }

    public int GetCurrentPage()
    {
        return currentPage;
    }

    public void SetCurrentPage(int pageIndex)
    {
        if (pageIndex < 0 || pageIndex > GetPageCount() || currentPage == pageIndex)
        {
            return;
        }
        currentPage = pageIndex;
        RenderPage(currentPage);
    }
    public int GetPageCount()
    {
        return pageCount;
    }
    public int GetPostCount()
    {
        if (postList == null)
        {
            return 0;
        }
        return postList.Count;
    }

    public override void OnImageLoadSuccess(IVRCImageDownload result)
    {
        downloadingImages = false;
        Debug.Log($"Image loaded: {result.SizeInMemoryBytes} bytes.");
    }

    public override void OnImageLoadError(IVRCImageDownload result)
    {
        downloadingImages = false;
        Debug.Log($"Image not loaded: {result.Error.ToString()}: {result.ErrorMessage}.");
    }
    public override void OnStringLoadSuccess(IVRCStringDownload downloadedText)
    {
        downloadingText = false;
        Debug.Log("Downloaded string!");
        if (VRCJson.TryDeserializeFromJson(downloadedText.Result, out DataToken result))
        {
            if (result.TokenType == TokenType.DataDictionary)
            {
                if (result.DataDictionary.TryGetValue("motd", TokenType.String, out DataToken motdToken))
                {
                    motdText = motdToken.String;
                }
                if (result.DataDictionary.TryGetValue("status", TokenType.String, out DataToken statusToken))
                {
                    statusText = statusToken.String;
                }
                if (result.DataDictionary.TryGetValue("posts", TokenType.DataList, out DataToken posts)) {
                    if (postList != null)
                    {
                        int oldPostCount = postList.Count;
                        postList = posts.DataList;
                        if (oldPostCount == postList.Count)
                        {
                            return;
                        }
                    }
                    else
                    {
                        postList = posts.DataList;
                    }

                } else {
                    return;
                }
            }
        }
        // Calculate page count
        int leftovers = postList.Count % pageSize;
        int totalPages = postList.Count / pageSize;
        if (leftovers != 0)
        {
            totalPages += 1;
        }
        pageCount = totalPages;

        RenderPage(currentPage);
    }


    public void RenderPage(int page)
    {
        if (page >= pageCount)
        {
            return;
        }
        int lowerPostIndex = page * pageSize;
        int upperPostIndex;
        if (page == pageCount - 1)
        {
            upperPostIndex = postList.Count;
        }
        else
        {
            upperPostIndex = (page + 1) * pageSize;
        }

        RenderThread(lowerPostIndex, upperPostIndex);
    }

    private void RenderThread(int lowerPostIndex, int upperPostIndex)
    {
        int currentImage = 0;
        int[] images = new int[upperPostIndex - lowerPostIndex];
        float[] postWidths = new float[upperPostIndex - lowerPostIndex];
        float w = referenceTMP.GetComponent<RectTransform>().rect.width;
        //todo: move this so its not happening on all posts
        for (int i = 0; i < upperPostIndex; i++)
        {
            int adjustedIndex = i - lowerPostIndex;
            if (adjustedIndex >= 0)
            {
                postWidths[adjustedIndex] = w;
            }
            if (postList.TryGetValue(i, out DataToken post))
            {
                if (post.DataDictionary.TryGetValue("ext", out DataToken ext))
                {
                    if (adjustedIndex >= 0)
                    {
                        if (i == 0)
                        {
                            images[i] = -2;
                        }
                        else
                        {
                            images[adjustedIndex] = currentImage;
                        }
                    }
                    if (i != 0)
                    {
                        currentImage += 1;
                    }
                }
                else
                {
                    if (adjustedIndex >= 0)
                    {
                        images[adjustedIndex] = -1;
                    }
                }
            }
        }
        float[] postHeights = new float[upperPostIndex - lowerPostIndex];
        string newThreadText = "";
        string postSpacingText = "";
        for (int j = 0; j < postSpaces; j++)
        {
            postSpacingText += "<br>";
        }

        for (int i = lowerPostIndex; i < upperPostIndex; i++)
        {
            if (postList.TryGetValue(i, out DataToken post))
            {
                int postNum = (int)post.DataDictionary["no"].Double;
                string postName = post.DataDictionary["name"].String;
                string postDateTime = post.DataDictionary["now"].String;
                float postHeight = minPostHeight;
                string postText = $"<indent=2%><b>{postName}</b> {postDateTime} No.{postNum.ToString()}</indent><br><br>";

                if (post.DataDictionary.TryGetValue("ext", out DataToken ext))
                {
                    if (post.DataDictionary.TryGetValue("com", out DataToken comment))
                    {
                        postText += "<indent=20%>";
                        postText += comment.String + "</indent><br>";
                    }
                    postHeight = GetTextHeight(postNum.ToString(), postText);
                    while (postHeight < minPostHeight)
                    {
                        postText += "<br>";
                        postHeight = GetTextHeight("null", postText);
                    }
                }
                else
                {
                    if (post.DataDictionary.TryGetValue("com", out DataToken comment))
                    {
                        postText += "<indent=5%>";
                        postText += comment.String + "</indent><br>";
                    }
                    postHeight = GetTextHeight(postNum.ToString(), postText);
                }
                postHeights[i - lowerPostIndex] = postHeight;
                newThreadText += postText;
                newThreadText += postSpacingText;
            }
        }
        threadText = newThreadText;
        float threadHeight = GetTextHeight("null", threadText);
        threadMesh.mesh = CreateThreadMesh(postHeights, postWidths, images, threadHeight);
    }

    public override void OnStringLoadError(IVRCStringDownload result)
    {
        downloadingText = false;
        Debug.LogError($"Could not load string {result.Error}");
    }

    private float GetTextHeight(string key, string text)
    {
        if (key != "null" && postHeightCache.TryGetValue(key, out DataToken value))
        {
            return value.Float;
        }
        else
        {
            float height = CalculateTextHeight(text, referenceTMP);
            postHeightCache.SetValue(key, height);
            return height;
        }
    }

    public float CalculateTextHeight(string text, TextMeshProUGUI textComponent)
    {
        // Set the text to measure and adjust the width
        textComponent.text = text;

        // Force a text update with the new settings
        textComponent.ForceMeshUpdate();
        LayoutRebuilder.ForceRebuildLayoutImmediate(referenceCanvas);
        RectTransform rectTransform = textComponent.GetComponent<RectTransform>();

        return rectTransform.rect.height;
    }


    public Mesh CreateThreadMesh(float[] postHeights, float[] postWidths, int[] images, float totalHeight)
    {
        //Debug.Log($"postheights: {postHeights.Length}, postWidths: {postWidths.Length}, images: {images.Length}");
        var mesh = new Mesh();
        int imagePostTotal = 0;
        for (int i = 0; i < images.Length; i++)
        {
            if (images[i] != -1)
            {
                imagePostTotal++;
            }
        }
        int textPostTotal = postHeights.Length - imagePostTotal;

        // Rescale everything by a set factor.
        float scaleFactor = 0.5f;
        for (int i = 0; i < postHeights.Length; i++)
        {
            postHeights[i] = postHeights[i] * scaleFactor;
            postWidths[i] = postWidths[i] * scaleFactor;
        }
        //postSpacing *= scaleFactor;
        totalHeight *= scaleFactor;

        //Calculate appropriate post spacing. 
        float totalPostHeight = 0;
        for (int i = 0; i < postHeights.Length; i++)
        {
            totalPostHeight += postHeights[i];
        }
        float postSpacing = (totalHeight - totalPostHeight) / (postHeights.Length);
        //Debug.Log($"Post Spacing: {postSpacing.ToString()}");
        //Debug.Log($"Total Height: {totalHeight.ToString()}");
        float heightdebug = 0;



        float currentY = 0;

        Vector3[] vertices = new Vector3[(textPostTotal * 4) + (imagePostTotal * 12)];
        Vector2[] uv = new Vector2[(textPostTotal * 4) + (imagePostTotal * 12)];
        int[] triangles = new int[(2 * textPostTotal * 3) + (10 * imagePostTotal * 3)];

        int textPostCount = 0;
        int imagePostCount = 0;
        for (int i = 0; i < postHeights.Length; i++)
        {
            int baseVert = (textPostCount * 4) + (imagePostCount * 12);
            int baseTri = (2 * textPostCount * 3) + (10 * imagePostCount * 3);
            // Text posts
            if (i >= images.Length || images[i] == -1)
            {
                vertices[baseVert] = new Vector3(0, currentY + postBoxPadding, 0);
                vertices[baseVert + 1] = new Vector3(postWidths[i], currentY + postBoxPadding, 0); ;
                vertices[baseVert + 2] = new Vector3(postWidths[i], currentY - (postHeights[i] + postBoxPadding), 0);
                vertices[baseVert + 3] = new Vector3(0, currentY - (postHeights[i] + postBoxPadding), 0);
                uv[baseVert] = postColorUV;
                uv[baseVert + 1] = postColorUV + new Vector2(0.02f, 0f);
                uv[baseVert + 2] = postColorUV + new Vector2(0.02f, 0.02f);
                uv[baseVert + 3] = postColorUV + new Vector2(0f, 0.02f);

                triangles[baseTri + 0] = baseVert + 0;
                triangles[baseTri + 1] = baseVert + 1;
                triangles[baseTri + 2] = baseVert + 2;
                triangles[baseTri + 3] = baseVert + 2;
                triangles[baseTri + 4] = baseVert + 3;
                triangles[baseTri + 5] = baseVert + 0;

                textPostCount++;
            }
            else
            { //Image posts
                vertices[baseVert] = new Vector3(0, currentY + postBoxPadding, 0);
                vertices[baseVert + 1] = new Vector3(postWidths[i], currentY + postBoxPadding, 0);
                vertices[baseVert + 2] = new Vector3(postWidths[i], currentY - (postHeights[i] + postBoxPadding), 0);
                vertices[baseVert + 3] = new Vector3(0, currentY - (postHeights[i] + postBoxPadding), 0);
                vertices[baseVert + 4] = vertices[baseVert] + imageLocation;
                vertices[baseVert + 5] = vertices[baseVert] + imageLocation + new Vector3(imageSize, 0, 0);
                vertices[baseVert + 6] = vertices[baseVert] + imageLocation + new Vector3(imageSize, -imageSize, 0);
                vertices[baseVert + 7] = vertices[baseVert] + imageLocation + new Vector3(0, -imageSize, 0);
                vertices[baseVert + 8] = vertices[baseVert + 4];
                vertices[baseVert + 9] = vertices[baseVert + 5];
                vertices[baseVert + 10] = vertices[baseVert + 6];
                vertices[baseVert + 11] = vertices[baseVert + 7];

                uv[baseVert] = postColorUV;
                uv[baseVert + 1] = postColorUV + new Vector2(0.02f, 0f);
                uv[baseVert + 2] = postColorUV + new Vector2(0.02f, 0.02f);
                uv[baseVert + 3] = postColorUV + new Vector2(0f, 0.02f);
                uv[baseVert + 4] = postColorUV + new Vector2(0.012f, 0.012f);
                uv[baseVert + 5] = postColorUV + new Vector2(0.015f, 0.012f);
                uv[baseVert + 6] = postColorUV + new Vector2(0.015f, 0.015f);
                uv[baseVert + 7] = postColorUV + new Vector2(0.012f, 0.015f);
                //special handling for OP posts
                if (images[i] == -2)
                {
                    Vector2 imageCoords = new Vector2((float)(textureTileSize - 3) / textureTileSize, 2.0f / textureTileSize);
                    uv[baseVert + 8] = imageCoords;
                    uv[baseVert + 9] = imageCoords + new Vector2(1.0f / ((textureTileSize / 2) * imageMarginCutoff), 0);
                    uv[baseVert + 10] = imageCoords + new Vector2(1.0f / ((textureTileSize / 2) * imageMarginCutoff), -1.0f / ((textureTileSize / 2) * imageMarginCutoff));
                    uv[baseVert + 11] = imageCoords + new Vector2(0, -1.0f / ((textureTileSize / 2) * imageMarginCutoff));
                }
                else
                {
                    Vector2 imageCoords = new Vector2((float)(images[i] % textureTileSize) / textureTileSize, 1 - ((float)(images[i] / textureTileSize) / textureTileSize));
                    uv[baseVert + 8] = imageCoords;
                    uv[baseVert + 9] = imageCoords + new Vector2(1.0f / (textureTileSize * imageMarginCutoff), 0);
                    uv[baseVert + 10] = imageCoords + new Vector2(1.0f / (textureTileSize * imageMarginCutoff), -1.0f / (textureTileSize * imageMarginCutoff));
                    uv[baseVert + 11] = imageCoords + new Vector2(0, -1.0f / (textureTileSize * imageMarginCutoff));
                }
                /*
                Debug.Log(String.Format("First UV Coord: {0} , {1}", uv[baseVert + 8].x, uv[baseVert + 8].y));
                Debug.Log(String.Format("Second UV Coord: {0} , {1}", uv[baseVert + 9].x, uv[baseVert + 9].y));
                Debug.Log(String.Format("Third UV Coord: {0} , {1}", uv[baseVert + 10].x, uv[baseVert + 10].y));
                Debug.Log(String.Format("Fourth UV Coord: {0} , {1}", uv[baseVert + 11].x, uv[baseVert + 11].y));
                */
                // 0 1 4, 1 5 4, 1 2 5, 5 2 6, 6 2 3, 6 3 7, 0 7 3, 0 4 7, 8 9 11, 9 10 11
                triangles[baseTri] = baseVert + 0;
                triangles[baseTri + 1] = baseVert + 1;
                triangles[baseTri + 2] = baseVert + 4;

                triangles[baseTri + 3] = baseVert + 1;
                triangles[baseTri + 4] = baseVert + 5;
                triangles[baseTri + 5] = baseVert + 4;

                triangles[baseTri + 6] = baseVert + 1;
                triangles[baseTri + 7] = baseVert + 2;
                triangles[baseTri + 8] = baseVert + 5;

                triangles[baseTri + 9] = baseVert + 5;
                triangles[baseTri + 10] = baseVert + 2;
                triangles[baseTri + 11] = baseVert + 6;

                triangles[baseTri + 12] = baseVert + 6;
                triangles[baseTri + 13] = baseVert + 2;
                triangles[baseTri + 14] = baseVert + 3;

                triangles[baseTri + 15] = baseVert + 6;
                triangles[baseTri + 16] = baseVert + 3;
                triangles[baseTri + 17] = baseVert + 7;

                triangles[baseTri + 18] = baseVert + 0;
                triangles[baseTri + 19] = baseVert + 7;
                triangles[baseTri + 20] = baseVert + 3;

                triangles[baseTri + 21] = baseVert + 0;
                triangles[baseTri + 22] = baseVert + 4;
                triangles[baseTri + 23] = baseVert + 7;

                triangles[baseTri + 24] = baseVert + 8;
                triangles[baseTri + 25] = baseVert + 9;
                triangles[baseTri + 26] = baseVert + 11;

                triangles[baseTri + 27] = baseVert + 9;
                triangles[baseTri + 28] = baseVert + 10;
                triangles[baseTri + 29] = baseVert + 11;

                imagePostCount++;
            }

            currentY = (currentY - postHeights[i]) - postSpacing;
        }
        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.triangles = triangles;
        return mesh;

    }

}


