import "shaka-player/dist/shaka-player.compiled.d.ts";
import {
  HlsManifestParser,
  DashManifestParser,
} from "./manifest-parser-decorator.js";
import { SegmentManager } from "./segment-manager.js";
import {
  StreamInfo,
  Shaka,
  Stream,
  HookedNetworkingEngine,
  HookedRequest,
  P2PMLShakaData,
} from "./types.js";
import { Loader } from "./loading-handler.js";
import {
  CoreConfig,
  Core,
  CoreEventMap,
  DynamicCoreConfig,
  DefinedCoreConfig,
} from "p2p-media-loader-core";

/** Type for specifying dynamic configuration options that can be changed at runtime for the P2P engine's core. */
export type DynamicShakaP2PEngineConfig = {
  /** Dynamic core config */
  core?: DynamicCoreConfig;
};

/** Represents the complete configuration for ShakaP2PEngine. */
export type ShakaP2PEngineConfig = {
  /** Complete core configuration settings. */
  core: DefinedCoreConfig;
};

/** Allows for partial configuration settings for the Shaka P2P Engine. */
export type PartialShakaEngineConfig = Partial<
  Omit<ShakaP2PEngineConfig, "core">
> & {
  /** Partial core config */
  core?: Partial<CoreConfig>;
};

const LIVE_EDGE_DELAY = 25;

/**
 * Represents a P2P (peer-to-peer) engine for HLS (HTTP Live Streaming) to enhance media streaming efficiency.
 * This class integrates P2P technologies into Shaka Player, enabling the distribution of media segments via a peer network
 * alongside traditional HTTP fetching. It reduces server bandwidth costs and improves scalability by sharing the load
 * across multiple clients.
 *
 * The engine manages core functionalities such as segment fetching, segment management, peer connection management,
 * and event handling related to the P2P and HLS processes.
 *
 * @example
 * // Initializing the ShakaP2PEngine with custom configuration
 * const shakaP2PEngine = new ShakaP2PEngine({
 *   core: {
 *     highDemandTimeWindow: 30, // 30 seconds
 *     simultaneousHttpDownloads: 3,
 *     webRtcMaxMessageSize: 64 * 1024, // 64 KB
 *     p2pNotReceivingBytesTimeoutMs: 10000, // 10 seconds
 *     p2pInactiveLoaderDestroyTimeoutMs: 15000, // 15 seconds
 *     httpNotReceivingBytesTimeoutMs: 8000, // 8 seconds
 *     httpErrorRetries: 2,
 *     p2pErrorRetries: 2,
 *     announceTrackers: ["wss://personal.tracker.com"],
 *     rtcConfig: {
 *       iceServers: [{ urls: "stun:personal.stun.com" }]
 *     },
 *     swarmId: "example-swarm-id"
 *   }
 * });
 */
export class ShakaP2PEngine {
  private player?: shaka.Player;
  private readonly shaka: Shaka;
  private readonly streamInfo: StreamInfo = {};
  private readonly core: Core<Stream>;
  private readonly segmentManager: SegmentManager;
  private requestFilter?: shaka.extern.RequestFilter;

  /**
   * Constructs an instance of ShakaP2PEngine.
   *
   * @param config Optional configuration for customizing the P2P engine's behavior.
   * @param shaka The Shaka Player library instance.
   */
  constructor(config?: PartialShakaEngineConfig, shaka = window.shaka) {
    validateShaka(shaka);

    this.shaka = shaka;
    this.core = new Core(config?.core);
    this.segmentManager = new SegmentManager(this.streamInfo, this.core);
  }

  /**
   * Configures and initializes the Shaka Player instance with predefined settings for optimal P2P performance.
   *
   * @param player The Shaka Player instance to configure.
   */
  bindShakaPlayer(player: shaka.Player) {
    if (this.player === player) return;
    if (this.player) this.destroy();

    this.player = player;
    this.player.configure("manifest.defaultPresentationDelay", LIVE_EDGE_DELAY);
    this.player.configure(
      "manifest.dash.ignoreSuggestedPresentationDelay",
      true,
    );
    this.player.configure("streaming.useNativeHlsOnSafari", false);

    this.updatePlayerEventHandlers("register");
  }

  /**
   * Applies dynamic configuration updates to the P2P engine.
   *
   * @param dynamicConfig Configuration changes to apply.
   *
   * @example
   * // Assuming `shakaP2PEngine` is an instance of ShakaP2PEngine
   *
   * const newDynamicConfig = {
   *   core: {
   *     // Increase the number of cached segments to 1000
   *     cachedSegmentsCount: 1000,
   *     // 50 minutes of segments will be downloaded further through HTTP connections if P2P fails
   *     httpDownloadTimeWindow: 3000,
   *     // 100 minutes of segments will be downloaded further through P2P connections
   *     p2pDownloadTimeWindow: 6000,
   * };
   *
   * shakaP2PEngine.applyDynamicConfig(newDynamicConfig);
   */
  applyDynamicConfig(dynamicConfig: DynamicShakaP2PEngineConfig) {
    if (dynamicConfig.core) this.core.applyDynamicConfig(dynamicConfig.core);
  }

  /**
   * Retrieves the current configuration of the ShakaP2PEngine.
   *
   * @returns The configuration as a readonly object.
   */
  getConfig(): ShakaP2PEngineConfig {
    return { core: this.core.getConfig() };
  }

  /**
   * Adds an event listener for the specified event.
   * @param eventName The name of the event to listen for.
   * @param listener The callback function to be invoked when the event is triggered.
   *
   * @example
   * // Listening for a segment being successfully loaded
   * shakaP2PEngine.addEventListener('onSegmentLoaded', (details) => {
   *   console.log('Segment Loaded:', details);
   * });
   *
   * @example
   * // Handling segment load errors
   * shakaP2PEngine.addEventListener('onSegmentError', (errorDetails) => {
   *   console.error('Error loading segment:', errorDetails);
   * });
   *
   * @example
   * // Tracking data downloaded from peers
   * shakaP2PEngine.addEventListener('onChunkDownloaded', (bytesLength, downloadSource, peerId) => {
   *   console.log(`Downloaded ${bytesLength} bytes from ${downloadSource} ${peerId ? 'from peer ' + peerId : 'from server'}`);
   * });
   */
  addEventListener<K extends keyof CoreEventMap>(
    eventName: K,
    listener: CoreEventMap[K],
  ) {
    this.core.addEventListener(eventName, listener);
  }

  /**
   * Removes an event listener for the specified event.
   * @param eventName The name of the event.
   * @param listener The callback function that was previously added.
   */
  removeEventListener<K extends keyof CoreEventMap>(
    eventName: K,
    listener: CoreEventMap[K],
  ) {
    this.core.removeEventListener(eventName, listener);
  }

  private updatePlayerEventHandlers = (type: "register" | "unregister") => {
    const { player } = this;
    if (!player) return;

    const networkingEngine =
      player.getNetworkingEngine() as HookedNetworkingEngine | null;
    if (networkingEngine) {
      if (type === "register") {
        const p2pml: P2PMLShakaData = {
          player,
          shaka: this.shaka,
          core: this.core,
          streamInfo: this.streamInfo,
          segmentManager: this.segmentManager,
        };
        this.requestFilter = (requestType, request) => {
          (request as HookedRequest).p2pml = p2pml;
        };
        networkingEngine.p2pml = p2pml;
        networkingEngine.registerRequestFilter(this.requestFilter);
      } else {
        networkingEngine.p2pml = undefined;
        if (this.requestFilter) {
          networkingEngine.unregisterRequestFilter(this.requestFilter);
        }
      }
    }
    const method =
      type === "register" ? "addEventListener" : "removeEventListener";
    player[method]("loaded", this.handlePlayerLoaded);
    player[method]("loading", this.destroyCurrentStreamContext);
    player[method]("unloading", this.handlePlayerUnloading);
    player[method]("adaptation", this.onVariantChanged);
    player[method]("variantchanged", this.onVariantChanged);
  };

  private onVariantChanged = () => {
    if (!this.player) return;
    const activeTrack = this.player
      .getVariantTracks()
      .find((track) => track.active);

    if (!activeTrack) return;
    this.core.setActiveLevelBitrate(activeTrack.bandwidth);
  };

  private handlePlayerLoaded = () => {
    if (!this.player) return;
    this.core.setIsLive(this.player.isLive());
    this.updateMediaElementEventHandlers("register");
  };

  private handlePlayerUnloading = () => {
    this.destroyCurrentStreamContext();
    this.updateMediaElementEventHandlers("unregister");
  };

  private destroyCurrentStreamContext = () => {
    this.streamInfo.protocol = undefined;
    this.streamInfo.manifestResponseUrl = undefined;
    this.core.destroy();
  };

  private updateMediaElementEventHandlers = (
    type: "register" | "unregister",
  ) => {
    const media = this.player?.getMediaElement();
    if (!media) return;
    const method =
      type === "register" ? "addEventListener" : "removeEventListener";
    media[method]("timeupdate", this.handlePlaybackUpdate);
    media[method]("ratechange", this.handlePlaybackUpdate);
    media[method]("seeking", this.handlePlaybackUpdate);
  };

  private handlePlaybackUpdate = (event: Event) => {
    const media = event.target as HTMLVideoElement;
    this.core.updatePlayback(media.currentTime, media.playbackRate);
  };

  /** Clean up and release all resources. Unregister all event handlers. */
  destroy() {
    this.destroyCurrentStreamContext();
    this.updatePlayerEventHandlers("unregister");
    this.updateMediaElementEventHandlers("unregister");
    this.player = undefined;
  }

  private static registerManifestParsers(shaka: Shaka) {
    const hlsParserFactory = () => new HlsManifestParser(shaka);
    const dashParserFactory = () => new DashManifestParser(shaka);

    const Parser = shaka.media.ManifestParser;
    Parser.registerParserByMime("application/dash+xml", dashParserFactory);
    Parser.registerParserByMime("application/x-mpegurl", hlsParserFactory);
    Parser.registerParserByMime(
      "application/vnd.apple.mpegurl",
      hlsParserFactory,
    );
  }

  private static unregisterManifestParsers(shaka: Shaka) {
    const Parser = shaka.media.ManifestParser;
    Parser.unregisterParserByMime("mpd");
    Parser.unregisterParserByMime("application/dash+xml");
    Parser.unregisterParserByMime("m3u8");
    Parser.unregisterParserByMime("application/x-mpegurl");
    Parser.unregisterParserByMime("application/vnd.apple.mpegurl");
  }

  private static registerNetworkingEngineSchemes(shaka: Shaka) {
    const { NetworkingEngine } = shaka.net;

    const handleLoading: shaka.extern.SchemePlugin = (...args) => {
      const request = args[1] as HookedRequest;
      const { p2pml } = request;
      if (!p2pml) {
        return shaka.net.HttpFetchPlugin.parse(
          ...args,
        ) as shaka.extern.IAbortableOperation<shaka.extern.Response>;
      }

      const loader = new Loader(p2pml.shaka, p2pml.core, p2pml.streamInfo);
      return loader.load(...args);
    };
    NetworkingEngine.registerScheme("http", handleLoading);
    NetworkingEngine.registerScheme("https", handleLoading);
    NetworkingEngine.registerScheme("data", handleLoading);
  }

  private static unregisterNetworkingEngineSchemes(shaka: Shaka) {
    const { NetworkingEngine } = shaka.net;
    NetworkingEngine.unregisterScheme("http");
    NetworkingEngine.unregisterScheme("https");
    NetworkingEngine.unregisterScheme("data");
  }

  /**
   * Registers plugins related to P2P functionality into the Shaka Player.
   * Plugins must be registered before initializing the player to ensure proper integration.
   *
   * @param shaka - The Shaka Player library. Defaults to the global Shaka Player instance if not provided.
   */
  static registerPlugins(shaka = window.shaka) {
    validateShaka(shaka);

    ShakaP2PEngine.registerManifestParsers(shaka);
    ShakaP2PEngine.registerNetworkingEngineSchemes(shaka);
  }

  /**
   * Unregister plugins related to P2P functionality from the Shaka Player.
   *
   * @param shaka - The Shaka Player library. Defaults to the global Shaka Player instance if not provided.
   */
  static unregisterPlugins(shaka = window.shaka) {
    validateShaka(shaka);

    ShakaP2PEngine.unregisterManifestParsers(shaka);
    ShakaP2PEngine.unregisterNetworkingEngineSchemes(shaka);
  }
}

function validateShaka(shaka: unknown) {
  if (!shaka) {
    throw new Error(
      "shaka namespace is not defined in global scope and not passed as an argument to Shaka P2P engine constructor",
    );
  }
}
