using System;
using System.Collections.Generic;
using System.Numerics;
using Godot;
using Sfs3.Client;
using Sfs3.Client.Core;
using Sfs3.Client.Entities;
using Sfs3.Client.Entities.Audio;
using Sfs3.Client.Entities.Data;
using Sfs3.Client.Entities.Variables;
using Sfs3.Client.Logging;
using Sfs3.Client.Requests;
using Sfs3.Client.Requests.Audio;
using Sfs3.Client.Util;

public partial class World : Node3D
{
    private const int WORLD_SIZE = 50;

    PackedScene avatarScene;

    private Hud hud;
    private GlobalManager global;
	private SmartFox sfs;
    private Player player;
    private Dictionary<int, Avatar> avatars = [];

    // Last position/rotation sent to the server, to avoid flooding it with unchanged updates
    private Godot.Vector3 lastSentPosition;
    private float lastSentRotationY;
    private bool hasSentInitialState;

    //----------------------------------------------------------
    // Callback Methods
    //----------------------------------------------------------
    #region

	public override void _Ready()
	{
        // NOTE
        // As this class is not extending BaseController, we have to replicate
        // some of its features, like accessing the GlobalManager instance

        global = (GlobalManager)GetNode("/root/GlobalManager");
        sfs = global.GetSfsClient();

        // Add SFS event listeners
        AddSmartFoxListeners();

        // Load avatar nested scene
        avatarScene = GD.Load<PackedScene>("scenes/sub/Avatar.tscn");

        // Spawn avatars for users already in the Room
        SpawnExistingUsers();

        // Get reference to HUD and set user name
        hud = GetNode<Hud>("HUD");
        hud.SetUserName(sfs.MySelf.Name);

        // Set random player coordinates
        var rnd = new Random();
        float x = (float)(rnd.NextDouble() * WORLD_SIZE) - WORLD_SIZE / 2;
        float y = 1;
        float z = (float)(rnd.NextDouble() * WORLD_SIZE) - WORLD_SIZE / 2;
        int deg = rnd.Next(0, 360);

        // Set player instance position
        player = GetNode<Player>("Player");
        player.Translate(new Godot.Vector3(x, y, z));
        player.RotationDegrees = new Godot.Vector3(0, deg, 0);
		
		// Init audio
		sfs.Send(new InitAudioRequest());
	}

	// Called every frame. 'delta' is the elapsed time since the previous frame.
	public override void _Process(double delta)
	{
        // Broadcast the local player position/rotation when it changes
        if (sfs != null)
            SetPlayerPosition();
	}

    public override void _ExitTree()
    {
        RemoveSmartFoxListeners();
    }

    #endregion

    //----------------------------------------------------------
    // UI event listeners
    //----------------------------------------------------------
    #region
    
    public void OnLogoutButtonClick()
    {
        sfs.Disconnect();
    }

    private void OnMuteButtonToggle(bool isPressed)
    {
        sfs.Send(new ToggleMuteAudioStreamRequest());
    }

    #endregion

    //----------------------------------------------------------
    // Helper methods
    //----------------------------------------------------------
    #region

    private void AddSmartFoxListeners()
    {
        sfs.AddEventListener(SFSEvent.USER_ENTER_ROOM, OnUserEnterRoom);
        sfs.AddEventListener(SFSEvent.USER_EXIT_ROOM, OnUserExitRoom);
        sfs.AddEventListener(SFSEvent.USER_VARIABLES_UPDATE, OnUserVarsUpdate);
        sfs.AddEventListener(SFSAudioEvent.INIT, OnAudioInit);
        sfs.AddEventListener(SFSAudioEvent.STREAM_START, OnAudioStreamStart);
        sfs.AddEventListener(SFSAudioEvent.STREAM_STOP, OnAudioStreamStop);
        sfs.AddEventListener(SFSAudioEvent.STREAM_ADD, OnAudioStreamAdd);
        sfs.AddEventListener(SFSAudioEvent.STREAM_REMOVE, OnAudioStreamRemove);
        sfs.AddEventListener(SFSAudioEvent.TOGGLE_MUTE, OnToggleMute);
    }

    private void RemoveSmartFoxListeners()
    {
        if (sfs != null)
        {
            sfs.RemoveEventListener(SFSEvent.USER_ENTER_ROOM, OnUserEnterRoom);
            sfs.RemoveEventListener(SFSEvent.USER_EXIT_ROOM, OnUserExitRoom);
            sfs.RemoveEventListener(SFSEvent.USER_VARIABLES_UPDATE, OnUserVarsUpdate);
            sfs.RemoveEventListener(SFSAudioEvent.INIT, OnAudioInit);
            sfs.RemoveEventListener(SFSAudioEvent.STREAM_START, OnAudioStreamStart);
            sfs.RemoveEventListener(SFSAudioEvent.STREAM_STOP, OnAudioStreamStop);
            sfs.RemoveEventListener(SFSAudioEvent.STREAM_ADD, OnAudioStreamAdd);
            sfs.RemoveEventListener(SFSAudioEvent.STREAM_REMOVE, OnAudioStreamRemove);
            sfs.RemoveEventListener(SFSAudioEvent.TOGGLE_MUTE, OnToggleMute);
        }
    }

    /**
     * Spawn avatars for the users already in the Room when this scene loads.
     */
    private void SpawnExistingUsers()
    {
        Room room = sfs.LastJoinedRoom;

        if (room == null)
            return;

        foreach (User user in room.UserList)
        {
            if (user.IsItMe)
                continue;

            EnsureAvatar(user);
        }
    }

    /**
     * Returns the avatar for the given user, creating (and positioning
     * it from its current User Variables) if it doesn't exist yet.
     */
    private Avatar EnsureAvatar(User user)
    {
        if (avatars.TryGetValue(user.Id, out Avatar avatar))
            return avatar;

        // Add user avatar to scene
        avatar = (Avatar)avatarScene.Instantiate();
        avatar.Name = "Avatar_" + user.Id;
        AddChild(avatar);

        avatar.SetLabel(user.Name);

        avatars.Add(user.Id, avatar);

        // Apply the user's current position/rotation (if already set), so avatars spawned for
        // pre-existing users appear in the right place without waiting for the next update
        if (user.GetVariable("p") != null)
            avatar.Position = ToGodotVector3(user.GetVariable("p").Vector3Value);

        if (user.GetVariable("r") != null)
            avatar.RotationDegrees = new Godot.Vector3(0, (float)user.GetVariable("r").FloatValue, 0);

        return avatar;
    }

    private void AddAudioStreamPlayer(int id, AudioStreamPlayer3D asp, bool isMuted)
    {
        if (avatars.TryGetValue(id, out Avatar avatar))
            avatar.AddAudioStreamPlayer(asp, isMuted);
    }

    private void RemoveAudioStreamPlayer(int id)
    {
        if (avatars.TryGetValue(id, out Avatar avatar))
            avatar.RemoveAudioStreamPlayer();
    }

    private void SetPlayerPosition()
    {
        Godot.Vector3 pos = player.Position;
        float rotY = player.RotationDegrees.Y;

        // Only send when position or rotation actually changed (small dead zone), to avoid
        // flooding the server with an update every frame while standing still
        if (hasSentInitialState &&
            (pos - lastSentPosition).LengthSquared() < 0.0001f &&
            Mathf.Abs(NormalizeAngleDeg(rotY - lastSentRotationY)) < 0.1f)
            return;

        lastSentPosition = pos;
        lastSentRotationY = rotY;
        hasSentInitialState = true;

        var p = new SFSUserVariable("p", ToSfsVector3(pos));
        var r = new SFSUserVariable("r", rotY);

        sfs.Send(new SetUserVariablesRequest([p, r]));
    }

    /**
     * Wraps an angle (in degrees) into the [-180, 180] range, so the dead-zone check
     * measures the real shortest rotation even across the 0/360 boundary.
     */
    private static float NormalizeAngleDeg(float deg)
    {
        deg %= 360f;

        if (deg > 180f)
            deg -= 360f;
        else if (deg < -180f)
            deg += 360f;

        return deg;
    }

    private SFSVector3 ToSfsVector3(Godot.Vector3 v)
    {
        return new SFSVector3(v.X, v.Y, v.Z);
    }

    private Godot.Vector3 ToGodotVector3(SFSVector3 v)
    {
        return new Godot.Vector3(v.X, v.Y, v.Z);
    }
    #endregion

    //----------------------------------------------------------
    // SmartFoxServer event listeners
    //----------------------------------------------------------
    #region

    private void OnUserEnterRoom(ApiEvent evt)
    {
        Room room = (Room)evt.GetParam(EventParam.Room);
        User user = (User)evt.GetParam(EventParam.User);

        if (user == sfs.MySelf)
            global.Trace(String.Format("I entered Room {0}", room));
        else
            global.Trace(String.Format("User {0} entered Room {1}", user, room));
    }

    private void OnUserExitRoom(ApiEvent evt)
    {
        Room room = (Room)evt.GetParam(EventParam.Room);
        User user = (User)evt.GetParam(EventParam.User);

        if (user == sfs.MySelf)
            global.Trace(String.Format("I left Room {0}", room));
        else
        {
            global.Trace(String.Format("User {0} left Room {1}", user, room));

            if (avatars.Remove(user.Id, out Avatar avatar))
                RemoveChild(avatar);
        }
    }

    private void OnUserVarsUpdate(ApiEvent evt)
    {
        User user = (User)evt.GetParam(EventParam.User);
        List<string> changedVars = (List<string>)evt.GetParam(EventParam.ChangedVars);

        if (user.IsItMe)
            return;

        // Create the avatar if this is the first time we hear about this user
        Avatar avatar = EnsureAvatar(user);

        if (changedVars.Contains("p"))
            avatar.Position = ToGodotVector3(user.GetVariable("p").Vector3Value);

        if (changedVars.Contains("r"))
            avatar.RotationDegrees = new Godot.Vector3(0, (float)user.GetVariable("r").FloatValue, 0);
    }

    private void OnAudioInit(ApiEvent evt)
    {
        global.Trace("Audio Streaming system initialized");

        // Add utility scene node to SFS API
        sfs.AudioManager.SetSceneObject(GetNode("SfsNode"));

		// Start stream
		sfs.Send(new StartAudioStreamRequest());
    }

    private void OnAudioStreamStart(ApiEvent evt)
    {
        global.Trace("My audio stream started");
    }

    private void OnAudioStreamStop(ApiEvent evt)
    {
        hud.ResetMuteButton();

        global.Trace("My audio stream stopped");
    }

    private void OnAudioStreamAdd(ApiEvent evt)
    {
        AudioStreamInfo asi = (AudioStreamInfo)evt.GetParam(EventParam.AudioStream)!;
        AudioStreamPlayer3D asp = (AudioStreamPlayer3D)evt.GetParam(EventParam.AudioAsset)!;

        AddAudioStreamPlayer(asi.Id, asp, asi.IsMuted);

        global.Trace("Audio stream added; user: " + asi.User);
    }

    private void OnAudioStreamRemove(ApiEvent evt)
    {
        AudioStreamInfo asi = (AudioStreamInfo)evt.GetParam(EventParam.AudioStream)!;

        RemoveAudioStreamPlayer(asi.Id);

        global.Trace("Audio stream removed; user: " + asi.User);
    }

    private void OnToggleMute(ApiEvent evt)
    {
        AudioStreamInfo asi = (AudioStreamInfo)evt.GetParam(EventParam.AudioStream)!;
        bool isMuted = asi.IsMuted;

        if (avatars.TryGetValue(asi.Id, out Avatar avatar))
            avatar.SetMuted(isMuted);

        if (isMuted)
            global.Trace("Audio stream muted; user: " + asi.User);
        else
            global.Trace("Audio stream unmuted; user: " + asi.User);
    }

    #endregion
}
