using System;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using Multiplayer.Client.Comp;
using Multiplayer.Client.Desyncs;
using Multiplayer.Client.Factions;
using Multiplayer.Client.Patches;
using Multiplayer.Client.Saving;
using Multiplayer.Client.Util;
using Multiplayer.Common;
using RimWorld;
using RimWorld.Planet;
using UnityEngine;
using Verse;

namespace Multiplayer.Client.AsyncTime;

public class AsyncWorldTimeComp : IExposable, ITickable
{
    public static bool tickingWorld;
    private TimeSpeed timeSpeedInt;

    public float TimeToTickThrough { get; set; }

    public float TickRateMultiplier(TimeSpeed speed)
    {
        if (Multiplayer.GameComp.asyncTime)
        {
            var enforcePause = Multiplayer.WorldComp.sessionManager.IsAnySessionCurrentlyPausing(null);

            if (enforcePause)
                return 0f;
        }

        return speed switch
        {
            TimeSpeed.Paused => 0f,
            TimeSpeed.Normal => 1f,
            TimeSpeed.Fast => 3f,
            TimeSpeed.Superfast => 6f,
            TimeSpeed.Ultrafast => 15f,
            _ => -1f
        };
    }

    // Run at the speed of the fastest map or at chosen speed if there are no maps
    public TimeSpeed DesiredTimeSpeed
    {
        get => !Find.Maps.Any()
            ? timeSpeedInt
            : Find.Maps.Select(m => m.AsyncTime())
                .Where(a => a.ActualRateMultiplier(a.DesiredTimeSpeed) != 0f)
                .Max(a => a?.DesiredTimeSpeed) ?? TimeSpeed.Paused;
        set => timeSpeedInt = value;
    }

    public Queue<ScheduledCommand> Cmds => cmds;
    public Queue<ScheduledCommand> cmds = new();

    public int CurrentPlayerCount { get; private set; }
    public int VTR => CurrentPlayerCount > 0 ? VTRSync.MinimumVtr : VTRSync.MaximumVtr;

    public int TickableId => -1;

    public World world;
    public ulong randState;

    public int worldTicks;

    public AsyncWorldTimeComp(World world)
    {
        this.world = world;

        // Use the world's constant rand seed as our initial randState.
        // Only fill the seed part, leave the iterations out.
        randState = (uint)world.ConstantRandSeed;
    }

    public void ExposeData()
    {
        var timer = TickPatch.Timer;
        Scribe_Values.Look(ref timer, "timer");
        TickPatch.SetTimer(timer);

        Scribe_Values.Look(ref timeSpeedInt, "timeSpeed");
        Scribe_Custom.LookULong(ref randState, "randState", 2);

        TimeSpeed timeSpeed = Find.TickManager.CurTimeSpeed;
        Scribe_Values.Look(ref timeSpeed, "timeSpeed");
        if (Scribe.mode == LoadSaveMode.LoadingVars)
            Find.TickManager.CurTimeSpeed = timeSpeed;

        if (Scribe.mode == LoadSaveMode.LoadingVars)
            Multiplayer.game.worldComp = new MultiplayerWorldComp(world);

        Multiplayer.game.worldComp.ExposeData();

        if (Scribe.mode == LoadSaveMode.LoadingVars)
            worldTicks = Find.TickManager.TicksGame;
    }

    public void Tick()
    {
        tickingWorld = true;
        PreContext();

        try
        {
            Find.TickManager.DoSingleTick();
            worldTicks++;
            Multiplayer.WorldComp.TickWorldSessions();

            if (ModsConfig.BiotechActive)
            {
                // Vanilla puts those into a separate try/catch blocks
                try
                {
                    CompDissolutionEffect_Goodwill.WorldUpdate();
                }
                catch (Exception e)
                {
                    Log.Error(e.ToString());
                }
                try
                {
                    CompDissolutionEffect_Pollution.WorldUpdate();
                }
                catch (Exception e)
                {
                    Log.Error(e.ToString());
                }
            }
        }
        finally
        {
            PostContext();
            tickingWorld = false;

            Multiplayer.game.sync.TryAddWorldRandomState(randState);
        }
    }

    public void PreContext()
    {
        Find.TickManager.CurTimeSpeed = DesiredTimeSpeed;
        Rand.PushState();
        Rand.StateCompressed = randState;

        if (Multiplayer.GameComp.multifaction)
        {
            FactionExtensions.PushFaction(null, Multiplayer.WorldComp.spectatorFaction, force: true);
            foreach (var map in Find.Maps)
                map.MpComp().SetFaction(Multiplayer.WorldComp.spectatorFaction);
        }
    }

    public void PostContext()
    {
        if (Multiplayer.GameComp.multifaction)
        {
            var f = FactionExtensions.PopFaction();
            foreach (var map in Find.Maps)
                map.MpComp().SetFaction(f);
        }

        randState = Rand.StateCompressed;
        Rand.PopState();
    }

    public void ExecuteCmd(ScheduledCommand cmd)
    {
        CommandType cmdType = cmd.type;
        LoggingByteReader data = new LoggingByteReader(cmd.data);
        data.Log.Node($"{cmdType} Global");

        TickPatch.currentExecutingCmdIssuedBySelf = cmd.IsIssuedBySelf() && !TickPatch.Simulating;
        TickPatch.currentExecutingCmdType = cmdType;

        PreContext();
        FactionExtensions.PushFaction(null, cmd.GetFaction());

        bool prevDevMode = Prefs.data.devMode;
        var prevGodMode = DebugSettings.godMode;
        Multiplayer.GameComp.playerData.GetValueOrDefault(cmd.playerId)?.SetContext();

        var randCalls1 = DeferredStackTracing.randCalls;

        try
        {
            if (cmdType == CommandType.Sync)
            {
                var handler = SyncUtil.HandleCmd(data);
                data.Log.current.text = handler.ToString();
            }

            if (cmdType == CommandType.DebugTools)
            {
                DebugSync.HandleCmd(data);
            }

            if (cmdType == CommandType.GlobalTimeSpeed)
            {
                HandleTimeSpeed(cmd, data);
            }

            if (cmdType == CommandType.TimeSpeedVote)
            {
                HandleTimeVote(cmd, data);
            }

            if (cmdType == CommandType.PauseAll)
            {
                SetTimeEverywhere(TimeSpeed.Paused);
            }

            if (cmdType == CommandType.CreateJoinPoint)
            {
                if (Multiplayer.session?.ConnectedToStandaloneServer == true && !TickPatch.currentExecutingCmdIssuedBySelf)
                    return;

                LongEventHandler.QueueLongEvent(CreateJoinPointAndSendIfHost, "MpCreatingJoinPoint", false, null);
            }

            if (cmdType == CommandType.InitPlayerData)
            {
                var playerId = data.ReadInt32();
                var canUseDevMode = data.ReadBool();
                Multiplayer.GameComp.playerData[playerId] = new PlayerData { canUseDevMode = canUseDevMode };
            }

            if (cmdType == CommandType.PlayerCount)
            {
                int previousMapId = data.ReadInt32();
                int newMapId = data.ReadInt32();
                int mapCount = Find.Maps.Count;

                var prev = -1;
                if (previousMapId >= 0)
                    prev = Find.Maps.FirstOrDefault(x => x.uniqueID == previousMapId)?.AsyncTime()?.DecreasePlayerCount() ?? -1;
                else if (previousMapId == VTRSync.WorldMapId)
                    prev = Multiplayer.AsyncWorldTime.CurrentPlayerCount -= 1;

                var curr = -1;
                if (newMapId >= 0)
                    curr = Find.Maps.FirstOrDefault(x => x.uniqueID == newMapId)?.AsyncTime()?.IncreasePlayerCount() ?? -1;
                else if (newMapId == VTRSync.WorldMapId)
                    curr = Multiplayer.AsyncWorldTime.CurrentPlayerCount += 1;

                MpLog.Debug($"[{worldTicks}|{Multiplayer.session.remoteTickUntil}] Player count change: previousMapId={previousMapId} ({prev}), newMapId={newMapId} ({curr}), mapCount={mapCount}");
            }
        }
        catch (Exception e)
        {
            Log.Error($"World cmd exception ({cmdType}): {e}");
        }
        finally
        {
            DebugSettings.godMode = prevGodMode;
            Prefs.data.devMode = prevDevMode;

            MpLog.Debug($"rand calls {DeferredStackTracing.randCalls - randCalls1}");
            MpLog.Debug("rand state " + Rand.StateCompressed);

            FactionExtensions.PopFaction();
            PostContext();
            TickPatch.currentExecutingCmdIssuedBySelf = false;
            TickPatch.currentExecutingCmdType = null;

            Multiplayer.game.sync.TryAddCommandRandomState(randState);

            if (cmdType != CommandType.GlobalTimeSpeed)
                Multiplayer.ReaderLog.AddCurrentNode(data);
        }
    }

    private static void CreateJoinPointAndSendIfHost()
    {
        Multiplayer.session.dataSnapshot = SaveLoad.SaveReloadAndCreateSnapshot(
            Multiplayer.GameComp.multifaction,
            ReloadOptimizationMode.ForJoinPointSnapshot
        );

        if (!TickPatch.Simulating && !Multiplayer.IsReplay)
        {
            if (Multiplayer.session?.ConnectedToStandaloneServer == true)
            {
                // Standalone: every client uploads world data + individual snapshots
                SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, true);
                SaveLoad.SendStandaloneMapSnapshots(Multiplayer.session.dataSnapshot);
                SaveLoad.SendStandaloneWorldSnapshot(Multiplayer.session.dataSnapshot);
            }
            else if (Multiplayer.LocalServer != null || Multiplayer.arbiterInstance)
            {
                // Hosted: only host/arbiter uploads world data
                SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, true);
            }
        }
    }

    public void SetTimeEverywhere(TimeSpeed speed)
    {
        foreach (var map in Find.Maps)
            map.AsyncTime().DesiredTimeSpeed = speed;
        DesiredTimeSpeed = speed;
    }

    public static float lastSpeedChange;

    private void HandleTimeSpeed(ScheduledCommand cmd, ByteReader data)
    {
        TimeSpeed speed = (TimeSpeed)data.ReadByte();
        DesiredTimeSpeed = speed;

        if (!Multiplayer.GameComp.asyncTime)
        {
            SetTimeEverywhere(speed);

            if (!cmd.IsIssuedBySelf())
                lastSpeedChange = Time.realtimeSinceStartup;
        }

        MpLog.Debug($"Set world speed {speed} {TickPatch.Timer} {Find.TickManager.TicksGame}");
    }

    private void HandleTimeVote(ScheduledCommand cmd, ByteReader data)
    {
        TimeVote vote = (TimeVote)data.ReadByte();
        int tickableId = data.ReadInt32();

        // Update the vote
        if (vote >= TimeVote.ResetTickable)
            Multiplayer.GameComp.playerData.Do(p => p.Value.SetTimeVote(tickableId, vote));
        else if (Multiplayer.GameComp.playerData.GetValueOrDefault(cmd.playerId) is { } playerData)
            playerData.SetTimeVote(tickableId, vote);

        // Update the time speed
        if (!Multiplayer.GameComp.asyncTime || vote == TimeVote.ResetGlobal)
            SetTimeEverywhere(Multiplayer.GameComp.GetLowestTimeVote(TickableId));
        else if (TickPatch.TickableById(tickableId) is { } tickable)
            tickable.DesiredTimeSpeed = Multiplayer.GameComp.GetLowestTimeVote(tickableId);
    }

    public void FinalizeInit()
    {
        Multiplayer.game.SetThingMakerSeed((int)(randState >> 32));
    }
}
