﻿//
// Copyright 2017 Valve Corporation. All rights reserved. Subject to the following license:
// https://valvesoftware.github.io/steam-audio/license.html
//

using AOT;
using System;
using System.Runtime.InteropServices;
using System.Threading;
using UnityEngine;
using UnityEngine.SceneManagement;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif

namespace SteamAudio
{
    public enum BakeStatus
    {
        Ready,
        InProgress,
        Complete
    }

    public struct BakedDataTask
    {
        public GameObject gameObject;
        public MonoBehaviour component;
        public string name;
        public BakedDataIdentifier identifier;
        public SteamAudioProbeBatch[] probeBatches;
        public string[] probeBatchNames;
        public SerializedData[] probeBatchAssets;
    }

    public static class Baker
    {
        static BakeStatus sStatus = BakeStatus.Ready;
#if UNITY_EDITOR
        static float sProgress = 0.0f;
#endif
        static ProgressCallback sProgressCallback = null;
        static IntPtr sProgressCallbackPointer = IntPtr.Zero;
        static GCHandle sProgressCallbackHandle;
        static Thread sThread;
        static int sCurrentObjectIndex = 0;
        static int sTotalObjects = 0;
        static string sCurrentObjectName = null;
        static int sCurrentProbeBatchIndex = 0;
        static int sTotalProbeBatches = 0;
        static bool sCancel = false;
        static BakedDataTask[] sTasks = null;

        public static void BeginBake(BakedDataTask[] tasks)
        {
            SteamAudioManager.Initialize(ManagerInitReason.Baking);
            SteamAudioManager.LoadScene(SceneManager.GetActiveScene(), SteamAudioManager.Context, false);

            SteamAudioStaticMesh staticMeshComponent = null;
            var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects();
            foreach (var rootObject in rootObjects)
            {
                staticMeshComponent = rootObject.GetComponent<SteamAudioStaticMesh>();
                if (staticMeshComponent)
                    break;
            }

            if (staticMeshComponent == null || staticMeshComponent.asset == null)
            {
                Debug.LogError(string.Format("Scene {0} has not been exported. Click Steam Audio > Export Active Scene to do so.", SceneManager.GetActiveScene().name));
                return;
            }

            var staticMesh = new StaticMesh(SteamAudioManager.Context, SteamAudioManager.CurrentScene, staticMeshComponent.asset);
            staticMesh.AddToScene(SteamAudioManager.CurrentScene);

            SteamAudioManager.CurrentScene.Commit();

            sTasks = tasks;
            sStatus = BakeStatus.InProgress;

            sProgressCallback = new ProgressCallback(AdvanceProgress);

#if (UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN)
            sProgressCallbackPointer = Marshal.GetFunctionPointerForDelegate(sProgressCallback);
            sProgressCallbackHandle = GCHandle.Alloc(sProgressCallbackPointer);
            GC.Collect();
#endif

#if UNITY_EDITOR
            EditorApplication.update += InEditorUpdate;
#endif

            sThread = new Thread(BakeThread);
            sThread.Start();
        }

        public static void EndBake()
        {
            if (sThread != null)
            {
                sThread.Join();
            }

            SerializedObject.FlushAllWrites();

#if (UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN)
            if (sProgressCallbackHandle.IsAllocated)
            {
                sProgressCallbackHandle.Free();
            }
#endif

            SteamAudioManager.ShutDown();
            UnityEngine.Object.DestroyImmediate(SteamAudioManager.Singleton.gameObject);

#if UNITY_EDITOR
            sProgress = 0.0f;
#endif
            sCurrentObjectIndex = 0;
            sTotalObjects = 0;
            sCurrentObjectName = null;
            sCurrentProbeBatchIndex = 0;
            sTotalProbeBatches = 0;

            sStatus = BakeStatus.Ready;

#if UNITY_EDITOR
            EditorApplication.update -= InEditorUpdate;
#endif
        }

        public static bool IsBakeActive()
        {
            return (sStatus != BakeStatus.Ready);
        }

        public static bool DrawProgressBar()
        {
#if UNITY_EDITOR
            if (sStatus != BakeStatus.InProgress)
                return false;

            var progress = sProgress + .01f; // Adding an offset because progress bar when it is exact 0 has some non-zero progress.
            var progressPercent = Mathf.FloorToInt(Mathf.Min(progress * 100.0f, 100.0f));
            var progressString = string.Format("Object {0} / {1} [{2}]: Baking {3} / {4} Probe Batch ({5}% complete)",
                sCurrentObjectIndex + 1, sTotalObjects, sCurrentObjectName, sCurrentProbeBatchIndex + 1, sTotalProbeBatches, progressPercent);

            EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(), progress, progressString);

            if (GUILayout.Button("Cancel"))
            {
                CancelBake();
                return false;
            }
#endif
            return true;
        }

        public static void CancelBake()
        {
            // Ensures partial baked data is not serialized and that bake is properly canceled for multiple 
            // probe boxes.
            sCancel = true;
            API.iplReflectionsBakerCancelBake(SteamAudioManager.Context.Get());
            EndBake();
            sCancel = false;
        }

        [MonoPInvokeCallback(typeof(ProgressCallback))]
        static void AdvanceProgress(float progress, IntPtr userData)
        {
#if UNITY_EDITOR
            sProgress = progress;
#endif
        }

        static void InEditorUpdate()
        {
#if UNITY_EDITOR
            if (sStatus == BakeStatus.Complete)
            {
                EndBake();
                EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
            }
#endif
        }

        static void BakeThread()
        {
            sTotalObjects = sTasks.Length;

            for (var i = 0; i < sTotalObjects; ++i)
            {
                sCurrentObjectIndex = i;

                if (sTasks[i].identifier.type == BakedDataType.Pathing)
                {
                    sCurrentObjectName = "pathing";
                }
                else if (sTasks[i].identifier.variation == BakedDataVariation.Reverb)
                {
                    sCurrentObjectName = "reverb";
                }
                else
                {
                    sCurrentObjectName = sTasks[i].name;
                }

                Debug.Log(string.Format("START: Baking effect for {0}.", sCurrentObjectName));

                var probeBatches = sTasks[i].probeBatches;
                sTotalProbeBatches = probeBatches.Length;

                for (var j = 0; j < sTotalProbeBatches; ++j)
                {
                    sCurrentProbeBatchIndex = j;

                    if (sCancel)
                        return;

                    if (probeBatches[j] == null)
                    {
                        Debug.LogWarning(string.Format("{0}: Probe Batch at index {1} is null.", sCurrentObjectName, j));
                        continue;
                    }

                    if (probeBatches[j].GetNumProbes() == 0)
                    {
                        Debug.LogWarning(string.Format("{0}: Probe Batch {1} has no probes, skipping.", sCurrentObjectName, sTasks[i].probeBatchNames[j]));
                        continue;
                    }

                    var probeBatch = new ProbeBatch(SteamAudioManager.Context, sTasks[i].probeBatchAssets[j]);

                    var simulationSettings = SteamAudioManager.GetSimulationSettings(true);

                    if (sTasks[i].identifier.type == BakedDataType.Reflections)
                    {
                        var bakeParams = new ReflectionsBakeParams { };
                        bakeParams.scene = SteamAudioManager.CurrentScene.Get();
                        bakeParams.probeBatch = probeBatch.Get();
                        bakeParams.sceneType = simulationSettings.sceneType;
                        bakeParams.identifier = sTasks[i].identifier;
                        bakeParams.flags = 0;
                        bakeParams.numRays = simulationSettings.maxNumRays;
                        bakeParams.numDiffuseSamples = simulationSettings.numDiffuseSamples;
                        bakeParams.numBounces = SteamAudioSettings.Singleton.bakingBounces;
                        bakeParams.simulatedDuration = simulationSettings.maxDuration;
                        bakeParams.savedDuration = simulationSettings.maxDuration;
                        bakeParams.order = simulationSettings.maxOrder;
                        bakeParams.numThreads = simulationSettings.numThreads;
                        bakeParams.rayBatchSize = simulationSettings.rayBatchSize;
                        bakeParams.irradianceMinDistance = SteamAudioSettings.Singleton.bakingIrradianceMinDistance;
                        bakeParams.bakeBatchSize = 1;

                        if (SteamAudioSettings.Singleton.bakeConvolution)
                            bakeParams.flags = bakeParams.flags | ReflectionsBakeFlags.BakeConvolution;

                        if (SteamAudioSettings.Singleton.bakeParametric)
                            bakeParams.flags = bakeParams.flags | ReflectionsBakeFlags.BakeParametric;

                        if (simulationSettings.sceneType == SceneType.RadeonRays)
                        {
                            bakeParams.openCLDevice = SteamAudioManager.OpenCLDevice;
                            bakeParams.radeonRaysDevice = SteamAudioManager.RadeonRaysDevice;
                            bakeParams.bakeBatchSize = SteamAudioSettings.Singleton.bakingBatchSize;
                        }

                        API.iplReflectionsBakerBake(SteamAudioManager.Context.Get(), ref bakeParams, sProgressCallback, IntPtr.Zero);
                    }
                    else
                    {
                        var bakeParams = new PathBakeParams { };
                        bakeParams.scene = SteamAudioManager.CurrentScene.Get();
                        bakeParams.probeBatch = probeBatch.Get();
                        bakeParams.identifier = sTasks[i].identifier;
                        bakeParams.numSamples = SteamAudioSettings.Singleton.bakingVisibilitySamples;
                        bakeParams.radius = SteamAudioSettings.Singleton.bakingVisibilityRadius;
                        bakeParams.threshold = SteamAudioSettings.Singleton.bakingVisibilityThreshold;
                        bakeParams.visRange = SteamAudioSettings.Singleton.bakingVisibilityRange;
                        bakeParams.pathRange = SteamAudioSettings.Singleton.bakingPathRange;
                        bakeParams.numThreads = SteamAudioManager.Singleton.NumThreadsForCPUCorePercentage(SteamAudioSettings.Singleton.bakedPathingCPUCoresPercentage);

                        API.iplPathBakerBake(SteamAudioManager.Context.Get(), ref bakeParams, sProgressCallback, IntPtr.Zero);
                    }

                    if (sCancel)
                    {
                        Debug.Log("CANCELLED: Baking.");
                        return;
                    }

                    // Don't flush the writes to disk just yet, because we can only do it from the main thread.
                    probeBatches[j].probeDataSize = probeBatch.Save(sTasks[i].probeBatchAssets[j], false);

                    var dataSize = (int) probeBatch.GetDataSize(sTasks[i].identifier);
                    probeBatches[j].AddOrUpdateLayer(sTasks[i].gameObject, sTasks[i].identifier, dataSize);

                    if (sTasks[i].identifier.type == BakedDataType.Reflections)
                    {
                        switch (sTasks[i].identifier.variation)
                        {
                        case BakedDataVariation.Reverb:
                            (sTasks[i].component as SteamAudioListener).UpdateBakedDataStatistics();
                            break;

                        case BakedDataVariation.StaticSource:
                            (sTasks[i].component as SteamAudioBakedSource).UpdateBakedDataStatistics();
                            break;

                        case BakedDataVariation.StaticListener:
                            (sTasks[i].component as SteamAudioBakedListener).UpdateBakedDataStatistics();
                            break;
                        }
                    }
                }

                Debug.Log(string.Format("COMPLETED: Baking effect for {0}.", sCurrentObjectName));
            }

            sStatus = BakeStatus.Complete;
        }
    }
}

