/// High-level Flutter facade for discovering game data and sharing runtime services.
library;

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:wolf_3d_dart/wolf_3d_data.dart';
import 'package:wolf_3d_dart/wolf_3d_data_types.dart';
import 'package:wolf_3d_dart/wolf_3d_engine.dart';
import 'package:wolf_3d_dart/wolf_3d_synth.dart';
import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart';

/// Coordinates asset discovery, audio initialization, and input reuse for apps.
class Wolf3d {
  /// Creates an empty facade that must be initialized with [init].
  Wolf3d();

  /// All successfully discovered or bundled game data sets.
  final List<WolfensteinData> availableGames = [];
  WolfensteinData? _activeGame;

  /// Shared engine audio backend used by menus and gameplay sessions.
  final EngineAudio audio = WolfAudio();

  /// Engine menu background color as 24-bit RGB.
  int menuBackgroundRgb = 0x890000;

  /// Engine menu panel color as 24-bit RGB.
  int menuPanelRgb = 0x590002;

  /// Shared Flutter input adapter reused by gameplay screens.
  final Wolf3dFlutterInput input = Wolf3dFlutterInput();

  /// The currently selected game data set.
  ///
  /// Throws a [StateError] until [setActiveGame] has been called.
  WolfensteinData get activeGame {
    if (_activeGame == null) {
      throw StateError("No active game selected. Call setActiveGame() first.");
    }
    return _activeGame!;
  }

  /// Nullable access to the selected game, useful during menu bootstrap.
  WolfensteinData? get maybeActiveGame => _activeGame;

  // Episode selection lives on the facade so menus can configure gameplay
  // before constructing a new engine instance.
  int? _activeEpisode;

  /// Index of the episode currently selected in the UI flow.
  int? get activeEpisode => _activeEpisode;

  Difficulty? _activeDifficulty;

  /// The difficulty applied when [launchEngine] creates a new session.
  Difficulty? get activeDifficulty => _activeDifficulty;

  /// Stores [difficulty] so the next [launchEngine] call uses it.
  void setActiveDifficulty(Difficulty difficulty) {
    _activeDifficulty = difficulty;
  }

  /// Clears any previously selected difficulty so the engine can prompt for one.
  void clearActiveDifficulty() {
    _activeDifficulty = null;
  }

  WolfEngine? _engine;

  /// The most recently launched engine.
  ///
  /// Throws a [StateError] until [launchEngine] has been called.
  WolfEngine get engine {
    if (_engine == null) {
      throw StateError('No engine launched. Call launchEngine() first.');
    }
    return _engine!;
  }

  /// Creates and initializes a [WolfEngine] for the current session config.
  ///
  /// Uses [activeGame], [activeEpisode], and [activeDifficulty]. Stores the
  /// engine so it can be retrieved via [engine]. [onGameWon] is invoked when
  /// the player completes the final level of the episode.
  WolfEngine launchEngine({
    required void Function() onGameWon,
    void Function()? onQuit,
    WolfRendererCapabilities? rendererCapabilities,
    WolfRendererSettings? rendererSettings,
    void Function(WolfRendererSettings settings)? onRendererSettingsChanged,
  }) {
    if (availableGames.isEmpty) {
      throw StateError(
        'No game data was discovered. Add game files before launching the engine.',
      );
    }

    _engine = WolfEngine(
      availableGames: availableGames,
      difficulty: _activeDifficulty,
      startingEpisode: _activeEpisode,
      frameBuffer: FrameBuffer(320, 200),
      menuBackgroundRgb: menuBackgroundRgb,
      menuPanelRgb: menuPanelRgb,
      engineAudio: audio,
      input: input,
      onGameWon: onGameWon,
      // In Flutter we keep the renderer screen active while browsing menus,
      // so backing out of the top-level menu should not pop the route.
      onMenuExit: () {},
      onQuit: onQuit,
      rendererCapabilities: rendererCapabilities,
      rendererSettings: rendererSettings,
      onRendererSettingsChanged: onRendererSettingsChanged,
      onGameSelected: (game) {
        _activeGame = game;
        audio.activeGame = game;
      },
      onEpisodeSelected: (episodeIndex) {
        _activeEpisode = episodeIndex;
      },
    );
    _engine!.init();
    return _engine!;
  }

  /// Sets the active episode for the current [activeGame].
  void setActiveEpisode(int episodeIndex) {
    if (_activeGame == null) {
      throw StateError("No active game selected. Call setActiveGame() first.");
    }
    if (episodeIndex < 0 || episodeIndex >= _activeGame!.episodes.length) {
      throw RangeError("Episode index out of range for the active game.");
    }

    _activeEpisode = episodeIndex;
  }

  /// Clears any selected episode so menu flow starts fresh.
  void clearActiveEpisode() {
    _activeEpisode = null;
  }

  /// Convenience access to the active episode's level list.
  List<WolfLevel> get levels {
    if (_activeEpisode == null) {
      throw StateError('No active episode selected.');
    }
    return activeGame.episodes[_activeEpisode!].levels;
  }

  /// Convenience access to the active game's wall textures.
  List<Sprite> get walls => activeGame.walls;

  /// Convenience access to the active game's sprite set.
  List<Sprite> get sprites => activeGame.sprites;

  /// Convenience access to digitized PCM effects.
  List<PcmSound> get sounds => activeGame.sounds;

  /// Convenience access to AdLib/OPL effect assets.
  List<PcmSound> get adLibSounds => activeGame.adLibSounds;

  /// Convenience access to level music tracks.
  List<ImfMusic> get music => activeGame.music;

  /// Convenience access to VGA UI and splash images.
  List<VgaImage> get vgaImages => activeGame.vgaImages;

  /// Makes [game] the active data set and points shared services at it.
  void setActiveGame(WolfensteinData game) {
    if (!availableGames.contains(game)) {
      throw ArgumentError(
        "The provided game data is not in the list of available games.",
      );
    }

    if (_activeGame == game) {
      return; // No change needed
    }

    _activeGame = game;
    _activeEpisode = null;
    audio.activeGame = game;
  }

  /// Initializes the engine by loading available game data.
  Future<Wolf3d> init({String? directory}) async {
    await audio.init();
    availableGames.clear();

    // Bundled assets let the GUI work out of the box on supported platforms.
    final versionsToTry = [
      (version: GameVersion.retail, path: 'retail'),
      (version: GameVersion.shareware, path: 'shareware'),
    ];

    for (final version in versionsToTry) {
      try {
        final ext = version.version.fileExtension;
        final folder = 'packages/wolf_3d_assets/assets/${version.path}';

        final data = WolfensteinLoader.loadFromBytes(
          version: version.version,
          vswap: await _tryLoad('$folder/VSWAP.$ext'),
          mapHead: await _tryLoad('$folder/MAPHEAD.$ext'),
          gameMaps: await _tryLoad('$folder/GAMEMAPS.$ext'),
          vgaDict: await _tryLoad('$folder/VGADICT.$ext'),
          vgaHead: await _tryLoad('$folder/VGAHEAD.$ext'),
          vgaGraph: await _tryLoad('$folder/VGAGRAPH.$ext'),
          audioHed: await _tryLoad('$folder/AUDIOHED.$ext'),
          audioT: await _tryLoad('$folder/AUDIOT.$ext'),
        );

        availableGames.add(data);
      } catch (e) {
        debugPrint(e.toString());
      }
    }

    // On non-web platforms, also scan the local filesystem for user-supplied
    // data folders so the host can pick up extra versions automatically.
    if (!kIsWeb) {
      try {
        final externalGames = await WolfensteinLoader.discover(
          directoryPath: directory,
          recursive: true,
        );
        for (var entry in externalGames.entries) {
          if (!availableGames.any((g) => g.version == entry.key)) {
            availableGames.add(entry.value);
          }
        }
      } catch (e) {
        debugPrint("External discovery failed: $e");
      }
    }

    return this;
  }

  /// Loads an asset from the Flutter bundle, returning `null` when absent.
  Future<ByteData?> _tryLoad(String path) async {
    try {
      return await rootBundle.load(path);
    } catch (e) {
      debugPrint("Asset not found: $path");
      return null;
    }
  }
}
