namespace Fusion {
  using System;
  using System.Collections;
  using System.Collections.Generic;
  using System.Linq;
  using UnityEngine;
  using UnityEngine.SceneManagement;
#if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES
  using System.Threading.Tasks;
  using UnityEngine.AddressableAssets;
  using UnityEngine.ResourceManagement.AsyncOperations;
  using UnityEngine.ResourceManagement.ResourceProviders;
#endif

  public class NetworkSceneManagerDefault : Fusion.Behaviour, INetworkSceneManager {
    /// <summary>
    /// If enabled and there is an already loaded scene that matches what the scene manager has intended to load,
    /// that scene will be used instead and load will be avoided.
    /// </summary>
    [InlineHelp]
    [ToggleLeft]
    public bool IsSceneTakeOverEnabled = true;

    /// <summary>
    /// Should all scene load errors be logged into the console? If disabled, errors can still be retrieved via the
    /// <see cref="NetworkSceneAsyncOp.Error"/> or <see cref="NetworkSceneAsyncOp.AddOnCompleted"/>.
    /// </summary>
    [InlineHelp]
    [ToggleLeft]
    public bool LogSceneLoadErrors = true;

    /// <summary>
    /// All the scenes loaded by all the managers. Used when <see cref="IsSceneTakeOverEnabled"/> is enabled.
    /// </summary>
    private static Dictionary<Scene, NetworkSceneManagerDefault> _allOwnedScenes = new Dictionary<Scene, NetworkSceneManagerDefault>(new FusionUnitySceneManagerUtils.SceneEqualityComparer());

    /// <summary>
    /// In multiple peer mode, each runner maintains its own scene where all the newly loaded scenes
    /// are moved to. This is to make sure physics are properly sandboxed.
    /// </summary>
    private List<MultiPeerSceneRoot> _multiPeerSceneRoots = new List<MultiPeerSceneRoot>();
    private MultiPeerSceneRoot _multiPeerActiveRoot;

    /// <summary>
    /// List of running coroutines. Only one is actually executed at a time.
    /// </summary>
    private List<ICoroutine> _runningCoroutines = new List<ICoroutine>();

    /// <summary>
    /// For remote clients, this manager first unloads old scenes then loads the new ones. It might happen that all
    /// the current scenes need to be unloaded and in such case a temp scene needs to be created to ensure at least one
    /// scene loaded at all times. 
    /// </summary>
    private Scene _tempUnloadScene;

    /// <summary>
    /// Scene used when Multiple Peer mode is used. Each loaded scene is merged into this one, allowing
    /// for multiple runners to have separate cross-scene physics.
    /// </summary>
    public Scene MultiPeerScene { get; private set; }

    /// <summary>
    /// Root for DontDestroyOnLoad objects. Instantiated on <see cref="MultiPeerScene"/>.
    /// </summary>
    public Transform MultiPeerDontDestroyOnLoadRoot { get; private set; }

    public NetworkRunner Runner { get; private set; }

    private bool IsMultiplePeer => Runner.Config.PeerMode == NetworkProjectConfig.PeerModes.Multiple;
    private bool _isLoading;

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    static void ClearStatics() {
      _allOwnedScenes.Clear();
    }

    static NetworkSceneManagerDefault() {
      SceneManager.sceneUnloaded += (s) => _allOwnedScenes.Remove(s);
    }

    #region INetworkSceneManager

    public virtual void Initialize(NetworkRunner runner) {
      Log.TraceSceneManager(runner, $"Initialize with {runner}");
      
#if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES
      LoadAddressableScenePathsAsync();
#endif

      Debug.Assert(Runner == null);
      Runner = runner;
      
      // assign an empty scene with a separate physics stage immediately, so that they won't spawn anything on the currently active scene
      // an lose track of it
      if (IsMultiplePeer) {
        var scene = SceneManager.CreateScene($"{runner.name}_{runner.LocalPlayer}",
          new CreateSceneParameters(LocalPhysicsMode.Physics2D | LocalPhysicsMode.Physics3D));
        Log.TraceSceneManager(Runner, $"Assigned an initial scene: {scene.Dump()}");

        MultiPeerScene                 = scene;
        MultiPeerDontDestroyOnLoadRoot = new GameObject("[DontDestroyOnLoad]").transform;
        SceneManager.MoveGameObjectToScene(MultiPeerDontDestroyOnLoadRoot.gameObject, MultiPeerScene);
      }
    }

    public virtual void Shutdown() {
      
      Log.TraceSceneManager(Runner, $"Shutdown with {Runner}");
      
      Runner = null;

      // clear owned scenes in case this manager is reused
      var ownedScenes = _allOwnedScenes
                       .Where(x => x.Value == this)
                       .Select(x => x.Key)
                       .ToList();
      
      foreach (var ownedScene in ownedScenes) {
        _allOwnedScenes.Remove(ownedScene);
      }
      
      _multiPeerSceneRoots.Clear();
      _multiPeerActiveRoot = null;
      
      MultiPeerDontDestroyOnLoadRoot = null;

      var sceneToUnload = MultiPeerScene;
      MultiPeerScene = default;
      
      if (sceneToUnload.isLoaded) {
        if (!sceneToUnload.CanBeUnloaded()) {
          SceneManager.CreateScene($"FusionSceneManager_TempEmptyScene");
        }
        SceneManager.UnloadSceneAsync(sceneToUnload);
      }
    }

    public virtual bool IsBusy {
      get {
        if (_isLoading) {
          return true;
        }
        
        if (IsMultiplePeer && _multiPeerSceneRoots.Count == 0) {
          // nothing to spawn on
          return true;
        }

        return false;
      }
    }

    public virtual Scene MainRunnerScene {
      get {
        if (IsMultiplePeer) {
          return MultiPeerScene;
        } else {
          return SceneManager.GetActiveScene();
        }
      }
    }

    public virtual bool IsRunnerScene(Scene scene) {
      if (IsMultiplePeer) {
        return scene == MultiPeerScene;
      } else {
        return true;
      }
    }

    public virtual bool TryGetPhysicsScene2D(out PhysicsScene2D scene2D) {
      var mainScene = MainRunnerScene;
      if (mainScene.IsValid()) {
        scene2D = mainScene.GetPhysicsScene2D();
        return true;
      } else {
        scene2D = default;
        return false;
      }
    }

    public virtual bool TryGetPhysicsScene3D(out PhysicsScene scene3D) {
      var mainScene = MainRunnerScene;
      if (mainScene.IsValid()) {
        scene3D = mainScene.GetPhysicsScene();
        return true;
      } else {
        scene3D = default;
        return false;
      }
    }
    
    public virtual void MakeDontDestroyOnLoad(GameObject obj) {
      if (IsMultiplePeer) {
        Debug.Assert(obj.transform.parent == null || obj.transform.parent == MultiPeerDontDestroyOnLoadRoot);
        obj.transform.SetParent(MultiPeerDontDestroyOnLoadRoot, true);
      } else {
        DontDestroyOnLoad(obj);
      }
    }
    
    public bool MoveGameObjectToScene(GameObject gameObject, SceneRef sceneRef) {
      if (IsMultiplePeer) {
        // find the first matching scene ref
        foreach (var root in _multiPeerSceneRoots) {
          if (sceneRef != default && root.SceneRef != sceneRef) {
            continue;
          }

          if (sceneRef == default) {
            // if scene ref is not specified, use the active root, if it exists
            if (_multiPeerActiveRoot && root != _multiPeerActiveRoot) {
              continue;
            }
          }

          if (gameObject.scene != MultiPeerScene) {
            gameObject.transform.SetParent(null, true);
            SceneManager.MoveGameObjectToScene(gameObject, MultiPeerScene);
            
            if (Application.isBatchMode == false)
              Runner.AddVisibilityNodes(gameObject);
          }
          
          gameObject.transform.SetParent(root.transform, true);
          return true;
        }

        return false;
      } else {
        if (sceneRef == default) {
          // do nothing, all scenes belong to the runner
          return true;
        } 
        
        for (int i = 0; i < SceneManager.sceneCount; ++i) {
          var scene = SceneManager.GetSceneAt(i);
          if (scene.isLoaded && GetSceneRef(scene.path) == sceneRef) {
            SceneManager.MoveGameObjectToScene(gameObject, scene);
            return true;
          }
        }

        return false;
      }
    }

    public virtual NetworkSceneAsyncOp LoadScene(SceneRef sceneRef, NetworkLoadSceneParameters parameters) {
      Log.TraceSceneManager(Runner, $"Load scene {sceneRef} called with parameters: {parameters}");
      return NetworkSceneAsyncOp.FromCoroutine(sceneRef, StartTracedCoroutine(LoadSceneCoroutine(sceneRef, parameters)));
    }
    
    public virtual NetworkSceneAsyncOp UnloadScene(SceneRef sceneRef) {
      Log.TraceSceneManager(Runner, $"Unload scene {sceneRef} called");
      return NetworkSceneAsyncOp.FromCoroutine(sceneRef, StartTracedCoroutine(UnloadSceneCoroutine(sceneRef)));
    }

    public virtual SceneRef GetSceneRef(string sceneNameOrPath) {
      int buildIndex = FusionUnitySceneManagerUtils.GetSceneBuildIndex(sceneNameOrPath);
      if (buildIndex >= 0) {
        return SceneRef.FromIndex(buildIndex);
      }
      
#if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES
      // this may be a blocking call due to WaitForCompletion being used internally
      if (!TryGetAddressableScenes(out var addressableScenes)) {
        Log.Error(this, $"Failed to resolve addressable scene paths, won't be able to resolve {sceneNameOrPath} or any other addressable scene.");
        addressableScenes = Array.Empty<string>();
      }

      var index = FusionUnitySceneManagerUtils.GetSceneIndex(addressableScenes, sceneNameOrPath);
      if (index >= 0) {
        return SceneRef.FromPath(addressableScenes[index]);
      }
#endif

      return SceneRef.None;
    }

    public SceneRef GetSceneRef(GameObject gameObject) {
      if (IsMultiplePeer) {
        if (gameObject.scene != MultiPeerScene) {
          // not a part of this scene
          return default;
        }
        
        // find among scene roots
        var sceneRoot = gameObject.transform.root;
        foreach (var root in _multiPeerSceneRoots) {
          if (root.transform == sceneRoot) {
            return root.SceneRef;
          }
        }

        return default;
      } else {
        var scene = gameObject.scene;
        return GetSceneRef(scene.path);
      }
    }
    
    public bool OnSceneInfoChanged(NetworkSceneInfo sceneInfo, NetworkSceneInfoChangeSource changeSource) {
      // implement this method and return true if you want to handle scene info changes manually
      return false;
    }

    #endregion

    protected virtual IEnumerator LoadSceneCoroutine(SceneRef sceneRef, NetworkLoadSceneParameters sceneParams) {
      Runner.InvokeSceneLoadStart(sceneRef);

      Scene scene = default;

      using (MakeLoadingScope()) {
        Log.TraceSceneManager(Runner, $"LoadSceneCoroutine called with {sceneRef}, {sceneParams}");
        var localPhysicsMode = sceneParams.LocalPhysicsMode;
        var loadSceneMode    = sceneParams.LoadSceneMode;

        if (IsMultiplePeer) {
          if (localPhysicsMode != LocalPhysicsMode.None) {
            throw new ArgumentException($"Local physics mode is not supported in multiple peer mode",
              nameof(sceneParams));
          }

          if (loadSceneMode == LoadSceneMode.Single) {
            // all the current scenes need to be "unloaded", except possibly for the one
            // that matches the sceneRef, if scene take over is enabled
            loadSceneMode = LoadSceneMode.Additive;

            try {
              foreach (var root in _multiPeerSceneRoots) {
                Log.TraceSceneManager(Runner, $"Destroying scene {sceneRef} root {root.name} due to single-mode load");
                Destroy(root.gameObject);
              }

              // wait for each root to be destroyed
              foreach (var root in _multiPeerSceneRoots) {
                while (root != null) {
                  yield return null;
                }
              }
            } finally {
              _multiPeerSceneRoots.Clear();
            }
          }
        }

        if (IsSceneTakeOverEnabled) {
          // check if a loaded scene can be taken over
          Scene candidate = FindSceneToTakeOver(sceneRef);
          if (candidate.IsValid()) {
            Log.TraceSceneManager(Runner, $"Taking over {sceneRef}: {candidate.Dump()}");

            if (candidate.GetLocalPhysicsMode() != localPhysicsMode) {
              throw new InvalidOperationException($"Tried to take over {candidate.Dump()} for {sceneRef}, but physics mode were different: {candidate.GetLocalPhysicsMode()} != {localPhysicsMode}");
            }

            scene = candidate;
            MarkSceneAsOwned(sceneRef, candidate);

            if (loadSceneMode == LoadSceneMode.Single && !IsMultiplePeer) {
              // need to unload scenes manually, multiple peer mode is handled at the beginning of this method, because
              // it always needs to the manual cleanup for single mode
              for (int i = 0; i < SceneManager.sceneCount; i++) {
                var toUnload = SceneManager.GetSceneAt(i);
                if (toUnload != candidate) {
                  Log.TraceSceneManager(Runner, $"Unloading {sceneRef} ({toUnload.Dump()}) due to single-mode take over of {candidate.Dump()}");
                  yield return SceneManager.UnloadSceneAsync(toUnload);
                }
              }
            }
          }
        }

        if (!scene.IsValid()) {
#if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES
          if (loadSceneMode == LoadSceneMode.Single) {
            // single mode unloads all the scenes anyway
            _addressableOperations.Clear();
          }
#endif

          if (sceneRef.IsIndex) {
            Log.TraceSceneManager(Runner, $"Loading scene {sceneRef} with build index {sceneRef.AsIndex} with mode {loadSceneMode}");
            var op = SceneManager.LoadSceneAsync(sceneRef.AsIndex,
              new LoadSceneParameters(loadSceneMode, localPhysicsMode));
            if (op == null) {
              throw new InvalidOperationException($"Scene not found: {sceneRef.AsIndex}");
            }

            Debug.Assert(SceneManager.sceneCount > 0);
            scene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1);
            MarkSceneAsOwned(sceneRef, scene);

            Debug.Assert(scene.buildIndex == sceneRef.AsIndex);

            while (!op.isDone) {
              OnLoadSceneProgress(sceneRef, op.progress);
              yield return null;
            }
          } else {
#if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES
            if (!TryGetAddressableScenes(out var addressableScenes)) {
              Log.Error(this, $"Failed to resolve addressable scene paths, won't be able to resolve {sceneRef}");
              addressableScenes = Array.Empty<string>();
            }

            string sceneAddress = null;
            foreach (var path in addressableScenes) {
              if (sceneRef.IsPath(path)) {
                sceneAddress = path;
                break;
              }
            }
            
            if (sceneAddress == null) {
              throw new InvalidOperationException($"Unable to find addressable scene path for {sceneRef}");
            }

            Log.TraceSceneManager(Runner, $"Loading scene {sceneRef} from addressable: {sceneAddress}");

#if FUSION_ENABLE_ADDRESSABLES_LOCAL_PHYSICS
            var loadSceneParameters = new LoadSceneParameters(loadSceneMode, localPhysicsMode);
#else
            if (localPhysicsMode != LocalPhysicsMode.None) {
              throw new InvalidOperationException($"{nameof(LocalPhysicsMode)} is not supported in this version of Addressables");
            }
            var loadSceneParameters = loadSceneMode;
#endif
            var op = Addressables.LoadSceneAsync(sceneAddress, loadSceneParameters);

            // to get the scene a callback is used, as it fires immediately when loading finished,
            // compared to waiting for the coroutine to resume
            scene = default;
            op.Completed += op => {
              if (op.Status == AsyncOperationStatus.Succeeded) {
                scene = op.Result.Scene;
                MarkSceneAsOwned(sceneRef, scene);
              }
            };

            op.Destroyed += _ => {
              // this will happen in MP mode when scenes are merged or when a scene is loaded in a single mode
              if (_addressableOperations.Remove(sceneRef)) {
                Log.TraceSceneManager(Runner, $"Destroyed Addressables op for {sceneRef}");
              }
            };

            _addressableOperations.Add(sceneRef, op);

            while (!op.IsDone) {
              OnLoadSceneProgress(sceneRef, op.PercentComplete);
              yield return null;
            }

            if (!op.IsValid()) {
              throw new InvalidOperationException($"Loading operation for {sceneRef} has been destroyed");
            }

            if (op.Status == AsyncOperationStatus.Failed) {
              Addressables.Release(op);
              throw new InvalidOperationException($"Failed to load scene from addressable: {sceneAddress}");
            }
#else
            throw new InvalidOperationException($"SceneRef {sceneRef} points to an addressable scene, but FUSION_ENABLE_ADDRESSABLES is not defined");
#endif
          }
        }
      }

      yield return StartCoroutine(OnSceneLoaded(sceneRef, scene, sceneParams));
    }

    protected virtual IEnumerator UnloadSceneCoroutine(SceneRef sceneRef) {
      Log.TraceSceneManager(Runner, $"UnloadSceneCoroutine called for {sceneRef}");

      using (MakeLoadingScope()) {
        if (IsMultiplePeer) {
          // in multiple peer, the unload simply destroys the scene root
          for (int i = 0; i < _multiPeerSceneRoots.Count; ++i) {
            var root = _multiPeerSceneRoots[i];
            if (root.SceneRef == sceneRef) {

              if (root == _multiPeerActiveRoot) {
                _multiPeerActiveRoot = null;
              }
              
              _multiPeerSceneRoots.RemoveAt(i);
              Log.TraceSceneManager(Runner, $"Destroying scene root {root.name} for {sceneRef}");

              Log.TraceSceneManager(Runner, $"Started unloading {root.Scene.ToString()} for {sceneRef}");
              Destroy(root.gameObject);
              while (root != null) {
                yield return null;
              }

              Log.TraceSceneManager(Runner, $"Finished unloading {root.Scene.ToString()} for {sceneRef}");
              yield break;
            }
          }

          throw new ArgumentOutOfRangeException($"Did not find a scene to unload: {sceneRef}", nameof(sceneRef));
        } else {
          Scene sceneToUnload = default;

          // find the scene to unload
          for (int i = 0; i < SceneManager.sceneCount; ++i) {
            var scene = SceneManager.GetSceneAt(i);
            if (GetSceneRef(scene.path) == sceneRef) {
              sceneToUnload = scene;
              break;
            }
          }

          if (!sceneToUnload.IsValid()) {
            throw new ArgumentOutOfRangeException($"Did not find a scene to unload: {sceneRef}", nameof(sceneRef));
          }

          Log.TraceSceneManager(Runner, $"Started unloading {sceneToUnload.Dump()} for {sceneRef}");

          if (!sceneToUnload.CanBeUnloaded()) {
            Log.Warn(Runner, $"Scene {sceneToUnload.Dump()} can't be unloaded for {sceneRef}, creating a temporary scene to unload it");
            Debug.Assert(!_tempUnloadScene.IsValid());
            _tempUnloadScene = SceneManager.CreateScene($"FusionSceneManager_TempEmptyScene");
          }

#if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES
          if (_addressableOperations.TryGetValue(sceneRef, out var asyncOp)) {
            Log.TraceSceneManager(Runner, $"Unloading addressable scene {sceneToUnload.Dump()} for {sceneRef}");
            yield return Addressables.UnloadSceneAsync(asyncOp);
          } else
#endif
          {
            Log.TraceSceneManager(Runner, $"Unloading {sceneToUnload.Dump()} for {sceneRef}");
            var op = SceneManager.UnloadSceneAsync(sceneToUnload);
            if (op == null) {
              throw new InvalidOperationException($"Failed to unload {sceneToUnload.Dump()}");
            }

            yield return op;
          }

          Log.TraceSceneManager(Runner, $"Finished unloading {sceneToUnload.Dump()} for {sceneRef}");
        }
      }
    }

    protected virtual IEnumerator OnSceneLoaded(SceneRef sceneRef, Scene scene, NetworkLoadSceneParameters sceneParams) {
      Log.TraceSceneManager(Runner, $"Finished loading, starting processing {scene.Dump()} for {sceneRef}");

      var sceneObjects = scene.GetComponents<NetworkObject>(includeInactive: true, out var rootObjects);

      // since it is impossible to get objects in deterministic order (sibling index is 0 for all root objects in builds),
      // scene objects need to be sorted by something that will guarantee the order
      Array.Sort(sceneObjects, NetworkObjectSortKeyComparer.Instance);

      if (IsMultiplePeer) {
        // create a root GO for all the gameObjects in the newly loaded scene
        var newSceneRoot = new GameObject($"[{scene.name}]").AddComponent<MultiPeerSceneRoot>();
        newSceneRoot.SceneRef    = sceneRef;
        newSceneRoot.SceneHandle = scene.handle;
        newSceneRoot.Scene       = scene;
        newSceneRoot.ScenePath   = scene.path;

        SceneManager.MoveGameObjectToScene(newSceneRoot.gameObject, scene);

        foreach (var rootGameObject in rootObjects) {
          rootGameObject.transform.SetParent(newSceneRoot.transform, true);
        }

        // store the info
        _multiPeerSceneRoots.Add(newSceneRoot);

        Log.TraceSceneManager(Runner, $"Merging {scene.Dump()} to {MultiPeerScene.Dump()} for {sceneRef}");
        SceneManager.MergeScenes(scene, MultiPeerScene);

        if (sceneParams.IsActiveOnLoad) {
          _multiPeerActiveRoot = newSceneRoot;
        }
      } else {
        if (sceneParams.IsActiveOnLoad) {
          SceneManager.SetActiveScene(scene);
        }
      }
      
      // register scene objects; this will deactivate GameObjects for clients
      // the additional loadId parameter is passed to ensure each scene load
      // yields unique type ids for scene objects
      Runner.RegisterSceneObjects(sceneRef, sceneObjects, loadId: sceneParams.LoadId);
      
      Log.TraceSceneManager(Runner, $"Finished loading & processing {scene.Dump()} for {sceneRef}");
      Runner.InvokeSceneLoadDone(new SceneLoadDoneArgs(sceneRef, sceneObjects, scene, rootObjects));
      yield break;
    }

    protected virtual void OnLoadSceneProgress(SceneRef sceneRef, float progress) {
      Log.TraceSceneManager(Runner, $"Loading scene progress {sceneRef} ({progress:P2})");
    }

    private Scene FindSceneToTakeOver(SceneRef sceneRef) {
      for (int i = 0; i < SceneManager.sceneCount; ++i) {
        var candidate = SceneManager.GetSceneAt(i);
        if (!candidate.isLoaded) {
          continue;
        }

        if (GetSceneRef(candidate.path) != sceneRef) {
          continue;
        }

        if (_allOwnedScenes.ContainsKey(candidate)) {
          continue;
        }

        return candidate;
      }

      return default;
    }

    private ICoroutine StartTracedCoroutine(IEnumerator inner) {
      var coro = new FusionCoroutine(inner);

      _runningCoroutines.Add(coro);

      coro.Completed += x => {

        if (LogSceneLoadErrors && x.Error != null) {
          Log.Error(Runner, $"Failed async op: {x.Error.SourceException}");
        }
        
        // remove this one from the list
        var index = _runningCoroutines.IndexOf((ICoroutine)x);
        Debug.Assert(index == 0, "Expected the completed coroutine to be the first in the list");
        _runningCoroutines.RemoveAt(index);

        // start the next one
        if (index < _runningCoroutines.Count) {
          Log.TraceSceneManager(Runner, $"Starting enqueued coroutine {index} of {_runningCoroutines.Count}");
          StartCoroutine(_runningCoroutines[index]);
        }
      };

      if (_runningCoroutines.Count == 1) {
        // start immediately
        StartCoroutine(coro);
      } else {
        Log.TraceSceneManager(Runner, $"Enqueued coroutine, there are already {_runningCoroutines.Count - 1} running");
      }

      return coro;
    }

    protected LoadingScope MakeLoadingScope() {
      return new LoadingScope(this);
    }

    protected void MarkSceneAsOwned(SceneRef sceneRef, Scene scene) {
      if (_allOwnedScenes.TryGetValue(scene, out var manager)) {
        Log.Warn(Runner, $"Scene {scene.Dump()} (for {sceneRef}) already owned by {manager}");
      } else {
        _allOwnedScenes.Add(scene, this);
      }
    }

    private NetworkSceneAsyncOp FailOp(SceneRef sceneRef, Exception exception) {
      if (LogSceneLoadErrors) {
        Log.Error(Runner, $"Failed with: {exception}");
      }

      return NetworkSceneAsyncOp.FromError(sceneRef, exception);
    }

#if FUSION_ENABLE_ADDRESSABLES && !FUSION_DISABLE_ADDRESSABLES
    /// <summary>
    /// A label by which addressable scenes can be discovered.
    /// </summary>
    [InlineHelp]
    public string AddressableScenesLabel = "FusionScenes";
    
    public NetworkSceneManagerDefault() {
      _addressableScenesTask = new(() => GetAddressableScenes());
    }
    
    public Task LoadAddressableScenePathsAsync() {
      return _addressableScenesTask.Value.Task;
    }
    
    /// <summary>
    /// Creates a task that resolves addressable scene paths. By default, this method locates all the addressable scenes with
    /// <see cref="AddressableScenesLabel"/> label. Override this method to provide a custom implementation. For example, user
    /// might want to have a pre-defined set of addressable scenes to avoid the wait:
    /// <example><code>
    /// protected override GetAddressableScenesResult GetAddressableScenes() {
    ///   return Task.FromResult(new string[] {
    ///     "Assets/Scenes/AddressableScene1.unity",
    ///     "Assets/Scenes/AddressableScene2.unity",
    ///   });
    /// }
    /// </code></example>
    /// </summary>
    /// <returns>A task representing resolve operation and optionally a delegate to be invoked before the task is going to be
    /// awaited synchronously</returns>
    protected virtual GetAddressableScenesResult GetAddressableScenes() {
      Log.TraceSceneManager(Runner, $"Locating addressable scenes with label: {AddressableScenesLabel}");
      
      var tcs    = new TaskCompletionSource<string[]>();
      var result = Addressables.LoadResourceLocationsAsync(AddressableScenesLabel, typeof(SceneInstance));
        
      result.Completed += op => {
        try {
          if (op.Status == AsyncOperationStatus.Failed) {
            tcs.SetException(op.OperationException);
          } else {
            var paths = op.Result.Select(x => x.PrimaryKey).ToArray();
            Log.TraceSceneManager(Runner, $"Found {paths.Length} addressable scenes: {string.Join(", ", paths)}");
            tcs.SetResult(paths);
          }
        } finally {
          Addressables.Release(op);
        }
      };
      
      return new GetAddressableScenesResult {
        Task = tcs.Task,
        
        // awaiting tasks synchronously does not play well with addressables; simply waiting will block the main thread and that's it.
        // addressables *need* to have WaitForCompletion called
        BeforeWaitForCompletion = () => {
          if (result.IsValid()) {
            result.WaitForCompletion();
          }
        },
      };
    }

    /// <summary>
    /// Returns the timeout for addressable scene paths to be resolved. By default, this method returns 10 seconds.
    /// </summary>
    /// <returns></returns>
    protected virtual TimeSpan GetAddressableScenePathsTimeout() {
      return TimeSpan.FromSeconds(10);
    }
    
    private bool TryGetAddressableScenes(out string[] addressableScenes) {
      if (!_addressableScenesTask.IsValueCreated) {
        Log.Warn(Runner, $"Going to block the thread in wait for addressable scene paths being resolved, call and await {nameof(LoadAddressableScenePathsAsync)} to avoid this.");
      }

      var t = _addressableScenesTask.Value;
      if (!t.Task.IsCompleted) {
        t.BeforeWaitForCompletion?.Invoke();
        
        if (!t.Task.Wait(GetAddressableScenePathsTimeout())) {
          addressableScenes = null;
          return false;
        }
      }

      addressableScenes = t.Task.Result;
      return true;
    }

    protected struct GetAddressableScenesResult {
      public Task<string[]> Task;
      public Action         BeforeWaitForCompletion;
      public static implicit operator GetAddressableScenesResult(Task<string[]> task) {
        return new GetAddressableScenesResult {
          Task = task,
        };
      }
    }

    private Lazy<GetAddressableScenesResult>                          _addressableScenesTask;
    private Dictionary<SceneRef, AsyncOperationHandle<SceneInstance>> _addressableOperations = new();
#endif

    protected sealed class MultiPeerSceneRoot : MonoBehaviour {
      public SceneRef SceneRef;
      public string   ScenePath;
      public int      SceneHandle;
      public Scene    Scene;
    }

    protected struct LoadingScope : IDisposable {
      private readonly NetworkSceneManagerDefault _manager;

      public LoadingScope(NetworkSceneManagerDefault manager) {
        _manager            = manager;
        _manager._isLoading = true;
        Log.TraceSceneManager(manager.Runner, "Loading scope started");
      }

      public void Dispose() {
        _manager._isLoading = false;
        Log.TraceSceneManager(_manager.Runner, "Loading scope ended");
      }
    }
  }
}