using UnityEngine;
using UnityEditor;

namespace Cinemachine.Editor
{
    internal static class CinemachineMenu
    {
        const string m_CinemachineAssetsRootMenu = "Assets/Create/Cinemachine/";
        const string m_CinemachineGameObjectRootMenu = "GameObject/Cinemachine/";
        const int m_GameObjectMenuPriority = 11; // Right after Camera.

        // Assets Menu

        [MenuItem(m_CinemachineAssetsRootMenu + "BlenderSettings")]
        static void CreateBlenderSettingAsset()
        {
            ScriptableObjectUtility.Create<CinemachineBlenderSettings>();
        }

        [MenuItem(m_CinemachineAssetsRootMenu + "NoiseSettings")]
        static void CreateNoiseSettingAsset()
        {
            ScriptableObjectUtility.Create<NoiseSettings>();
        }

        [MenuItem(m_CinemachineAssetsRootMenu + "Fixed Signal Definition")]
        static void CreateFixedSignalDefinition()
        {
            ScriptableObjectUtility.Create<CinemachineFixedSignal>();
        }

        // GameObject Menu

        [MenuItem(m_CinemachineGameObjectRootMenu + "Virtual Camera", false, m_GameObjectMenuPriority)]
        static void CreateVirtualCamera(MenuCommand command)
        {
            CinemachineEditorAnalytics.SendCreateEvent("Virtual Camera");
            CreateDefaultVirtualCamera(parentObject: command.context as GameObject, select: true);
        }

        [MenuItem(m_CinemachineGameObjectRootMenu + "FreeLook Camera", false, m_GameObjectMenuPriority)]
        static void CreateFreeLookCamera(MenuCommand command)
        {
            CinemachineEditorAnalytics.SendCreateEvent("FreeLook Camera");
            CreateCinemachineObject<CinemachineFreeLook>("FreeLook Camera", command.context as GameObject, true);
        }

        [MenuItem(m_CinemachineGameObjectRootMenu + "Blend List Camera", false, m_GameObjectMenuPriority)]
        static void CreateBlendListCamera(MenuCommand command)
        {
            CinemachineEditorAnalytics.SendCreateEvent("Blend List Camera");
            var blendListCamera = CreateCinemachineObject<CinemachineBlendListCamera>(
                "Blend List Camera", command.context as GameObject, true);

            // We give the camera a couple of children as an example of setup
            var childVcam1 = CreateDefaultVirtualCamera(parentObject: blendListCamera.gameObject);
            var childVcam2 = CreateDefaultVirtualCamera(parentObject: blendListCamera.gameObject);
            childVcam2.m_Lens.FieldOfView = 10;

            // Set up initial instruction set
            blendListCamera.m_Instructions = new CinemachineBlendListCamera.Instruction[2];
            blendListCamera.m_Instructions[0].m_VirtualCamera = childVcam1;
            blendListCamera.m_Instructions[0].m_Hold = 1f;
            blendListCamera.m_Instructions[1].m_VirtualCamera = childVcam2;
            blendListCamera.m_Instructions[1].m_Blend.m_Style = CinemachineBlendDefinition.Style.EaseInOut;
            blendListCamera.m_Instructions[1].m_Blend.m_Time = 2f;
        }

#if CINEMACHINE_UNITY_ANIMATION
        [MenuItem(m_CinemachineGameObjectRootMenu + "State-Driven Camera", false, m_GameObjectMenuPriority)]
        static void CreateStateDivenCamera(MenuCommand command)
        {
            CinemachineEditorAnalytics.SendCreateEvent("State-Driven Camera");
            var stateDrivenCamera = CreateCinemachineObject<CinemachineStateDrivenCamera>(
                "State-Driven Camera", command.context as GameObject, true);

            // We give the camera a child as an example setup
            CreateDefaultVirtualCamera(parentObject: stateDrivenCamera.gameObject);
        }
#endif

#if CINEMACHINE_PHYSICS
        [MenuItem(m_CinemachineGameObjectRootMenu + "ClearShot Camera", false, m_GameObjectMenuPriority)]
        static void CreateClearShotVirtualCamera(MenuCommand command)
        {
            CinemachineEditorAnalytics.SendCreateEvent("ClearShot Camera");
            var clearShotCamera = CreateCinemachineObject<CinemachineClearShot>(
                "ClearShot Camera", command.context as GameObject, true);

            // We give the camera a child as an example setup
            var childVcam = CreateDefaultVirtualCamera(parentObject: clearShotCamera.gameObject);
            Undo.AddComponent<CinemachineCollider>(childVcam.gameObject).m_AvoidObstacles = false;
        }
#endif

        [MenuItem(m_CinemachineGameObjectRootMenu + "Dolly Camera with Track", false, m_GameObjectMenuPriority)]
        static void CreateDollyCameraWithPath(MenuCommand command)
        {
            CinemachineEditorAnalytics.SendCreateEvent("Dolly Camera with Track");
            var path = CreateCinemachineObject<CinemachineSmoothPath>(
                "Dolly Track", command.context as GameObject, false);
            var vcam = CreateCinemachineObject<CinemachineVirtualCamera>(
                "Virtual Camera", command.context as GameObject, true);
            vcam.m_Lens = MatchSceneViewCamera(vcam.transform);

            AddCinemachineComponent<CinemachineComposer>(vcam);
            AddCinemachineComponent<CinemachineTrackedDolly>(vcam).m_Path = path;
        }

        [MenuItem(m_CinemachineGameObjectRootMenu + "Dolly Track with Cart", false, m_GameObjectMenuPriority)]
        static void CreateDollyTrackWithCart(MenuCommand command)
        {
            CinemachineEditorAnalytics.SendCreateEvent("Dolly Track with Cart");
            var path = CreateCinemachineObject<CinemachineSmoothPath>(
                "Dolly Track", command.context as GameObject, false);
            CreateCinemachineObject<CinemachineDollyCart>(
                "Dolly Cart", command.context as GameObject, true).m_Path = path;
        }

        [MenuItem(m_CinemachineGameObjectRootMenu + "Target Group Camera", false, m_GameObjectMenuPriority)]
        static void CreateTargetGroupCamera(MenuCommand command)
        {
            CinemachineEditorAnalytics.SendCreateEvent("Target Group Camera");
            var vcam = CreateCinemachineObject<CinemachineVirtualCamera>(
                "Virtual Camera", command.context as GameObject, false);
            vcam.m_Lens = MatchSceneViewCamera(vcam.transform);

            AddCinemachineComponent<CinemachineGroupComposer>(vcam);
            AddCinemachineComponent<CinemachineTransposer>(vcam);

            var targetGroup = CreateCinemachineObject<CinemachineTargetGroup>(
                "Target Group", command.context as GameObject, true);
            vcam.LookAt = targetGroup.transform;
            vcam.Follow = targetGroup.transform;
        }

        [MenuItem(m_CinemachineGameObjectRootMenu + "Mixing Camera", false, m_GameObjectMenuPriority)]
        static void CreateMixingCamera(MenuCommand command)
        {
            CinemachineEditorAnalytics.SendCreateEvent("Mixing Camera");
            var mixingCamera = CreateCinemachineObject<CinemachineMixingCamera>(
                "Mixing Camera", command.context as GameObject, true);

            // We give the camera a couple of children as an example of setup
            CreateDefaultVirtualCamera(parentObject: mixingCamera.gameObject);
            CreateDefaultVirtualCamera(parentObject: mixingCamera.gameObject);
        }

        [MenuItem(m_CinemachineGameObjectRootMenu + "2D Camera", false, m_GameObjectMenuPriority)]
        static void Create2DCamera(MenuCommand command)
        {
            CinemachineEditorAnalytics.SendCreateEvent("2D Camera");
            var vcam = CreateCinemachineObject<CinemachineVirtualCamera>(
                "Virtual Camera", command.context as GameObject, true);
            vcam.m_Lens = MatchSceneViewCamera(vcam.transform);

            AddCinemachineComponent<CinemachineFramingTransposer>(vcam);
        }

        /// <summary>
        /// Sets the specified <see cref="Transform"/> to match the position and 
        /// rotation of the <see cref="SceneView"/> camera, and returns the scene view 
        /// camera's lens settings.
        /// </summary>
        /// <param name="sceneObject">The <see cref="Transform"/> to match with the 
        /// <see cref="SceneView"/> camera.</param>
        /// <returns>A <see cref="LensSettings"/> representing the scene view camera's lens</returns>
        public static LensSettings MatchSceneViewCamera(Transform sceneObject)
        {
            var lens = LensSettings.Default;

            // Take initial settings from the GameView camera, because we don't want to override 
            // things like ortho vs perspective - we just want position and FOV
            var brain = GetOrCreateBrain();
            if (brain != null && brain.OutputCamera != null)
                lens = LensSettings.FromCamera(brain.OutputCamera);

            if (SceneView.lastActiveSceneView != null)
            {
                var src = SceneView.lastActiveSceneView.camera;
                sceneObject.SetPositionAndRotation(src.transform.position, src.transform.rotation);
                if (lens.Orthographic == src.orthographic)
                {
                    if (src.orthographic)
                        lens.OrthographicSize = src.orthographicSize;
                    else
                        lens.FieldOfView = src.fieldOfView;
                }
            }
            return lens;
        }

        /// <summary>
        /// Creates a <see cref="CinemachineVirtualCamera"/> with standard procedural components.
        /// </summary>
        public static CinemachineVirtualCamera CreateDefaultVirtualCamera(
            string name = "Virtual Camera", GameObject parentObject = null, bool select = false)
        {
            var vcam = CreateCinemachineObject<CinemachineVirtualCamera>(name, parentObject, select);
            vcam.m_Lens = MatchSceneViewCamera(vcam.transform);

            AddCinemachineComponent<CinemachineComposer>(vcam);
            AddCinemachineComponent<CinemachineTransposer>(vcam);

            return vcam;
        }

        /// <summary>
        /// Creates a <see cref="CinemachineVirtualCamera"/> with no procedural components.
        /// </summary>
        public static CinemachineVirtualCamera CreatePassiveVirtualCamera(
            string name = "Virtual Camera", GameObject parentObject = null, bool select = false)
        {
            var vcam = CreateCinemachineObject<CinemachineVirtualCamera>(name, parentObject, select);
            vcam.m_Lens = MatchSceneViewCamera(vcam.transform);
            return vcam;
        }

        /// <summary>
        /// Creates a Cinemachine <see cref="GameObject"/> in the scene with a specified component.
        /// </summary>
        /// <typeparam name="T">The type of <see cref="Component"/> to add to the new <see cref="GameObject"/>.</typeparam>
        /// <param name="name">The name of the new <see cref="GameObject"/>.</param>
        /// <param name="parentObject">The <see cref="GameObject"/> to parent the new <see cref="GameObject"/> to.</param>
        /// <param name="select">Whether the new <see cref="GameObject"/> should be selected.</param>
        /// <returns>The instance of the component that is added to the new <see cref="GameObject"/>.</returns>
        static T CreateCinemachineObject<T>(string name, GameObject parentObject, bool select) where T : Component
        {
            // We always enforce the existence of the CM brain
            GetOrCreateBrain();

            // We use ObjectFactory to create a new GameObject as it automatically supports undo/redo
            var go = ObjectFactory.CreateGameObject(name);
            T component = go.AddComponent<T>();

            if (parentObject != null)
                Undo.SetTransformParent(go.transform, parentObject.transform, "Set parent of " + name);

            // We ensure that the new object has a unique name, for example "Camera (1)".
            // This must be done after setting the parent in order to get an accurate unique name
            GameObjectUtility.EnsureUniqueNameForSibling(go);

            // We set the new object to be at the current pivot of the scene.
            // GML TODO: Support the "Place Objects At World Origin" preference option in 2020.3+, see GOCreationCommands.cs
            if (SceneView.lastActiveSceneView != null)
                go.transform.position = SceneView.lastActiveSceneView.pivot;

            if (select)
                Selection.activeGameObject = go;

            return component;
        }

        /// <summary>
        /// Gets the first loaded <see cref="CinemachineBrain"/>. Creates one on 
        /// the <see cref="Camera.main"/> if none were found.
        /// </summary>
        static CinemachineBrain GetOrCreateBrain()
        {
            if (CinemachineCore.Instance.BrainCount > 0)
                return CinemachineCore.Instance.GetActiveBrain(0);

            // Create a CinemachineBrain on the main camera
            var cam = Camera.main;
            if (cam == null)
#if UNITY_2023_1_OR_NEWER
                cam = Object.FindFirstObjectByType<Camera>(FindObjectsInactive.Exclude);
#else
                cam = Object.FindObjectOfType<Camera>();
#endif
            if (cam != null)
                return Undo.AddComponent<CinemachineBrain>(cam.gameObject);

            // No camera, just create a brain on an empty object
            return ObjectFactory.CreateGameObject("CinemachineBrain").AddComponent<CinemachineBrain>();
        }

        /// <summary>
        /// Adds an component to the specified <see cref="CinemachineVirtualCamera"/>'s hidden 
        /// component owner, that supports undo.
        /// </summary>
        /// <typeparam name="T">The type of <see cref="Component"/> to add to the cinemachine pipeline.</typeparam>
        /// <param name="vcam">The <see cref="CinemachineVirtualCamera"/> to add components to.</param>
        /// <returns>The instance of the componented that was added.</returns>
        static T AddCinemachineComponent<T>(CinemachineVirtualCamera vcam) where T : CinemachineComponentBase
        {
            // We can't use the CinemachineVirtualCamera.AddCinemachineComponent<T>()
            // because we want to support undo/redo
            var componentOwner = vcam.GetComponentOwner().gameObject;
            if (componentOwner == null)
                return null; // maybe it's an invalid prefab instance
            var component = Undo.AddComponent<T>(componentOwner);
            vcam.InvalidateComponentPipeline();
            return component;
        }
    }
}