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

using System;
using System.Text;
using TMPro;
using UnityEngine.Animations;
using UnityEngine.UI;

public class GuideGrabberParser : UdonSharpBehaviour
{
    public VRCUrl _url = new VRCUrl("");
    public VRCUrl guideImageURL = new VRCUrl("");
    public VRCUrl[] channelURLs = new VRCUrl[0];
    public TextAsset channelInfoFile;
    public bool IsPanelActive = false;
    public Transform GuideSpawnLocation;
    public GameObject GuidePanelPrefab;
    public GameObject GuideChannelHeaderPrefab;
    public GameObject GuideRowPrefab;
    public GameObject GuideEntryPrefab;
    public Slider GuideScrollbar;
    public Material PreviewImageMaterial;
    public Canvas passRemoteView;

    private UdonBehaviour a;
    private IUdonEventReceiver _udonEventReceiver;
    private Texture2D loadedThumbImages;
    DataToken _lastParsedGuide;
    DataToken[] _channels;
    int _availableChannels;
    GameObject ActiveGuidePanel;
    String[][] availableChannelsArray;
    public object[][][] availableEntriesArray;

    private string[][] channelInfoArray;
    private TextMeshProUGUI passCurrentHolderText;
    private TextMeshProUGUI passToText;
    private TextMeshProUGUI CurrentChannelText;
    private TextMeshProUGUI CurrentTimeText;
    private TextMeshProUGUI ShowTitleText;
    private TextMeshProUGUI ShowTimeText;
    private TextMeshProUGUI ShowPlotText;
    private GameObject PreviewImage;
    private CanvasRenderer PreviewImageRenderer;
    private Transform ChannelHeaderRow;
    private Transform ScrollTransform;
    private Transform ScrollContent;
    private Transform LoadingOverlay;
    private TextMeshProUGUI LoadingText;
    private Transform LoadingIsTakingForeverText;
    private Light[] activeChannelButtons = {};
    private Light[] activeGuideButtons = {};
    private Transform InfoOverlay;
    private TextMeshProUGUI InfoButton;
    private TextMeshProUGUI HideInfoButton;

    Transform[] GuideChannelHeaders = new Transform[5];
    Transform[] GuideRows = new Transform[5];
    private TextMeshProUGUI[] GuideChannelHeadersText = new TextMeshProUGUI[5];
    private int SpawnedRowNum;
    private VRCImageDownloader _imageDownloader;
    private TextureInfo rgbInfo;
    private bool updateReady = false;
    int renderGuideTimeScale = 4;
    private float scrollLocation = 0;
    private int maxScrollOffset = 0;

    private int currentPassSelectIndex = 0;
    private bool isLoadingVideo = false;
    private int loadingStage = 0;
    private float loadingTime = 0f;
    private float lastRedrawTime = 0f;
    private float lastMetadataRefreshTime = 0f;
    private float lastUserInteractionTime = 0f;
    private bool arePreviewsReady = false;

    [UdonSynced]
    private bool isPlayerIdle = true;

    [UdonSynced]
    private int currentTunedChannel = 1; //default channel

    private int currentlyPlayingMedia;
    private bool canChangeVideo;
    private VRCPlayerApi local;
    private VRCPlayerApi[] playersInWorld;

    // protv stuff to control the tv
    public UdonBehaviour videoPlayer;

    public void _TvPlay()
    {
        if (isLoadingVideo)
        {
            Debug.Log("stop loading callback received");
            EndLoadingVideo();
            // hack: send a re-sync request 5 seconds after loading ends, causing a small stutter to hopefully prevent a bigger, uglier one later
            //videoPlayer.SendCustomEventDelayedSeconds("_Play", 1.1f, VRC.Udon.Common.Enums.EventTiming.Update);
        }
    }

    public void _TvVideoPlayerError()
    {
        Debug.Log("video player returned error");
    }

    void Start()
    {
        a = (UdonBehaviour) this.gameObject.GetComponent(typeof(UdonBehaviour));
        _udonEventReceiver = (IUdonEventReceiver)this;
        _imageDownloader = new VRCImageDownloader();
        rgbInfo = new TextureInfo();
        videoPlayer.SetProgramVariable("IN_LISTENER", a);
        videoPlayer.SendCustomEvent("_RegisterListener");
        local = Networking.LocalPlayer;
        if (Networking.GetOwner(a.gameObject).Equals(local))
        {
            canChangeVideo = true;
        }

        string[] channelInfoBlocks = channelInfoFile.text.Split('\n');

        channelInfoArray = new string[channelInfoBlocks.Length][];
        for (int i = 0; i < channelInfoBlocks.Length; i++)
        {
            string[] channelInfo = channelInfoBlocks[i].Split(';');
            channelInfoArray[i] = new string[channelInfo.Length];
            for (int j = 0; j < channelInfo.Length; j++)
            {
                channelInfoArray[i][j] = channelInfo[j];
            }
        }

        foreach (VRCUrl url in channelURLs)
        {
            Debug.Log(url);
        }

        //if (canChangeVideo)
        //{
        //    ChangeChannel(currentTunedChannel);
        //}
    }

    public override void OnPlayerJoined(VRCPlayerApi player)
    {
        playersInWorld = new VRCPlayerApi[VRCPlayerApi.GetPlayerCount()];
        playersInWorld = VRCPlayerApi.GetPlayers(playersInWorld);
        if (local.IsOwner(a.gameObject))
        {
            Debug.Log("sending joined player network vars");
            RequestSerialization(); // if we hold the remote, send variables to late joiners to make sure they're caught up
        }
    }

    public override void OnPlayerLeft(VRCPlayerApi player)
    {
        playersInWorld = new VRCPlayerApi[VRCPlayerApi.GetPlayerCount()];
        playersInWorld = VRCPlayerApi.GetPlayers(playersInWorld);
        if (Networking.GetOwner(a.gameObject).Equals(local))
        { // did we become the new remote holder, if the last one left?
            canChangeVideo = true;
        }
        else
        {
            canChangeVideo = false;
        }
    }

    public void ForceReturnRemoteToMaster()
    {
        Debug.Log("hit force return routine");
        if (Networking.GetOwner(a.gameObject).Equals(local))
        {
            Debug.Log("master requested remote yank");
            PassRemoteToPlayer(Networking.Master);
        }
    }

    public void ForceReturnRemoteToMasterProxy()
    {
        if (Networking.Master.isLocal)
        {
            Debug.Log("we, master, tried to yank the remote");
            a.SendCustomNetworkEvent(NetworkEventTarget.Owner, "ForceReturnRemoteToMaster");
        }
    }

    public override bool OnOwnershipRequest(VRCPlayerApi requester, VRCPlayerApi newOwner)
    {
        Debug.Log("ownership request hit");
        if (newOwner.isLocal || requester.isLocal)
        {
            Debug.Log("ownership request returns true, remote handoff clean");
            return true; // maybe hack docs are vague about this one
        }
        Debug.Log("ownership request returns false");
        return false;
    }

    public override void OnOwnershipTransferred(VRCPlayerApi player)
    {
        Debug.Log("ownership transferred");
        Debug.Log("we are: " + VRCPlayerApi.GetPlayerId(local) + "new remote holder id: " + VRCPlayerApi.GetPlayerId(Networking.GetOwner(a.gameObject)));
        if (Networking.GetOwner(a.gameObject).Equals(local))
        { // are we the new owner?
            Debug.Log("we became the new owner!");
            canChangeVideo = true;
            currentPassSelectIndex = 0;
            RenderPassText();
        }
    }

    void PassRemoteToPlayer(VRCPlayerApi player)
    {
        if (!player.Equals(local) && player.IsValid())
        {
            if (Networking.GetOwner(a.gameObject).Equals(local))
            { // if we are the current remote holder
                Debug.Log("we passed the remote");
                Networking.SetOwner(player, a.gameObject);
                canChangeVideo = false;
                RenderPassText();
            }
        }
    }

    public override void Interact()
    {
        GuideToggle();
    }

    public void Update()
    {
        float timenow = Time.time;
        if (IsPanelActive && updateReady)
        {
            if (InfoButton.maskable)
            {
                InfoButton.maskable = false;
                ShowInfoWindow();
            }

            if (HideInfoButton.maskable)
            {
                HideInfoButton.maskable = false;
                HideInfoWindow();
            }

            activeChannelButtons = ChannelHeaderRow.GetComponentsInChildren<Light>();
            for (int i = 0; i < activeChannelButtons.Length; i++)
            {
                if (activeChannelButtons[i].colorTemperature == 6969f)
                {
                    activeChannelButtons[i].colorTemperature = 0;
                    Transform pressedButton = activeChannelButtons[i].transform;
                    if (pressedButton.GetSiblingIndex() != 0)
                    {
                        if (canChangeVideo)
                        {
                            Debug.Log("valid user requested channel change");
                            currentTunedChannel = pressedButton.GetSiblingIndex()-1;
                            RequestSerialization();
                            ChangeChannel(currentTunedChannel);
                        }
                        else
                        {
                            Debug.Log("invalid user requested channel change");
                        }
                    }
                }
            }

            // write the button array for click detection
            activeGuideButtons = ScrollContent.GetComponentsInChildren<Light>();
            for (int i = 0; i < activeGuideButtons.Length; i++)
            {
                if (activeGuideButtons[i].colorTemperature == 6969f)
                {
                    activeGuideButtons[i].colorTemperature = 0;
                    Transform pressedButton = activeGuideButtons[i].transform;
                    if (pressedButton.parent.GetSiblingIndex() != 0) // make sure they didn't click the time row
                    {
                        lastUserInteractionTime = timenow;
                        float clickedEpisodeIndexi = activeGuideButtons[i].range;
                        float clickedEpisodeIndexj = activeGuideButtons[i].intensity;
                        UpdateGuideShowDisplay((int)clickedEpisodeIndexi, (int)clickedEpisodeIndexj);
                    }
                }
            }
            
            if (timenow - lastUserInteractionTime <= 120) // if the menu was interacted with recently...
            {
                if (timenow - lastRedrawTime >= 60f)
                {
                    PartialRenderGuide(); // then only partially refresh the guide if it's time
                    lastRedrawTime = timenow;
                }
            }
            else
            {
                if (timenow - lastRedrawTime >= 60f)
                {
                    GuideScrollbar.value = 0;
                    RenderGuide();
                    lastRedrawTime = timenow;
                }
            }

            if (timenow - lastMetadataRefreshTime >= 7200f) // 2 hour automatic refresh
            {
                MakeRequest();
                Debug.Log("automatic metadata refresh requested");
                lastMetadataRefreshTime = timenow;
            }

            if (scrollLocation != GuideScrollbar.value)
            {
                lastUserInteractionTime = timenow;
                scrollLocation = GuideScrollbar.value;
                RectTransform rt = ScrollContent.GetComponent<RectTransform>();
                float scrollAmount = Mathf.Lerp(0, maxScrollOffset, scrollLocation);
                TimeSpan scrollAmountMinutes = new TimeSpan(0, (int)(scrollAmount/renderGuideTimeScale), 0);
                DateTime scrollTime = DateTime.Now.Add(scrollAmountMinutes);
                if (DateTime.Compare(scrollTime, DateTime.Today.AddDays(1)) > 0)
                {
                    GuideChannelHeadersText[0].text = scrollTime.ToString("ddd d");
                }
                else
                {
                    GuideChannelHeadersText[0].text = "Today";
                }

                rt.offsetMin = new Vector2(scrollAmount*-1, rt.offsetMin.y);
            }

            if (isLoadingVideo)
            {
                float loadingtimediff = timenow - loadingTime;
                loadingStage = (int)Mathf.Floor(loadingtimediff % 4);
                char[] ellipsis = new char[3] { (char)8203, (char)8203, (char)8203 };
                for (int i = 0; i < loadingStage; i++)
                {
                    ellipsis[i] = '.';
                }
                LoadingText.text = "Please wait" + new string(ellipsis);
                if (loadingtimediff >= 10f) // if we've been loading for 10 seconds straight
                {
                    LoadingIsTakingForeverText.gameObject.SetActive(true); // then turn the taking forever text on. bug: this will call it every frame until loading is over. oh well
                }
            }
        }
    }

    public override void OnDeserialization()
    {
        // we don't really need to know what the new data is, just that the channel/current show changed
        Debug.Log("received network vars");
        if (IsPanelActive)
        {
            PartialRenderGuide(); // just render the guide locally to stay up to date
        }
    }

    void ChangeChannel(int index)
    {
        videoPlayer.SetProgramVariable("IN_MAINURL", channelURLs[index]);
        videoPlayer.SendCustomEvent("_ChangeMedia");
        isPlayerIdle = false;
        RequestSerialization();
        a.SendCustomNetworkEvent(NetworkEventTarget.All, "StartLoadingVideo");
    }

    public void StartLoadingVideo()
    {
        if (IsPanelActive)
        {
            isLoadingVideo = true;
            loadingTime = Time.time;
            LoadingOverlay.gameObject.SetActive(true);
        }
    }

    void EndLoadingVideo()
    {
        isLoadingVideo = false;
        if (IsPanelActive)
        {
            LoadingOverlay.gameObject.SetActive(false);
            LoadingIsTakingForeverText.gameObject.SetActive(false);
            RenderGuide();
        }
    }

    public void ReceiveLeftPassInput()
    {
        if (currentPassSelectIndex!=0)
        {
            currentPassSelectIndex--;
        }
        else
        {
            currentPassSelectIndex = playersInWorld.Length-1;
        }
        RenderPassText();
    }

    public void ReceiveRightPassInput()
    {
        if (currentPassSelectIndex+1<playersInWorld.Length)
        {
            currentPassSelectIndex++;
        }
        else
        {
            currentPassSelectIndex = 0;
        }
        RenderPassText();
    }

    public void PassRemoteInput()
    {
        PassRemoteToPlayer(playersInWorld[currentPassSelectIndex]);
    }

    void RenderPassText()
    {
        Transform passRemoteUI = passRemoteView.transform.Find("PassRemoteUI");
        Transform remoteHolderInfo = passRemoteView.transform.Find("RemoteHolderInfo");
        if (canChangeVideo)
        {
            remoteHolderInfo.GetComponent<TextMeshProUGUI>().text = "<b>You have the remote.\nPass it to someone else to let them change the channel.</b>";
            passRemoteUI.gameObject.SetActive(true);
            passRemoteUI.Find("username").GetComponent<TextMeshProUGUI>().text = VRCPlayerApi.GetPlayerId(playersInWorld[currentPassSelectIndex]) + " " + playersInWorld[currentPassSelectIndex].displayName;
        }
        else
        {
            passRemoteUI.gameObject.SetActive(false);
            VRCPlayerApi remoteHolder = Networking.GetOwner(a.gameObject);
            remoteHolderInfo.GetComponent<TextMeshProUGUI>().text = "You do not have the remote.\n<b>" + remoteHolder.displayName + "</b>\nholds the remote.";
        }
    }

    void GuideToggle()
    {
        if (IsPanelActive)
        {
            GuideDestroy();
            passRemoteView.gameObject.SetActive(false);
            IsPanelActive = false;
        }
        else
        {
            GuideInstantiate();
            passRemoteView.gameObject.SetActive(true);
            RenderPassText();
            IsPanelActive = true;
        }
    }

    void GuideInstantiate()
    {
        updateReady = false;
        isLoadingVideo = false;
        ActiveGuidePanel = Instantiate(GuidePanelPrefab, GuideSpawnLocation);
        Debug.Log("populate references hit");
        PopulateObjectReferences();

        Debug.Log("last metadata refresh time: " + lastMetadataRefreshTime);
        if (lastMetadataRefreshTime == 0f || (Time.time - lastMetadataRefreshTime) >= 900f) // 15 minute manual guide refresh limit
        {
            Debug.Log("hit manual guide refresh");
            MakeRequest();
            lastMetadataRefreshTime = Time.time;
            // if this is the first time the guide has been opened, show the welcome text
            if (isPlayerIdle)
            {  
                Debug.Log("hit first show");
                CurrentChannelText.text = "No Channel Tuned";
                ShowTitleText.text = "...";
                ShowTimeText.text = "";
                ShowPlotText.text = "Nothing is playing right now.\nTo tune to a channel, click that channels name in the guide.\nTo view information about a programme, click on its entry.\nStuck here? Make sure \"Allow Untrusted URLs\" is enabled in your settings, then re-join this instance.";
                Image PreviewImageUI = PreviewImage.GetComponent<Image>();
                PreviewImageUI.material.SetTexture("_MainTex", loadedThumbImages);
                PreviewImageUI.material.SetColor("_Color", new Color(1, 1, 1, 0));
            }
        }
        else
        {
            RenderGuide();
            //PreviewImageRenderer.SetTexture(loadedThumbImages);
            Image PreviewImageUI = PreviewImage.GetComponent<Image>();
            PreviewImageUI.material.SetTexture("_MainTex", loadedThumbImages);
        }
    }

    void GuideDestroy()
    {
        Destroy(ActiveGuidePanel);
    }

    void JSONDataReadySignal()
    {
        ParseGuideInfoFromDataTokens();
        //DebugLogAllChannels();
        PartialRenderGuide();
    }

    public void MakeRequest()
    {
        VRC.SDK3.StringLoading.VRCStringDownloader.LoadUrl(_url, _udonEventReceiver);
        _imageDownloader.DownloadImage(guideImageURL, PreviewImageRenderer.GetMaterial(), _udonEventReceiver, rgbInfo);
    }

    void PopulateObjectReferences()
    {
        LoadingOverlay = ActiveGuidePanel.transform.Find("LoadingOverlay");
        InfoOverlay = ActiveGuidePanel.transform.Find("InfoOverlay");
        HideInfoButton = InfoOverlay.Find("Background").Find("Window").Find("ExitButton").Find("ExitText").GetComponent<TextMeshProUGUI>();
        LoadingText = LoadingOverlay.Find("Image").Find("LoadingText").GetComponent<TextMeshProUGUI>();
        LoadingIsTakingForeverText = LoadingOverlay.Find("Image").Find("LoadingIsTakingForeverText");
        
        CurrentChannelText = ActiveGuidePanel.transform.Find("GuideShowInfo").Find("GuideHeaderBar").Find("CurrentChannelText").GetComponent<TextMeshProUGUI>();
        CurrentTimeText = ActiveGuidePanel.transform.Find("GuideShowInfo").Find("GuideHeaderBar").Find("CurrentTimeText").GetComponent<TextMeshProUGUI>();

        ShowTitleText = ActiveGuidePanel.transform.Find("GuideShowInfo").Find("GuideBody").Find("ShowTitleText").GetComponent<TextMeshProUGUI>();
        ShowTimeText = ActiveGuidePanel.transform.Find("GuideShowInfo").Find("GuideBody").Find("ShowTimeText").GetComponent<TextMeshProUGUI>();
        ShowPlotText = ActiveGuidePanel.transform.Find("GuideShowInfo").Find("GuideBody").Find("ShowPlotText").GetComponent<TextMeshProUGUI>();

        PreviewImage = ActiveGuidePanel.transform.Find("GuideShowPreview").Find("PreviewImage").gameObject;
        PreviewImageRenderer = PreviewImage.GetComponent<CanvasRenderer>();
        
        GuideScrollbar = ActiveGuidePanel.transform.Find("Scrollbar").GetComponent<Slider>();
        InfoButton = ActiveGuidePanel.transform.Find("Scrollbar").Find("InfoButton").Find("Text (TMP)").GetComponent<TextMeshProUGUI>();
        ChannelHeaderRow = ActiveGuidePanel.transform.Find("ChannelHeaderRow");
        ScrollTransform = ActiveGuidePanel.transform.Find("Scroll View");
        ScrollContent = ScrollTransform.Find("Viewport").Find("Content");

        for (int i = 0; i < channelInfoArray.Length+1; i++) // spawn a channel header for each row, plus the time row
        {
            GuideChannelHeaders[i] = Instantiate(GuideChannelHeaderPrefab, ChannelHeaderRow).transform;
            GuideChannelHeadersText[i] = GuideChannelHeaders[i].Find("RowText").GetComponent<TextMeshProUGUI>();
            if (i == 0)
            {
                GuideChannelHeadersText[i].text = "Today";
                GuideChannelHeaders[i].gameObject.GetComponent<Image>().color = new Color(0, 0.137f, 0.239f, 1f);
                Destroy(GuideChannelHeaders[i].gameObject.GetComponent<Button>()); // no need for the button on these
                Destroy(GuideChannelHeaders[i].gameObject.GetComponent<Light>()); // no need for the light on these
            }
            else
            {
                GuideChannelHeadersText[i].text = channelInfoArray[i-1][1];
            }
        }

        for (int i = 0; i < channelInfoArray.Length+1; i++) // spawn a guide row for each channel, plus one for the time row
        {
            GuideRows[i] = Instantiate(GuideRowPrefab, ScrollContent).transform;
        }

        Debug.Log("Populate objects complete");
    }

    void ClearGuideRow(Transform row)
    {
        for (int i = 0; i < row.childCount; i++)
        {
            Destroy(row.GetChild(i).gameObject);
        }
    }

    public override void OnStringLoadSuccess(IVRCStringDownload result)
    {
        if (VRCJson.TryDeserializeFromJson(result.Result, out DataToken _lastParsedGuide))
        {
            if (_lastParsedGuide.TokenType == TokenType.DataList)
            {
                Debug.Log("parsed guide json as list");
            }
            else if (_lastParsedGuide.TokenType == TokenType.DataDictionary)
            {
                Debug.Log("...huh? parsed successfully but as wrong type.");
            }
        }
        else
        {
            Debug.Log("failed to parse json! big fuck!");
        }

        _channels = _lastParsedGuide.DataList.ToArray();
        JSONDataReadySignal();
    }

    public override void OnStringLoadError(IVRCStringDownload result)
    {
        Debug.LogError($"Error loading json: {result.ErrorCode} - {result.Error}");
    }

    public override void OnImageLoadSuccess(IVRCImageDownload result)
    {
        Debug.Log("image load success of " + result.SizeInMemoryBytes + " bytes");
        loadedThumbImages = result.Result;
        PreviewImageRenderer.SetTexture(loadedThumbImages);
        arePreviewsReady = true;
        if (!isPlayerIdle) {
            UpdateGuideShowDisplay(currentTunedChannel, currentlyPlayingMedia);
        }
    }

    public override void OnImageLoadError(IVRCImageDownload result)
    {
        Debug.Log($"Image not loaded: {result.Error.ToString()}: {result.ErrorMessage}.");
    }

    public void DebugLogAllChannels()
    {
        for (int i = 0; i < _availableChannels; i++)
        {
            Debug.Log("Channel Name: " + availableChannelsArray[i][0] + "\nChannel Short Code: " + availableChannelsArray[i][1]);
            for (int j = 0; j < availableEntriesArray[i].Length; j++)
            {
                Debug.Log(String.Format("Name: {0}\nStart Date: {1}\nEpisode: {2}\nEpisode Number: {3}", availableEntriesArray[i][j][0], availableEntriesArray[i][j][1].ToString(), availableEntriesArray[i][j][2], availableEntriesArray[i][j][3]));
            }
        }
    }

    void UpdateGuideHeader()
    {
        CurrentTimeText.text = DateTime.Now.ToString("h:mm tt");
        if (!isPlayerIdle)
        {
            CurrentChannelText.text = channelInfoArray[currentTunedChannel][2];
        }
    }

    void UpdateGuideShowDisplay(int i_index, int j_index)
    {
        object[] requestedEntry = availableEntriesArray[i_index][j_index];
        ShowTitleText.text = (string)requestedEntry[0];
        DateTime episodeTime = (DateTime)requestedEntry[1];
        DateTime nextEpisodeTime;
        if (j_index+1 == availableEntriesArray[i_index].Length) // if we're about to overrun the entries array by checking
        {
            nextEpisodeTime = DateTime.UtcNow.Add(new TimeSpan(1, 0, 0)); // then just say it's an hour long
        }
        else
        {
            nextEpisodeTime = (DateTime)availableEntriesArray[i_index][j_index+1][1];
        }
        
        ShowTimeText.text = episodeTime.ToLocalTime().ToString("h:mm tt") + " - " + nextEpisodeTime.ToLocalTime().ToString("h:mm tt");
        ShowPlotText.text = (string)requestedEntry[2] + " - " + (string)requestedEntry[3] + "\n" + (string)requestedEntry[4];
        if ((int)requestedEntry[5] != 0)
        {
            Vector2 offset = ConvertToXY((int)requestedEntry[5]);
            PreviewImageMaterial.SetTextureOffset("_MainTex", offset);
            if (arePreviewsReady)
            {
                PreviewImageMaterial.SetColor("_Color", new Color(1, 1, 1, 1));
            }
        }
        else
        {
            PreviewImageMaterial.SetColor("_Color", new Color(1, 1, 1, 0));
        }
    }

    public void PartialRenderGuide()
    {
        Debug.Log("partial guide render requested");
        UpdateGuideHeader();
        RenderGuideViewport();
    }

    public void RenderGuide()
    {
        Debug.Log("guide render requested");
        UpdateGuideHeader();
        RenderGuideViewport();
        UpdateGuideShowDisplay(currentTunedChannel, currentlyPlayingMedia);
    }

    public void RenderGuideViewport()
    {
        DateTime earliestFinalEntryEndTime = new DateTime(2200, 1, 1, 1, 1, 1);
        for (int i = 0; i < GuideRows.Length; i++)
        {
            ClearGuideRow(GuideRows[i]);
        }

        Transform lastSpawnedEntry;

        // populate the time row
        DateTime now = DateTime.Now;
        DateTime closestHalfHour = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute / 30 * 30, 0);
        //GuideRowText[0].text = "Today";

        // for the next 24h ahead of us (48 half-hour increments), create a time entry in the top row.
        lastSpawnedEntry = Instantiate(GuideEntryPrefab, GuideRows[0]).transform;
        Destroy(lastSpawnedEntry.GetComponent<Light>()); // no need for the light on these
        Destroy(lastSpawnedEntry.GetComponent<Button>()); // no need for the button on these
        lastSpawnedEntry.Find("GuideEntryText").GetComponent<TextMeshProUGUI>().text = now.ToString("h:mm tt");
        TimeSpan remainingTimeInCurrentBlock = new TimeSpan(0, 30, 0).Subtract(now.Subtract(closestHalfHour));
        Vector2 currentTimeSpanRectSize = new Vector2(remainingTimeInCurrentBlock.Minutes * renderGuideTimeScale, 36);
        lastSpawnedEntry.GetComponent<RectTransform>().sizeDelta = currentTimeSpanRectSize;
        lastSpawnedEntry.GetComponent<Image>().color = new Color(0.149f, 0.282f, 0.341f, 1f);

        // spawn the other 47 time entries
        for (int i = 1; i < 48; i++)
        {
            lastSpawnedEntry = Instantiate(GuideEntryPrefab, GuideRows[0]).transform;
            Destroy(lastSpawnedEntry.GetComponent<Light>()); // no need for the light on these
            Destroy(lastSpawnedEntry.GetComponent<Button>()); // no need for the button on these
            TextMeshProUGUI EntryText = lastSpawnedEntry.Find("GuideEntryText").GetComponent<TextMeshProUGUI>();
            EntryText.text = closestHalfHour.AddMinutes(i * 30).ToString("h:mm tt");
            lastSpawnedEntry.GetComponent<RectTransform>().sizeDelta = new Vector2(30*renderGuideTimeScale, 36);
            lastSpawnedEntry.GetComponent<Image>().color = new Color(0.149f, 0.282f, 0.341f, 1f);
        }

        // cool!~ now, time to populate our channel information.
        for (int i = 0; i < _availableChannels; i++)
        {
            //GuideRowText[i+1].text = channelInfoArray[i][1];
            for (int j = 0; j < availableEntriesArray[i].Length; j++)
            {
                DateTime episodeTime = (DateTime)availableEntriesArray[i][j][1];
                DateTime nextEpisodeTime;
                if (j == availableEntriesArray[i].Length-1)
                {
                    nextEpisodeTime = episodeTime;
                }
                else
                {
                    nextEpisodeTime = (DateTime)availableEntriesArray[i][j+1][1];
                }
                TimeSpan episodeTimeDiff;

                if (DateTime.Compare(episodeTime, now) <= 0 && DateTime.Compare(nextEpisodeTime, now) <= 0)
                {
                    // the current episode and the next episode are both in the past, so this episode is over.
                    // don't do anything-- this gets skipped.
                    Debug.Log("skipping episode in past, " + episodeTime.ToString("h:mm tt dd mm yy") + " and " + nextEpisodeTime.ToString("h:mm tt dd mm yy") + " vs now " + now.ToString("h:mm tt dd mm yy"));
                }
                else if (DateTime.Compare(episodeTime, now) <= 0 && DateTime.Compare(nextEpisodeTime, now) > 0)
                {
                    // the current episode is in the past but the next episode is in the future.
                    // that means this is the episode we're playing right now! trim its length accordingly
                    episodeTimeDiff = nextEpisodeTime.Subtract(now);
                    lastSpawnedEntry = Instantiate(GuideEntryPrefab, GuideRows[i+1]).transform;
                    lastSpawnedEntry.Find("GuideEntryText").GetComponent<TextMeshProUGUI>().text = "<size=10px>" + (string)availableEntriesArray[i][j][0];
                    lastSpawnedEntry.GetComponent<RectTransform>().sizeDelta = new Vector2((float)(episodeTimeDiff.TotalMinutes) * renderGuideTimeScale, 36);

                    if (i == currentTunedChannel)
                    {
                        currentlyPlayingMedia = (int)j;
                    }
                }
                else if (j == availableEntriesArray[i].Length-1)
                {
                    if (DateTime.Compare(episodeTime, earliestFinalEntryEndTime) < 0)
                    {
                        earliestFinalEntryEndTime = episodeTime;
                    }
                    // we're about to run out of next episode time so just say it's an hour long because fuck it
                    episodeTimeDiff = new TimeSpan(1, 0, 0); // fuck it
                    lastSpawnedEntry = Instantiate(GuideEntryPrefab, GuideRows[i+1]).transform;
                    lastSpawnedEntry.Find("GuideEntryText").GetComponent<TextMeshProUGUI>().text = "<size=10px>" + (string)availableEntriesArray[i][j][0];
                    lastSpawnedEntry.GetComponent<RectTransform>().sizeDelta = new Vector2((float)(episodeTimeDiff.TotalMinutes) * renderGuideTimeScale, 36);
                }
                else if (DateTime.Compare(episodeTime, now) > 0)
                {
                    // this episode is in the future, so calculate its length with its neighbour and no trim
                    episodeTimeDiff = nextEpisodeTime.Subtract(episodeTime);
                    lastSpawnedEntry = Instantiate(GuideEntryPrefab, GuideRows[i+1]).transform;
                    lastSpawnedEntry.Find("GuideEntryText").GetComponent<TextMeshProUGUI>().text = "<size=10px>" + (string)availableEntriesArray[i][j][0];
                    lastSpawnedEntry.GetComponent<RectTransform>().sizeDelta = new Vector2((float)(episodeTimeDiff.TotalMinutes) * renderGuideTimeScale, 36);
                    // debug shit here
                    /*
                    if (j == availableEntriesArray[i].Length-1)
                    {
                        Debug.Log((string)availableEntriesArray[i][j][0] + " " + episodeTime.ToString("h:mm tt dd mm yy") + "no next episode");
                    }
                    else
                    {
                        Debug.Log((string)availableEntriesArray[i][j][0] + " " + episodeTime.ToString("h:mm tt dd mm yy") + " to " + (string)availableEntriesArray[i][j+1][0] + " " + nextEpisodeTime.ToString("h:mm tt dd mm yy") + "\ncalculated runtime: " + episodeTimeDiff + "\npixel width: " + episodeTimeDiff.TotalMinutes * renderGuideTimeScale);
                    }
                    */
                }
                else
                {
                    Debug.Log("fucked if i know");
                }

                // we use these parameters to store the i and j component so that if we click this box later we can track down which episode it represents
                lastSpawnedEntry.GetComponent<Light>().range = (float)i;
                lastSpawnedEntry.GetComponent<Light>().intensity = (float)j;
            }
        }

        Debug.Log($"total scrollable timespan: {(earliestFinalEntryEndTime - now).TotalMinutes}");

        maxScrollOffset = (int)((earliestFinalEntryEndTime - now).TotalMinutes * renderGuideTimeScale) - (int)ScrollTransform.GetComponent<RectTransform>().sizeDelta.x;
        updateReady = true;
    }

    void ParseGuideInfoFromDataTokens()
    {
        availableChannelsArray = new String[channelInfoArray.Length][];
        availableEntriesArray = new object[6][][];
        _availableChannels = 0;

        // future me: jagged array format is as such:
        // FIRST LAYER INDEX IS THE CHANNEL
        // SECOND LAYER INDEX IS THE EPISODE
        // 0: show name, string
        // 1: episode start date, DateTime
        // 2: episode number, string
        // 3: episode name, string
        // 4: episode plot, string
        // 5: episode preview index, double

        for (int i = 0; i < _channels.Length; i++)
        {
            _channels[i].DataDictionary.TryGetValue("name", out DataToken channelName);
            if (_channels[i].DataDictionary.TryGetValue("media", out DataToken media))
            {
                int foundEpisodesNumber = 0;
                object[][] foundEpisodesBuffer = new object[128][];
                for (int j = 0; j < media.DataList.Count; j++)
                {
                    // try to parse the media entry.
                    if (media.DataList.TryGetValue(j, out DataToken currentMedia))
                    {
                        currentMedia.DataDictionary.TryGetValue("name", out DataToken showName);
                        currentMedia.DataDictionary.TryGetValue("startDate", out DataToken startDate);
                        DataToken episodeNumber;
                        if (!currentMedia.DataDictionary.TryGetValue("episodeNumber", out episodeNumber))
                        {
                            Debug.Log(currentMedia.Error);
                            episodeNumber = new DataToken("S??E??");
                        }
                        DataToken episodeName = new DataToken();
                        DataToken episodePlot  = new DataToken();
                        DataToken episodePreviewIndex  = new DataToken();
                        if (currentMedia.DataDictionary.TryGetValue("info", out DataToken episodeInfo))
                        {
                            if (!episodeInfo.DataDictionary.TryGetValue("episode", out episodeName))
                            {
                                Debug.Log(episodeInfo.Error);
                                episodeName = new DataToken("Episode Name Unavailable");
                            }
                            if (!episodeInfo.DataDictionary.TryGetValue("plot", out episodePlot))
                            {
                                Debug.Log(episodeInfo.Error);
                                episodePlot = new DataToken("No Plot Available");
                            }
                            if (!episodeInfo.DataDictionary.TryGetValue("image", out episodePreviewIndex))
                            {
                                Debug.Log(episodeInfo.Error);
                                episodePreviewIndex  = new DataToken(0);
                            }
                        }

                        foundEpisodesBuffer[foundEpisodesNumber] = new object[6] {showName.String, DateTime.Parse(startDate.String), episodeNumber.String, episodeName.String, episodePlot.String, episodePreviewIndex.Double};
                        foundEpisodesNumber++;
                    } else {
                        Debug.Log("failed to parse media entry-- skipping.\nerror: " + currentMedia.Error);
                    }
                }
                Debug.Log(channelName.String);
                availableChannelsArray[_availableChannels] = new String[1] {channelName.String};
                
                object[][] foundEpisodesBufferFinalized = new object[foundEpisodesNumber][];
                Array.Copy(foundEpisodesBuffer, foundEpisodesBufferFinalized, foundEpisodesNumber);
                availableEntriesArray[_availableChannels] = foundEpisodesBufferFinalized;

                _availableChannels++;
            } else {
                Debug.Log("failed to parse channel entry-- skipping.\nerror: " + media.Error);
            }
        }
    }

    public Vector2 ConvertToXY(int position)
    {
        int row = (int)Math.Floor(((double)position-1)/8);
        int col = (position-1) % 8;

        float x = (float)(col*0.125);
        float y = (float)(1-((row+1)*0.125));

        return new Vector2(x, y);
    }

    void ShowInfoWindow()
    {
        InfoOverlay.gameObject.SetActive(true);
    }

    void HideInfoWindow()
    {
        InfoOverlay.gameObject.SetActive(false);
    }
}
