using System;
using NUnit.Framework;
using gdio.unity_api.v2;
using gdio.common.objects;

namespace D_Demo_Full
{
    [TestFixture]
    public class MyClass
    {
        // IDE, standalone or appium
        static string mode = "IDE";
        //static string mode = "standalone";

        // IP address of the test host. Use localhost for local.
        //static string host = "192.168.86.21"; // replace device IP address. Must be run in 'standalone' mode for Mobile
        static string host = "localhost";

        //static string pathToExe = @"C:\Users\<user>\Desktop\3DDemo\3DDemo.exe";
        //static string pathToExe = @"/Users/<user>/Desktop/3DDemo.app/Contents/MacOS/3D_Demo_Correct";
        static string pathToExe = null;

        ApiClient api;

        // Get the PID for later use
        int PID;

        public string testMode = TestContext.Parameters.Get("Mode", mode);
        public string testHost = TestContext.Parameters.Get("Host", host);
        public string executablePath = TestContext.Parameters.Get("executablePath", pathToExe);

        [OneTimeSetUp]
        public void Connect()
        {

            api = new ApiClient();

            if (executablePath != null && testMode == "standalone")
            {
                PID = ApiClient.Launch(executablePath);
                api.Wait(5000);
                Console.WriteLine($"Launching standalone executable with PID: {PID}");
                api.Connect(testHost);
            }
            else if (executablePath == null && testMode == "IDE")
            {
                api.Connect(testHost, 19734, true, 30);
            }
            else api.Connect(testHost, 19734, false, 30);

            api.EnableHooks(HookingObject.KEYBOARD);
            api.EnableHooks(HookingObject.MOUSE);

            api.LoggedMessage += (s, e) =>
            {
                Console.WriteLine(e.Message);
            };
            
            api.UnityLoggedMessage += (s, e) =>
            {
                Console.WriteLine($"Type: {e.type.ToString()}\r\nCondition: {e.condition}\r\nStackTrace: {e.stackTrace}");
            };

            if (api.GetSceneName() == "Start")
            {
                api.WaitForObject("//*[@name='StartButton']");
                api.ClickObject(MouseButtons.LEFT, "//*[@name='StartButton']", 30);
                api.Wait(2000);
                api.WaitForObject("//*[@name='Ellen']");
            }

            Assert.AreEqual("Level1", api.GetSceneName());
        }


        [OneTimeTearDown]
        public void Disconnect()
        {
            api.Wait(2000);

            api.DisableHooks(HookingObject.ALL);
            api.Wait(2000);

            api.Disconnect();

            if (testMode == "IDE")
            {
                api.Wait(2000);
                api.StopEditorPlay();
            }
            else if (testMode == "standalone")
            {
                ApiClient.TerminateGame();
            }
        }

        [Test, Order(0)]
        public void TestMovementInputs()
        {
            Assert.IsTrue(api.GetSceneName() == "Level1", "Wrong Scene!");

            // Wait for the Player to become active
            api.WaitForObject("//*[@name='Ellen']");

            // Get the initial position of the player and output to the log
            Vector3 ellenPos = api.GetObjectPosition("//*[@name='Ellen']");
            Console.WriteLine($"Original position is:" + ellenPos.ToString());

            // Move the Player along both axis, positive and negative
            var fps = (ulong)api.GetLastFPS();
            api.AxisPress("Horizontal", 1f, fps * 5);
            api.Wait(1000);
            api.AxisPress("Vertical", 1f, fps * 5);
            api.Wait(1000);
            api.AxisPress("Horizontal", -1f, fps * 5);
            api.Wait(1000);
            api.AxisPress("Vertical", -1f, fps * 5);
            api.Wait(1000);

            // Check the final position of the player, compare to the initial, and output to log 
            Vector3 newPos = api.GetObjectPosition("//*[@name='Ellen']");
            Console.WriteLine($"New position is:" + newPos.ToString());
            Assert.AreNotEqual(ellenPos, newPos, "Ellen didn't move!");
        }

        [Test, Order(1)]
        public void TestCameraMovement()
        {
            // Wait for the Player to become active
            api.WaitForObject("//*[@name='Ellen']");

            // Get the intitial position of the camera
            Vector3 initialCameraPos = api.GetObjectPosition("//MainCamera[@name='CameraBrain']");

            // Move the camera along both axis
            var fps = (ulong)api.GetLastFPS();
            api.AxisPress("CameraX", 1f, fps * 4);
            api.AxisPress("CameraY", 1f, fps * 4);
            api.Wait(5000);

            // Check the new position of the camera, and compare to the initial
            Vector3 newCameraPos = api.GetObjectPosition("//MainCamera[@name='CameraBrain']");
            Assert.AreNotEqual(newCameraPos, initialCameraPos, "Camera didn't move!");
            
        }

        [Test, Order(2)]
        public void TestMenu()
        {
            // It shouldn't matter where we are, the menu will appear
            api.ButtonPress("Pause", 30, 30);
            api.Wait(1000);

            // Check that the menu appeared
            Assert.IsTrue(api.GetObjectFieldValue<bool>("//*[@name='PauseCanvas']", "active"), "Menu didn't appear!");

            api.ButtonPress("Pause", 30, 30);
            api.Wait(3000);
            
        }

        [Test, Order(3)]
        public void GetWeaponToEnableAttacks()
        {
            // If the weapon is active, go get it
            if (api.GetObjectFieldValue<bool>("(//*[@name='Staff'])[1]/@active") == true)
            {
                // Move to the staff to enable melee attacks
                api.SetObjectFieldValue($"//*[@name='Ellen']/fn:component('UnityEngine.Transform')",
                    "position", api.GetObjectPosition("(//*[@name='Staff'])[1]", CoordinateConversion.None));
                api.Wait(1000);
            }

            // Check that we can attack now
            Assert.IsTrue(api.GetObjectFieldValue<bool>("/Untagged[@name='Ellen']/fn:component('Gamekit3D.PlayerController')/@canAttack"),
                "Melee not enabled!");
        }

        [Test, Order(4)]
        public void KillEverything()
        {
            // If the melee attack isn't enabled, enable it
            if (api.GetObjectFieldValue<bool>("/Untagged[@name='Ellen']/fn:component('Gamekit3D.PlayerController')/@canAttack") == false)
            {
                // Enable melee attack by calling the method
                api.CallMethod("/Untagged[@name='Ellen']/fn:component('Gamekit3D.PlayerController')", "SetCanAttack", new object[] { true });
            }

            try
            {
                while (api.WaitForObject("//*[@name='Chomper']", 5) != false) // Maybe add a check for whether the chomper is killed or alive, i.e. "&& api.WaitForObjectValue(...)"
                {
                    Vector3 dest = CloseToObject("//*[@name='Chomper']");
                    Vector3 target = api.GetObjectPosition("//*[@name='Chomper']", CoordinateConversion.None);

                    SetObjectPosition("//*[@name= 'Ellen']", dest);
                    api.Wait(300);

                    // LookAt the target object
                    api.CallMethod("//*[@name='Ellen']/fn:component('UnityEngine.Transform')", "LookAt", new Vector3[] { target });
                    api.Wait(300);

                    api.ButtonPress("Fire1", 30, 30);
                    api.Wait(1000);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }

            Assert.IsFalse(api.WaitForObject("//*[@name='Chomper']", 5), "We missed one!");
        }
        
        [Test, Order(5)]
        public void BreakAllBoxes()
        {
            api.Wait(5000);

            // If the melee attack isn't enabled, enable it
            if (api.GetObjectFieldValue<bool>("/Untagged[@name='Ellen']/fn:component('Gamekit3D.PlayerController')/@canAttack") == false)
            {
                // Enable melee attack by calling the method
                api.CallMethod("/Untagged[@name='Ellen']/fn:component('Gamekit3D.PlayerController')", "SetCanAttack", new object[] { true });
            }

            // Get the full list of objects in the scene
            var objectList = api.GetObjectList("//*[@name='Destructibles']/*", true, 60);

            //Test whether the list is null
            Assert.IsNotNull(objectList, "GetObjectList failed!");

            int boxCount = 0;

            /* Count the total number of boxes
             * The actual number will be half that
             * The even numbered boxes are the ones we want
             * We have to test whether the child object is active
             */
            foreach (var obj in objectList)
            {
                if (obj.name == "DestructibleBox")
                {
                    Console.WriteLine("Object Name: " + obj.name);
                    Console.WriteLine("Object Tag: " + obj.tag);
                    Console.WriteLine("Object HPath: " + obj.hierarchyPath);
                    Console.WriteLine($"Object Rotation (w): {obj.rotation.w}, (x): {obj.rotation.x}, (y): {obj.rotation.x}, (z): {obj.rotation.z}");
                    boxCount++;
                }   
            }
            
            try
            {
                while (boxCount > 0) // Count backwards
                {
                    if (api.GetObjectFieldValue<bool>($"(/*[@name='Destructibles']/*[@name='DestructibleBox'])[{boxCount / 2 - 1}]/*[@name='DestructibleBox']", "active") == true)
                    {
                        Vector3 dest = CloseToObject($"(//*[@name='DestructibleBox'])[{boxCount - 2}]");
                        Vector3 target = api.GetObjectPosition($"(//*[@name='DestructibleBox'])[{boxCount - 2}]", CoordinateConversion.None);

                        SetObjectPosition("//*[@name='Ellen']", dest);
                        api.Wait(500);

                        // LookAt the target object
                        api.CallMethod("//*[@name='Ellen']/fn:component('UnityEngine.Transform')", "LookAt", new Vector3[] { target });
                        api.Wait(500);

                        api.ButtonPress("Fire1", 30, 30);
                        api.Wait(1000);
                    }

                    boxCount -= 2;
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }

        }

        //Helper functions

        public void SetObjectPosition(string HPath, Vector3 pos)
        {
            api.SetObjectFieldValue($"{HPath}/fn:component('UnityEngine.Transform')", "position", pos);
        }

        Vector3 CloseToObject(string HPath)
        {
            Vector3 initialPos = api.GetObjectPosition(HPath);
            Vector3 returnPos = new Vector3(initialPos.x - 1f, initialPos.y, initialPos.z - 1f);
            return returnPos;
        }
    }
}
