#define COMMUNITY_LABS_SDK
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Resources;
using VRC.Core;
using System;
using System.IO;
using VRC.SDK3.Image;
#if UNITY_EDITOR
using UnityEditor;
using VRC.SDKBase;
#endif

namespace VRCSDK2
{
#if UNITY_EDITOR
    [Obsolete("Runtime uploads are deprecated. Use methods provided by the VRC.SDKBase.Editor.Api.VRCApi class for uploads")]
    public class RuntimeWorldCreation : RuntimeAPICreation
    {
        public VRC_SceneDescriptor descriptor;
        
        public GameObject waitingPanel;
        public GameObject blueprintPanel;
        public GameObject errorPanel;

        public Text titleText;
        public InputField blueprintName;
        public InputField blueprintDescription;
        public InputField worldCapacity;
        public RawImage bpImage;
        public Image liveBpImage;
        public Toggle shouldUpdateImageToggle;
        public Toggle releasePublic;
        public Toggle contentNsfw;

        public Toggle contentSex;
        public Toggle contentViolence;
        public Toggle contentGore;
        public Toggle contentOther;

        public Toggle contentFeatured;
        public Toggle contentSDKExample;

        public Image showInWorldsMenuGroup;
        public Toggle showInActiveWorlds;
        public Toggle showInPopularWorlds;
        public Toggle showInNewWorlds;

        public InputField userTags;

        public UnityEngine.UI.Button uploadButton;

        public UnityEngine.UI.Button openCommunityLabsDocsButton;

        public GameObject publishToCommunityLabsPanel;

        private Toggle publishToCommLabsToggle;

        private ApiWorld worldRecord;

        private const int MAX_USER_TAGS_FOR_WORLD = 5;
        private const int MAX_CHARACTERS_ALLOWED_IN_USER_TAG = 20;
        List<String> customTags;

        public static bool IsCurrentWorldInCommunityLabs = false;
        public static bool IsCurrentWorldUploaded = false;
        public static bool IsCurrentWorldPubliclyPublished = false;
        public static bool HasExceededPublishLimit = false;

        new void Start()
        {
            if (!Application.isEditor || !Application.isPlaying)
                return;

            base.Start();

            IsCurrentWorldInCommunityLabs = false;
            IsCurrentWorldUploaded = false;
            IsCurrentWorldPubliclyPublished = false;


            descriptor = pipelineManager.GetComponent<VRC.SDKBase.VRC_SceneDescriptor>();
            descriptor.PositionPortraitCamera(imageCapture.shotCamera.transform);

            Application.runInBackground = true;
            UnityEngine.XR.XRSettings.enabled = false;

            uploadButton.onClick.AddListener(SetupUpload);

            openCommunityLabsDocsButton.onClick.AddListener(OpenCommunityLabsDocumentation);

            shouldUpdateImageToggle.onValueChanged.AddListener(ToggleUpdateImage);

            releasePublic.gameObject.SetActive(false);

            System.Action<string> onError = (err) => {
                VRC.Core.Logger.LogError("Could not authenticate - " + err);
                blueprintPanel.SetActive(false);
                errorPanel.SetActive(true);
            };

            if (!ApiCredentials.Load())
                onError("Not logged in");
            else
                APIUser.InitialFetchCurrentUser(
                    delegate (ApiModelContainer<APIUser> c)
                    {
                        UserLoggedInCallback(c.Model as APIUser);
                    },
                    delegate (ApiModelContainer<APIUser> c)
                    {
                        onError(c.Error);
                    }
                );

#if !COMMUNITY_LABS_SDK
            publishToCommunityLabsPanel.gameObject.SetActive(false);
#endif
        }

        void UserLoggedInCallback(APIUser user)
        {
            pipelineManager.user = user;

            ApiWorld model = new ApiWorld();
            model.id = pipelineManager.blueprintId;
            model.Fetch(null,
                (c) =>
                {
                    VRC.Core.Logger.Log("<color=magenta>Updating an existing world.</color>", API.LOG_CATEGORY);
                    worldRecord = c.Model as ApiWorld;
                    pipelineManager.completedSDKPipeline = !string.IsNullOrEmpty(worldRecord.authorId);
                    GetUserUploadInformationAndSetupUI(model.id);
                },
                (c) =>
                {
                    VRC.Core.Logger.Log("<color=magenta>World record not found, creating a new world.</color>", API.LOG_CATEGORY);
                    worldRecord = new ApiWorld { capacity = 16 };
                    pipelineManager.completedSDKPipeline = false;
                    worldRecord.id = pipelineManager.blueprintId;
                    GetUserUploadInformationAndSetupUI(model.id);
                });
        }

        void CheckWorldStatus(string worldId, Action onCheckComplete)
        {
            // check if world has been previously uploaded, and if world is in community labs
            ApiWorld.FetchUploadedWorlds(
                delegate (IEnumerable<ApiWorld> worlds)
                {
                    ApiWorld selectedWorld = worlds.FirstOrDefault(w => w.id == worldId);
                    if (null!=selectedWorld)
                    {
                        IsCurrentWorldInCommunityLabs = selectedWorld.IsCommunityLabsWorld;
                        IsCurrentWorldPubliclyPublished = selectedWorld.IsPublicPublishedWorld;
                        IsCurrentWorldUploaded = true;
                    }
                    if (onCheckComplete != null) onCheckComplete();

                },
                delegate (string err)
                {
                    IsCurrentWorldInCommunityLabs = false;
                    IsCurrentWorldUploaded = false;
                    IsCurrentWorldPubliclyPublished = false;
                    Debug.Log("CheckWorldStatus error:" + err);
                    if (onCheckComplete != null) onCheckComplete();
                }
                );
        }


        void GetUserUploadInformationAndSetupUI(string worldId)
        {
            CheckWorldStatus(worldId, delegate()
                {
                    bool hasSufficientTrustLevelToPublishToCommunityLabs = APIUser.CurrentUser.hasKnownTrustLevel;
                    APIUser.FetchPublishWorldsInformation(
                        (c) =>
                        {
                            try
                            {
                                if (c["canPublish"].Type == BestHTTP.JSON.Json.TokenType.Boolean)
                                {
                                    HasExceededPublishLimit = !(bool)(c["canPublish"]);
                                }
                                else
                                    HasExceededPublishLimit = true;
                            }
                            catch (Exception)
                            {
                                HasExceededPublishLimit = true;
                            }

                            if(Application.isPlaying)
                            {
                                SetupUI(hasSufficientTrustLevelToPublishToCommunityLabs, HasExceededPublishLimit);
                            }
                        },
                        (c) =>
                        {
                            if(Application.isPlaying)
                            {
                                SetupUI(hasSufficientTrustLevelToPublishToCommunityLabs, HasExceededPublishLimit);
                            }
                        }
                    );
                }
            );
        }

        void SetupUI(bool hasEnoughTrustToPublishToCL = false, bool hasExceededWeeklyPublishLimit = false)
        {
#if COMMUNITY_LABS_SDK
            // do not display community labs panel if updating an existing CL world or updating a public world
            publishToCommunityLabsPanel.gameObject.SetActive(!IsCurrentWorldUploaded);
#endif

            if (!ValidateAssetBundleBlueprintID(worldRecord.id))
            {
                blueprintPanel.SetActive(false);
                errorPanel.SetActive(true);
                OnSDKPipelineError("The asset bundle is out of date.  Please rebuild the scene using 'New Build'.", "The blueprint ID in the scene does not match the id in the asset bundle.");
                return;
            }

            contentFeatured.gameObject.SetActive(APIUser.CurrentUser.hasSuperPowers);
            contentSDKExample.gameObject.SetActive(APIUser.CurrentUser.hasSuperPowers);

            if (APIUser.Exists(pipelineManager.user))
            {
                waitingPanel.SetActive(false);
                blueprintPanel.SetActive(true);
                errorPanel.SetActive(false);

                if (string.IsNullOrEmpty(worldRecord.authorId) || worldRecord.authorId == pipelineManager.user.id)
                {
                    titleText.text = "Configure World";
                    blueprintName.text = worldRecord.name;
                    worldCapacity.text = worldRecord.capacity.ToString();
                    contentSex.isOn = worldRecord.tags.Contains("content_sex");
                    contentViolence.isOn = worldRecord.tags.Contains("content_violence");
                    contentGore.isOn = worldRecord.tags.Contains("content_gore");
                    contentOther.isOn = worldRecord.tags.Contains("content_other");
                    shouldUpdateImageToggle.interactable = isUpdate;
                    shouldUpdateImageToggle.isOn = !isUpdate;
                    liveBpImage.enabled = !isUpdate;
                    bpImage.enabled = isUpdate;

                    if (!APIUser.CurrentUser.hasSuperPowers)
                    {
                        releasePublic.gameObject.SetActive(false);
                        releasePublic.isOn = false;
                        releasePublic.interactable = false;

                        contentFeatured.isOn = contentSDKExample.isOn = false;
                    }
                    else
                    {
                        contentFeatured.isOn = worldRecord.tags.Contains("content_featured");
                        contentSDKExample.isOn = worldRecord.tags.Contains("content_sdk_example");

                        releasePublic.isOn = worldRecord.releaseStatus == "public";
                        releasePublic.gameObject.SetActive(true);
                    }

                    // "show in worlds menu"
                    if (APIUser.CurrentUser.hasSuperPowers)
                    {
                        showInWorldsMenuGroup.gameObject.SetActive(true);
                        showInActiveWorlds.isOn = !worldRecord.tags.Contains("admin_hide_active");
                        showInPopularWorlds.isOn = !worldRecord.tags.Contains("admin_hide_popular");
                        showInNewWorlds.isOn = !worldRecord.tags.Contains("admin_hide_new");
                    }
                    else
                    {
                        showInWorldsMenuGroup.gameObject.SetActive(false);
                    }

                    blueprintDescription.text = worldRecord.description;

                    userTags.text = "";
                    foreach (var tag in worldRecord.publicTags)
                    {
                        userTags.text = userTags.text + tag.Replace("author_tag_", "");
                        userTags.text = userTags.text + " ";
                    }

                    ImageDownloader.DownloadImage(worldRecord.imageUrl, 0, obj => bpImage.texture = obj, null);
                }
                else // user does not own world id associated with descriptor
                {
                    Debug.LogErrorFormat("{0} is not an owner of {1}", worldRecord.authorId, pipelineManager.user.id);
                    blueprintPanel.SetActive(false);
                    errorPanel.SetActive(true);
                }
            }
            else
            {
                waitingPanel.SetActive(true);
                blueprintPanel.SetActive(false);
                errorPanel.SetActive(false);

                if (!APIUser.CurrentUser.hasSuperPowers)
                {
                    releasePublic.gameObject.SetActive(false);
                    releasePublic.isOn = false;
                    releasePublic.interactable = false;
                }
                else
                {
                    releasePublic.gameObject.SetActive(true);
                    releasePublic.isOn = false;
                }
            }

            // set up publish to Community Labs checkbox and text
            int worldsPublishedThisWeek = hasExceededWeeklyPublishLimit ? 1 : 0;
            int maximumWorldsAllowedToPublishPerWeek = 1;
            publishToCommLabsToggle = publishToCommunityLabsPanel.GetComponentInChildren<Toggle>();

            if (null != publishToCommLabsToggle)
            {
                // disable publishing to CL checkbox if not enough trust or exceeded publish limit 
                publishToCommLabsToggle.interactable = hasEnoughTrustToPublishToCL && !hasExceededWeeklyPublishLimit;

                Text publishText = publishToCommLabsToggle.gameObject.GetComponentInChildren<Text>();
                if (null != publishText)
                {
                    if (!hasEnoughTrustToPublishToCL)
                    {
                        publishText.text = "Not enough Trust to Publish to Community Labs";
                    }
                    else
                    {
                        if (hasExceededWeeklyPublishLimit)
                        {
                            publishText.text = "Publish limit for Community Labs Exceeded\n" + "(" + worldsPublishedThisWeek + "/" + maximumWorldsAllowedToPublishPerWeek + " Published this week)";
                        }
                        else
                        {
                            publishText.text = "Publish to Community Labs\n" + "(" + worldsPublishedThisWeek + "/" + maximumWorldsAllowedToPublishPerWeek + " Published this week)";
                        }
                    }
                }
            }
        }

        public void SetupUpload()
        {
            if (!ParseUserTags())
                return;

            publishingToCommunityLabs = (publishToCommLabsToggle != null) && (publishToCommLabsToggle.isActiveAndEnabled) && (publishToCommLabsToggle.isOn);

            uploadTitle = "Preparing For Upload";
            isUploading = true;

            string abPath = UnityEditor.EditorPrefs.GetString("currentBuildingAssetBundlePath");


            string unityPackagePath = UnityEditor.EditorPrefs.GetString("VRC_exportedUnityPackagePath");

            UnityEditor.EditorPrefs.SetBool("VRCSDK2_scene_changed", true);
            UnityEditor.EditorPrefs.SetInt("VRCSDK2_capacity", System.Convert.ToInt16(worldCapacity.text));
            UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_sex", contentSex.isOn);
            UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_violence", contentViolence.isOn);
            UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_gore", contentGore.isOn);
            UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_other", contentOther.isOn);
            UnityEditor.EditorPrefs.SetBool("VRCSDK2_release_public", releasePublic.isOn);
            UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_featured", contentFeatured.isOn);
            UnityEditor.EditorPrefs.SetBool("VRCSDK2_content_sdk_example", contentSDKExample.isOn);

            if (string.IsNullOrEmpty(worldRecord.id))
            {
                pipelineManager.AssignId(PipelineManager.ContentType.world);
                worldRecord.id = pipelineManager.blueprintId;
            }

            string blueprintId = worldRecord.id;
            int version = Mathf.Max(1, worldRecord.version + 1);
            PrepareVRCPathForS3(abPath, blueprintId, version, ApiWorld.VERSION);

            if (!string.IsNullOrEmpty(unityPackagePath) && System.IO.File.Exists(unityPackagePath))
            {
                VRC.Core.Logger.Log("Found unity package path. Preparing to upload!", API.LOG_CATEGORY);
                PrepareUnityPackageForS3(unityPackagePath, blueprintId, version, ApiWorld.VERSION);
            }

            StartCoroutine(UploadNew());
        }

        void OnUploadedWorld()
        {
            const string devUrl = "https://dev-api.vrchat.cloud";
            const string releaseUrl = "https://vrchat.com";
            
            string uploadedWorldURL = (API.IsDevApi() ? devUrl : releaseUrl) + "/home/world/" + pipelineManager.blueprintId;
            OnSDKPipelineComplete(uploadedWorldURL);
        }

        IEnumerator UploadNew()
        {
            bool caughtInvalidInput = false;
            if (!ValidateNameInput(blueprintName))
                caughtInvalidInput = true;

            if (caughtInvalidInput)
                yield break;

            VRC.Core.Logger.Log("Starting upload");

            // upload unity package
            if (!string.IsNullOrEmpty(uploadUnityPackagePath))
            {
                yield return StartCoroutine(UploadFile(uploadUnityPackagePath, isUpdate ? worldRecord.unityPackageUrl : "", GetFriendlyWorldFileName("Unity package"), "Unity package",
                    delegate (string fileUrl)
                    {
                        cloudFrontUnityPackageUrl = fileUrl;
                    }
                ));
            }

            // upload asset bundle
            if (!string.IsNullOrEmpty(uploadVrcPath))
            {
                yield return StartCoroutine(UploadFile(uploadVrcPath, isUpdate ? worldRecord.assetUrl : "", GetFriendlyWorldFileName("Asset bundle"), "Asset bundle",
                    delegate (string fileUrl)
                    {
                        cloudFrontAssetUrl = fileUrl;
                    }
                ));
            }

            if (isUpdate)
                yield return StartCoroutine(UpdateBlueprint());
            else
                yield return StartCoroutine(CreateBlueprint());

            if (publishingToCommunityLabs)
            {
                ApiWorld.PublishWorldToCommunityLabs(pipelineManager.blueprintId,
                    (world) => OnUploadedWorld(),
                    (err) =>
                    {
                        Debug.LogError("PublishWorldToCommunityLabs error:" + err);
                        OnUploadedWorld();
                    }
                );
            }
            else
            {
                OnUploadedWorld();
            }
        }

        private string GetFriendlyWorldFileName(string type)
        {
            return "World - " + blueprintName.text + " - " + type + " - " + Application.unityVersion + "_" + ApiWorld.VERSION.ApiVersion +
                   "_" + VRC.Tools.Platform + "_" + API.GetServerEnvironmentForApiUrl();
        }

        List<string> BuildTags()
        {
            var tags = new List<string>();
            if (contentSex.isOn)
                tags.Add("content_sex");
            if (contentViolence.isOn)
                tags.Add("content_violence");
            if (contentGore.isOn)
                tags.Add("content_gore");
            if (contentOther.isOn)
                tags.Add("content_other");

            if (APIUser.CurrentUser.hasSuperPowers)
            {
                if (contentFeatured.isOn)
                    tags.Add("content_featured");
                if (contentSDKExample.isOn)
                    tags.Add("content_sdk_example");
                if(releasePublic.isOn)
                    tags.Add("admin_approved");
            }

            // "show in worlds menu"
            if (APIUser.CurrentUser.hasSuperPowers)
            {
                if (!showInActiveWorlds.isOn)
                    tags.Add("admin_hide_active");
                if (!showInPopularWorlds.isOn)
                    tags.Add("admin_hide_popular");
                if (!showInNewWorlds.isOn)
                    tags.Add("admin_hide_new");
            }

            // add any author tags
            foreach (var word in customTags)
            {
                // add all custom tags with "author_tag_" prefix
                tags.Add("author_tag_" + word);
            }

            return tags;
        }

        bool ParseUserTags()
        {
            bool validTags = true;
            customTags = new List<string>();
            char[] delimiterChars = { ' ', ',', '.', ':', '\t', '\n', '"', '#' };

            // split user tags into individual words
            string[] words = userTags.text.Split(delimiterChars, StringSplitOptions.RemoveEmptyEntries);

            foreach (var word in words)
            {
                customTags.Add(word.ToLower());
            }

            // check that number of tags is within tag limit
            if (words.Count() > MAX_USER_TAGS_FOR_WORLD)
            {
                validTags = false;
                UnityEditor.EditorUtility.DisplayDialog("Tags are limited to a maximum of " + MAX_USER_TAGS_FOR_WORLD + " per world.", "Please remove excess tags before uploading!", "OK");
            }
            else
            {
                // check that no tags exceed maximum tag length
                int maximumTagLength = 0;
                foreach (string item in words)
                {
                    if (item.Length > maximumTagLength)
                    {
                        maximumTagLength = item.Length;
                    }
                }

                if (maximumTagLength > MAX_CHARACTERS_ALLOWED_IN_USER_TAG)
                {
                    validTags = false;
                    UnityEditor.EditorUtility.DisplayDialog("Tags are limited to a maximum of " + MAX_CHARACTERS_ALLOWED_IN_USER_TAG + " characters per tag.", "One or more of your tags exceeds the maximum " + MAX_CHARACTERS_ALLOWED_IN_USER_TAG + " character limit.\n\n" + "Please shorten tags before uploading!", "OK");
                }
                else
                {
                    // make sure tags are all alphanumeric
                    foreach (var word in words)
                    {
                        if (!word.All(char.IsLetterOrDigit))
                        {
                            validTags = false;
                            UnityEditor.EditorUtility.DisplayDialog("Tags should consist of alphanumeric characters only.", "Please remove any non-alphanumeric characters from tags before uploading!", "OK");
                        }
                    }
                }
            }

            return validTags;
        }

        protected override IEnumerator CreateBlueprint()
        {
            yield return StartCoroutine(UpdateImage(isUpdate ? worldRecord.imageUrl : "", GetFriendlyWorldFileName("Image")));

            SetUploadProgress("Saving Blueprint to user", "Almost finished!!", 0.0f);
            ApiWorld world = new ApiWorld
            {
                id = worldRecord.id,
                authorName = pipelineManager.user.displayName,
                authorId = pipelineManager.user.id,
                name = blueprintName.text,
                imageUrl = cloudFrontImageUrl,
                assetUrl = cloudFrontAssetUrl,
                unityPackageUrl = cloudFrontUnityPackageUrl,
                description = blueprintDescription.text,
                tags = BuildTags(),
                releaseStatus = (releasePublic.isOn) ? ("public") : ("private"),
                capacity = System.Convert.ToInt16(worldCapacity.text),
                occupants = 0,
                shouldAddToAuthor = true,
                udonProducts = descriptor.udonProducts
            };

            if (APIUser.CurrentUser.hasSuperPowers)
                world.isCurated = contentFeatured.isOn || contentSDKExample.isOn;
            else
                world.isCurated = false;

            bool doneUploading = false;
            world.Post(
                (c) =>
                {
                    ApiWorld savedBP = (ApiWorld)c.Model;
                    pipelineManager.blueprintId = savedBP.id;
                    UnityEditor.EditorPrefs.SetString("blueprintID-" + pipelineManager.GetInstanceID().ToString(), savedBP.id);
                    VRC.Core.Logger.Log("Setting blueprintID on pipeline manager and editor prefs", API.LOG_CATEGORY);
                    doneUploading = true;
                },
                (c) => { doneUploading = true; Debug.LogError(c.Error); });

            while (!doneUploading)
                yield return null;
        }

        protected override IEnumerator UpdateBlueprint()
        {
            bool doneUploading = false;

            worldRecord.name = blueprintName.text;
            worldRecord.description = blueprintDescription.text;
            worldRecord.capacity = System.Convert.ToInt16(worldCapacity.text);
            worldRecord.assetUrl = cloudFrontAssetUrl;
            worldRecord.tags = BuildTags();
            worldRecord.releaseStatus = (releasePublic.isOn) ? ("public") : ("private");
            worldRecord.unityPackageUrl = cloudFrontUnityPackageUrl;
            worldRecord.isCurated = contentFeatured.isOn || contentSDKExample.isOn;
            worldRecord.udonProducts = descriptor.udonProducts;

            if (shouldUpdateImageToggle.isOn)
            {
                yield return StartCoroutine(UpdateImage(isUpdate ? worldRecord.imageUrl : "", GetFriendlyWorldFileName("Image")));

                worldRecord.imageUrl = cloudFrontImageUrl;
            }

            SetUploadProgress("Saving Blueprint", "Almost finished!!", 0.0f);
            worldRecord.Save((c) => doneUploading = true, (c) => { doneUploading = true; Debug.LogError(c.Error); });

            while (!doneUploading)
                yield return null;
        }

        void ToggleUpdateImage(bool isOn)
        {
            if (isOn)
            {
                bpImage.enabled = false;
                liveBpImage.enabled = true;
            }
            else
            {
                bpImage.enabled = true;
                liveBpImage.enabled = false;
                ImageDownloader.DownloadImage(worldRecord.imageUrl, 0, obj => bpImage.texture = obj, null);
            }
        }

        protected override void DisplayUpdateCompletedDialog(string contentUrl = null)
        {
        }

        private void OpenCommunityLabsDocumentation()
        {
            Application.OpenURL(CommunityLabsConstants.COMMUNITY_LABS_DOCUMENTATION_URL);
        }

        void OnDestroy()
        {
            UnityEditor.EditorUtility.ClearProgressBar();
            UnityEditor.EditorPrefs.DeleteKey("currentBuildingAssetBundlePath");
        }
    }
#endif
}


