import {
  Color4,
  EventType,
  StoryboardSample,
  StoryboardAnimation,
  Vector2,
  StoryboardSprite,
  LayerType,
  StoryboardVideo,
  CompoundType,
  CommandType,
  ParameterType,
  BlendingParameters,
  Origins,
  Anchor,
  LoopType,
  BeatmapBreakEvent,
  SampleSet,
  HitObject,
  SliderPath,
  CommandLoop,
  CommandTrigger,
  HitType,
  HitSound,
  PathType,
  EffectType,
  TimeSignature,
  ControlPointType,
  ReplayButtonState,
  LegacyReplayFrame,
  Storyboard,
  SampleBank,
  PathPoint,
  HitSample,
  TimingPoint,
  DifficultyPoint,
  EffectPoint,
  SamplePoint,
  LifeBarFrame,
  Beatmap,
  ScoreInfo,
  Replay,
  Score,
} from "osu-classes";
import { decompress, compress } from "lzma-js-simple-v2";

class Parsing {
  static parseInt(input, parseLimit = this.MAX_PARSE_VALUE, allowNaN = false) {
    return this._getValue(parseInt(input), parseLimit, allowNaN);
  }
  static parseFloat(
    input,
    parseLimit = this.MAX_PARSE_VALUE,
    allowNaN = false
  ) {
    return this._getValue(parseFloat(input), parseLimit, allowNaN);
  }
  static parseEnum(enumObj, input) {
    const value = input.trim();
    const rawValue = parseInt(value);

    if (rawValue in enumObj) {
      return rawValue;
    }

    if (value in enumObj) {
      return enumObj[value];
    }

    throw new Error("Unknown enum value!");
  }
  static parseByte(input) {
    const value = parseInt(input);

    if (value < 0) {
      throw new Error("Value must be greater than 0!");
    }

    if (value > 255) {
      throw new Error("Value must be less than 255!");
    }

    return this._getValue(value);
  }
  static _getValue(value, parseLimit = this.MAX_PARSE_VALUE, allowNaN = false) {
    if (value < -parseLimit) {
      throw new Error("Value is too low!");
    }

    if (value > parseLimit) {
      throw new Error("Value is too high!");
    }

    if (!allowNaN && Number.isNaN(value)) {
      throw new Error("Not a number");
    }

    return value;
  }
}
Parsing.MAX_COORDINATE_VALUE = 131072;
Parsing.MAX_PARSE_VALUE = 2147483647;

class BeatmapColorDecoder {
  static handleLine(line, output) {
    const [key, ...values] = line.split(":");
    const rgba = values
      .join(":")
      .trim()
      .split(",")
      .map((c) => Parsing.parseByte(c));

    if (rgba.length !== 3 && rgba.length !== 4) {
      throw new Error(
        `Color specified in incorrect format (should be R,G,B or R,G,B,A): ${rgba.join(
          ","
        )}`
      );
    }

    const color = new Color4(rgba[0], rgba[1], rgba[2], rgba[3]);

    this.addColor(color, output, key.trim());
  }
  static addColor(color, output, key) {
    if (key === "SliderTrackOverride") {
      output.colors.sliderTrackColor = color;

      return;
    }

    if (key === "SliderBorder") {
      output.colors.sliderBorderColor = color;

      return;
    }

    if (key.startsWith("Combo")) {
      output.colors.comboColors.push(color);
    }
  }
}

class BeatmapDifficultyDecoder {
  static handleLine(line, beatmap) {
    const [key, ...values] = line.split(":");
    const value = values.join(":").trim();

    switch (key.trim()) {
      case "CircleSize":
        beatmap.difficulty.circleSize = Parsing.parseFloat(value);
        break;
      case "HPDrainRate":
        beatmap.difficulty.drainRate = Parsing.parseFloat(value);
        break;
      case "OverallDifficulty":
        beatmap.difficulty.overallDifficulty = Parsing.parseFloat(value);
        break;
      case "ApproachRate":
        beatmap.difficulty.approachRate = Parsing.parseFloat(value);
        break;
      case "SliderMultiplier":
        beatmap.difficulty.sliderMultiplier = Parsing.parseFloat(value);
        break;
      case "SliderTickRate":
        beatmap.difficulty.sliderTickRate = Parsing.parseFloat(value);
    }
  }
}

class BeatmapEditorDecoder {
  static handleLine(line, beatmap) {
    const [key, ...values] = line.split(":");
    const value = values.join(":").trim();

    switch (key.trim()) {
      case "Bookmarks":
        beatmap.editor.bookmarks = value.split(",").map((v) => +v);
        break;
      case "DistanceSpacing":
        beatmap.editor.distanceSpacing = Math.max(0, Parsing.parseFloat(value));
        break;
      case "BeatDivisor":
        beatmap.editor.beatDivisor = Parsing.parseInt(value);
        break;
      case "GridSize":
        beatmap.editor.gridSize = Parsing.parseInt(value);
        break;
      case "TimelineZoom":
        beatmap.editor.timelineZoom = Math.max(0, Parsing.parseFloat(value));
    }
  }
}

class StoryboardEventDecoder {
  static handleLine(line, storyboard) {
    const depth = this._getDepth(line);

    if (depth > 0) {
      line = line.substring(depth);
    }

    if (depth < 2 && this._storyboardSprite) {
      this._timelineGroup = this._storyboardSprite.timelineGroup;
    }

    switch (depth) {
      case 0:
        return this._handleElement(line, storyboard);
      case 1:
        return this._handleCompoundOrCommand(line);
      case 2:
        return this._handleCommand(line);
    }
  }
  static _handleElement(line, storyboard) {
    let _a;
    const data = line.split(",");
    const eventType = this.parseEventType(data[0]);

    if (
      (_a = this._storyboardSprite) === null || _a === void 0
        ? void 0
        : _a.hasCommands
    ) {
      this._storyboardSprite.updateCommands();
      this._storyboardSprite.adjustTimesToCommands();
      this._storyboardSprite.resetValuesToCommands();
    }

    switch (eventType) {
      case EventType.Video: {
        const layer = storyboard.getLayerByType(LayerType.Video);
        const offset = Parsing.parseInt(data[1]);
        const path = data[2].replace(/"/g, "");

        layer.elements.push(new StoryboardVideo(path, offset));

        return;
      }
      case EventType.Sprite: {
        const layer = storyboard.getLayerByType(this.parseLayerType(data[1]));
        const origin = this.parseOrigin(data[2]);
        const anchor = this.convertOrigin(origin);
        const path = data[3].replace(/"/g, "");
        const x = Parsing.parseFloat(data[4], Parsing.MAX_COORDINATE_VALUE);
        const y = Parsing.parseFloat(data[5], Parsing.MAX_COORDINATE_VALUE);

        this._storyboardSprite = new StoryboardSprite(
          path,
          origin,
          anchor,
          new Vector2(x, y)
        );
        layer.elements.push(this._storyboardSprite);

        return;
      }
      case EventType.Animation: {
        const layer = storyboard.getLayerByType(this.parseLayerType(data[1]));
        const origin = this.parseOrigin(data[2]);
        const anchor = this.convertOrigin(origin);
        const path = data[3].replace(/"/g, "");
        const x = Parsing.parseFloat(data[4], Parsing.MAX_COORDINATE_VALUE);
        const y = Parsing.parseFloat(data[5], Parsing.MAX_COORDINATE_VALUE);
        const frameCount = Parsing.parseInt(data[6]);
        let frameDelay = Parsing.parseFloat(data[7]);

        if (storyboard.fileFormat < 6) {
          frameDelay = Math.round(0.015 * frameDelay) * 1.186 * (1000 / 60);
        }

        const loopType = this.parseLoopType(data[8]);

        this._storyboardSprite = new StoryboardAnimation(
          path,
          origin,
          anchor,
          new Vector2(x, y),
          frameCount,
          frameDelay,
          loopType
        );
        layer.elements.push(this._storyboardSprite);

        return;
      }
      case EventType.Sample: {
        const time = Parsing.parseFloat(data[1]);
        const layer = storyboard.getLayerByType(this.parseLayerType(data[2]));
        const path = data[3].replace(/"/g, "");
        const volume = data.length > 4 ? Parsing.parseInt(data[4]) : 100;
        const sample = new StoryboardSample(path, time, volume);

        layer.elements.push(sample);
      }
    }
  }
  static _handleCompoundOrCommand(line) {
    let _a, _b;
    const data = line.split(",");
    const compoundType = data[0];

    switch (compoundType) {
      case CompoundType.Trigger: {
        this._timelineGroup =
          (_a = this._storyboardSprite) === null || _a === void 0
            ? void 0
            : _a.addTrigger(
                data[1],
                data.length > 2 ? Parsing.parseFloat(data[2]) : -Infinity,
                data.length > 3 ? Parsing.parseFloat(data[3]) : Infinity,
                data.length > 4 ? Parsing.parseInt(data[4]) : 0
              );

        return;
      }
      case CompoundType.Loop: {
        this._timelineGroup =
          (_b = this._storyboardSprite) === null || _b === void 0
            ? void 0
            : _b.addLoop(
                Parsing.parseFloat(data[1]),
                Math.max(0, Parsing.parseInt(data[2]) - 1)
              );

        return;
      }

      default:
        this._handleCommand(line);
    }
  }
  static _handleCommand(line) {
    let _a, _b, _c, _d, _e, _f, _g, _h, _j;
    const data = line.split(",");
    const type = data[0];
    const easing = Parsing.parseInt(data[1]);
    const startTime = Parsing.parseInt(data[2]);
    const endTime = Parsing.parseInt(data[3] || data[2]);

    switch (type) {
      case CommandType.Fade: {
        const startValue = Parsing.parseFloat(data[4]);
        const endValue =
          data.length > 5 ? Parsing.parseFloat(data[5]) : startValue;

        (_a = this._timelineGroup) === null || _a === void 0
          ? void 0
          : _a.alpha.add(
              type,
              easing,
              startTime,
              endTime,
              startValue,
              endValue
            );

        return;
      }
      case CommandType.Scale: {
        const startValue = Parsing.parseFloat(data[4]);
        const endValue =
          data.length > 5 ? Parsing.parseFloat(data[5]) : startValue;

        (_b = this._timelineGroup) === null || _b === void 0
          ? void 0
          : _b.scale.add(
              type,
              easing,
              startTime,
              endTime,
              startValue,
              endValue
            );

        return;
      }
      case CommandType.VectorScale: {
        const startX = Parsing.parseFloat(data[4]);
        const startY = Parsing.parseFloat(data[5]);
        const endX = data.length > 6 ? Parsing.parseFloat(data[6]) : startX;
        const endY = data.length > 7 ? Parsing.parseFloat(data[7]) : startY;

        (_c = this._timelineGroup) === null || _c === void 0
          ? void 0
          : _c.vectorScale.add(
              type,
              easing,
              startTime,
              endTime,
              new Vector2(startX, startY),
              new Vector2(endX, endY)
            );

        return;
      }
      case CommandType.Rotation: {
        const startValue = Parsing.parseFloat(data[4]);
        const endValue =
          data.length > 5 ? Parsing.parseFloat(data[5]) : startValue;

        (_d = this._timelineGroup) === null || _d === void 0
          ? void 0
          : _d.rotation.add(
              type,
              easing,
              startTime,
              endTime,
              startValue,
              endValue
            );

        return;
      }
      case CommandType.Movement: {
        const startX = Parsing.parseFloat(data[4]);
        const startY = Parsing.parseFloat(data[5]);
        const endX = data.length > 6 ? Parsing.parseFloat(data[6]) : startX;
        const endY = data.length > 7 ? Parsing.parseFloat(data[7]) : startY;

        (_e = this._timelineGroup) === null || _e === void 0
          ? void 0
          : _e.x.add(
              CommandType.MovementX,
              easing,
              startTime,
              endTime,
              startX,
              endX
            );
        (_f = this._timelineGroup) === null || _f === void 0
          ? void 0
          : _f.y.add(
              CommandType.MovementY,
              easing,
              startTime,
              endTime,
              startY,
              endY
            );

        return;
      }
      case CommandType.MovementX: {
        const startValue = Parsing.parseFloat(data[4]);
        const endValue =
          data.length > 5 ? Parsing.parseFloat(data[5]) : startValue;

        (_g = this._timelineGroup) === null || _g === void 0
          ? void 0
          : _g.x.add(type, easing, startTime, endTime, startValue, endValue);

        return;
      }
      case CommandType.MovementY: {
        const startValue = Parsing.parseFloat(data[4]);
        const endValue =
          data.length > 5 ? Parsing.parseFloat(data[5]) : startValue;

        (_h = this._timelineGroup) === null || _h === void 0
          ? void 0
          : _h.y.add(type, easing, startTime, endTime, startValue, endValue);

        return;
      }
      case CommandType.Color: {
        const startRed = Parsing.parseFloat(data[4]);
        const startGreen = Parsing.parseFloat(data[5]);
        const startBlue = Parsing.parseFloat(data[6]);
        const endRed = data.length > 7 ? Parsing.parseFloat(data[7]) : startRed;
        const endGreen =
          data.length > 8 ? Parsing.parseFloat(data[8]) : startGreen;
        const endBlue =
          data.length > 9 ? Parsing.parseFloat(data[9]) : startBlue;

        (_j = this._timelineGroup) === null || _j === void 0
          ? void 0
          : _j.color.add(
              type,
              easing,
              startTime,
              endTime,
              new Color4(startRed, startGreen, startBlue, 1),
              new Color4(endRed, endGreen, endBlue, 1)
            );

        return;
      }
      case CommandType.Parameter: {
        return this._handleParameterCommand(data);
      }
    }

    throw new Error(`Unknown command type: ${type}`);
  }
  static _handleParameterCommand(data) {
    let _a, _b, _c;
    const type = CommandType.Parameter;
    const easing = Parsing.parseInt(data[1]);
    const startTime = Parsing.parseInt(data[2]);
    const endTime = Parsing.parseInt(data[3] || data[2]);
    const parameter = data[4];

    switch (parameter) {
      case ParameterType.BlendingMode: {
        const startValue = BlendingParameters.Additive;
        const endValue =
          startTime === endTime
            ? BlendingParameters.Additive
            : BlendingParameters.Inherit;

        (_a = this._timelineGroup) === null || _a === void 0
          ? void 0
          : _a.blendingParameters.add(
              type,
              easing,
              startTime,
              endTime,
              startValue,
              endValue,
              parameter
            );

        return;
      }
      case ParameterType.HorizontalFlip:
        (_b = this._timelineGroup) === null || _b === void 0
          ? void 0
          : _b.flipH.add(
              type,
              easing,
              startTime,
              endTime,
              true,
              startTime === endTime,
              parameter
            );

        return;
      case ParameterType.VerticalFlip: {
        (_c = this._timelineGroup) === null || _c === void 0
          ? void 0
          : _c.flipV.add(
              type,
              easing,
              startTime,
              endTime,
              true,
              startTime === endTime,
              parameter
            );

        return;
      }
    }

    throw new Error(`Unknown parameter type: ${parameter}`);
  }
  static parseEventType(input) {
    if (input.startsWith(" ") || input.startsWith("_")) {
      return EventType.StoryboardCommand;
    }

    try {
      return Parsing.parseEnum(EventType, input);
    } catch {
      throw new Error(`Unknown event type: ${input}`);
    }
  }
  static parseLayerType(input) {
    try {
      return Parsing.parseEnum(LayerType, input);
    } catch {
      throw new Error(`Unknown layer type: ${input}`);
    }
  }
  static parseOrigin(input) {
    try {
      return Parsing.parseEnum(Origins, input);
    } catch {
      return Origins.TopLeft;
    }
  }
  static convertOrigin(origin) {
    switch (origin) {
      case Origins.TopLeft:
        return Anchor.TopLeft;
      case Origins.TopCentre:
        return Anchor.TopCentre;
      case Origins.TopRight:
        return Anchor.TopRight;
      case Origins.CentreLeft:
        return Anchor.CentreLeft;
      case Origins.Centre:
        return Anchor.Centre;
      case Origins.CentreRight:
        return Anchor.CentreRight;
      case Origins.BottomLeft:
        return Anchor.BottomLeft;
      case Origins.BottomCentre:
        return Anchor.BottomCentre;
      case Origins.BottomRight:
        return Anchor.BottomRight;
    }

    return Anchor.TopLeft;
  }
  static parseLoopType(input) {
    try {
      return Parsing.parseEnum(LoopType, input);
    } catch {
      return LoopType.LoopForever;
    }
  }
  static _getDepth(line) {
    let depth = 0;

    for (const char of line) {
      if (char !== " " && char !== "_") {
        break;
      }

      ++depth;
    }

    return depth;
  }
}

class BeatmapEventDecoder {
  static handleLine(line, beatmap, sbLines, offset) {
    const data = line.split(",").map((v, i) => (i ? v.trim() : v));
    const eventType = StoryboardEventDecoder.parseEventType(data[0]);

    switch (eventType) {
      case EventType.Background:
        beatmap.events.backgroundPath = data[2].replace(/"/g, "");
        break;
      case EventType.Break: {
        const start = Parsing.parseFloat(data[1]) + offset;
        const end = Math.max(start, Parsing.parseFloat(data[2]) + offset);
        const breakEvent = new BeatmapBreakEvent(start, end);

        if (!beatmap.events.breaks) {
          beatmap.events.breaks = [];
        }

        beatmap.events.breaks.push(breakEvent);
        break;
      }
      case EventType.Video:
      case EventType.Sample:
      case EventType.Sprite:
      case EventType.Animation:
      case EventType.StoryboardCommand:
        if (sbLines) {
          sbLines.push(line);
        }
    }
  }
}

class BeatmapGeneralDecoder {
  static handleLine(line, beatmap, offset) {
    const [key, ...values] = line.split(":");
    const value = values.join(":").trim();

    switch (key.trim()) {
      case "AudioFilename":
        beatmap.general.audioFilename = value;
        break;
      case "AudioHash":
        beatmap.general.audioHash = value;
        break;
      case "OverlayPosition":
        beatmap.general.overlayPosition = value;
        break;
      case "SkinPreference":
        beatmap.general.skinPreference = value;
        break;
      case "AudioLeadIn":
        beatmap.general.audioLeadIn = Parsing.parseInt(value);
        break;
      case "PreviewTime":
        beatmap.general.previewTime = Parsing.parseInt(value) + offset;
        break;
      case "Countdown":
        beatmap.general.countdown = Parsing.parseInt(value);
        break;
      case "StackLeniency":
        beatmap.general.stackLeniency = Parsing.parseFloat(value);
        break;
      case "Mode":
        beatmap.originalMode = Parsing.parseInt(value);
        break;
      case "CountdownOffset":
        beatmap.general.countdownOffset = Parsing.parseInt(value);
        break;
      case "SampleSet":
        beatmap.general.sampleSet = SampleSet[value];
        break;
      case "LetterboxInBreaks":
        beatmap.general.letterboxInBreaks = value === "1";
        break;
      case "StoryFireInFront":
        beatmap.general.storyFireInFront = value === "1";
        break;
      case "UseSkinSprites":
        beatmap.general.useSkinSprites = value === "1";
        break;
      case "AlwaysShowPlayfield":
        beatmap.general.alwaysShowPlayfield = value === "1";
        break;
      case "EpilepsyWarning":
        beatmap.general.epilepsyWarning = value === "1";
        break;
      case "SpecialStyle":
        beatmap.general.specialStyle = value === "1";
        break;
      case "WidescreenStoryboard":
        beatmap.general.widescreenStoryboard = value === "1";
        break;
      case "SamplesMatchPlaybackRate":
        beatmap.general.samplesMatchPlaybackRate = value === "1";
    }
  }
}

class HittableObject extends HitObject {
  constructor() {
    super(...arguments);
    this.isNewCombo = false;
    this.comboOffset = 0;
  }
  clone() {
    const cloned = super.clone();

    cloned.isNewCombo = this.isNewCombo;
    cloned.comboOffset = this.comboOffset;

    return cloned;
  }
}

class HoldableObject extends HitObject {
  constructor() {
    super(...arguments);
    this.endTime = 0;
    this.nodeSamples = [];
  }
  get duration() {
    return this.endTime - this.startTime;
  }
  clone() {
    const cloned = super.clone();

    cloned.endTime = this.endTime;
    cloned.nestedHitObjects = this.nestedHitObjects.map((h) => h.clone());
    cloned.nodeSamples = this.nodeSamples.map((n) => n.map((s) => s.clone()));

    return cloned;
  }
}

class SlidableObject extends HitObject {
  constructor() {
    super(...arguments);
    this.repeats = 0;
    this.velocity = 1;
    this.path = new SliderPath();
    this.legacyLastTickOffset = 36;
    this.nodeSamples = [];
    this.isNewCombo = false;
    this.comboOffset = 0;
  }
  get duration() {
    return this.spans * this.spanDuration;
  }
  get endTime() {
    return this.startTime + this.duration;
  }
  get spans() {
    return this.repeats + 1;
  }
  set spans(value) {
    this.repeats = value - 1;
  }
  get spanDuration() {
    return this.distance / this.velocity;
  }
  get distance() {
    return this.path.distance;
  }
  set distance(value) {
    this.path.distance = value;
  }
  applyDefaultsToSelf(controlPoints, difficulty) {
    super.applyDefaultsToSelf(controlPoints, difficulty);

    const timingPoint = controlPoints.timingPointAt(this.startTime);
    const difficultyPoint = controlPoints.difficultyPointAt(this.startTime);
    const scoringDistance =
      SlidableObject.BASE_SCORING_DISTANCE *
      difficulty.sliderMultiplier *
      difficultyPoint.sliderVelocity;

    this.velocity = scoringDistance / timingPoint.beatLength;
  }
  clone() {
    const cloned = super.clone();

    cloned.legacyLastTickOffset = this.legacyLastTickOffset;
    cloned.nodeSamples = this.nodeSamples.map((n) => n.map((s) => s.clone()));
    cloned.velocity = this.velocity;
    cloned.repeats = this.repeats;
    cloned.path = this.path.clone();
    cloned.isNewCombo = this.isNewCombo;
    cloned.comboOffset = this.comboOffset;

    return cloned;
  }
}
SlidableObject.BASE_SCORING_DISTANCE = 100;

class SpinnableObject extends HitObject {
  constructor() {
    super(...arguments);
    this.endTime = 0;
    this.isNewCombo = false;
    this.comboOffset = 0;
  }
  get duration() {
    return this.endTime - this.startTime;
  }
  clone() {
    const cloned = super.clone();

    cloned.endTime = this.endTime;
    cloned.isNewCombo = this.isNewCombo;
    cloned.comboOffset = this.comboOffset;

    return cloned;
  }
}

let FileFormat;

(function (FileFormat) {
  FileFormat["Beatmap"] = ".osu";
  FileFormat["Storyboard"] = ".osb";
  FileFormat["Replay"] = ".osr";
})(FileFormat || (FileFormat = {}));

let LineType;

(function (LineType) {
  LineType[(LineType["FileFormat"] = 0)] = "FileFormat";
  LineType[(LineType["Section"] = 1)] = "Section";
  LineType[(LineType["Empty"] = 2)] = "Empty";
  LineType[(LineType["Data"] = 3)] = "Data";
  LineType[(LineType["Break"] = 4)] = "Break";
})(LineType || (LineType = {}));

let Section;

(function (Section) {
  Section["General"] = "General";
  Section["Editor"] = "Editor";
  Section["Metadata"] = "Metadata";
  Section["Difficulty"] = "Difficulty";
  Section["Events"] = "Events";
  Section["TimingPoints"] = "TimingPoints";
  Section["Colours"] = "Colours";
  Section["HitObjects"] = "HitObjects";
  Section["Variables"] = "Variables";
  Section["Fonts"] = "Fonts";
  Section["CatchTheBeat"] = "CatchTheBeat";
  Section["Mania"] = "Mania";
})(Section || (Section = {}));

const browserFSOperation = function () {
  throw new Error(
    "Filesystem operations are not available in a browser environment"
  );
};

class BeatmapColorEncoder {
  static encodeColors(beatmap) {
    const colors = beatmap.colors;

    if (Object.keys(colors).length === 1 && !colors.comboColors.length) {
      return "";
    }

    const encoded = ["[Colours]"];

    colors.comboColors.forEach((color, i) => {
      encoded.push(`Combo${i + 1}:${color}`);
    });

    if (colors.sliderTrackColor) {
      encoded.push(`SliderTrackOverride:${colors.sliderTrackColor}`);
    }

    if (colors.sliderBorderColor) {
      encoded.push(`SliderBorder:${colors.sliderBorderColor}`);
    }

    return encoded.join("\n");
  }
}

class BeatmapDifficultyEncoder {
  static encodeDifficultySection(beatmap) {
    const encoded = ["[Difficulty]"];
    const difficulty = beatmap.difficulty;

    encoded.push(`HPDrainRate:${difficulty.drainRate}`);
    encoded.push(`CircleSize:${difficulty.circleSize}`);
    encoded.push(`OverallDifficulty:${difficulty.overallDifficulty}`);
    encoded.push(`ApproachRate:${difficulty.approachRate}`);
    encoded.push(`SliderMultiplier:${difficulty.sliderMultiplier}`);
    encoded.push(`SliderTickRate:${difficulty.sliderTickRate}`);

    return encoded.join("\n");
  }
}

class BeatmapEditorEncoder {
  static encodeEditorSection(beatmap) {
    const encoded = ["[Editor]"];
    const editor = beatmap.editor;

    encoded.push(`Bookmarks:${editor.bookmarks.join(",")}`);
    encoded.push(`DistanceSpacing:${editor.distanceSpacing}`);
    encoded.push(`BeatDivisor:${editor.beatDivisor}`);
    encoded.push(`GridSize:${editor.gridSize}`);
    encoded.push(`TimelineZoom:${editor.timelineZoom}`);

    return encoded.join("\n");
  }
}

class StoryboardEventEncoder {
  static encodeEventSection(storyboard) {
    const encoded = [];

    encoded.push("[Events]");
    encoded.push("//Background and Video events");
    encoded.push(this.encodeVideos(storyboard));
    encoded.push(this.encodeStoryboard(storyboard));

    return encoded.join("\n");
  }
  static encodeVideos(storyboard) {
    const encoded = [];
    const video = storyboard.getLayerByType(LayerType.Video);

    if (video.elements.length > 0) {
      encoded.push(this.encodeStoryboardLayer(video));
    }

    return encoded.join("\n");
  }
  static encodeStoryboard(storyboard) {
    const encoded = [];

    encoded.push("//Storyboard Layer 0 (Background)");

    const background = storyboard.getLayerByType(LayerType.Background);

    if (background.elements.length > 0) {
      encoded.push(this.encodeStoryboardLayer(background));
    }

    encoded.push("//Storyboard Layer 1 (Fail)");

    const fail = storyboard.getLayerByType(LayerType.Fail);

    if (fail.elements.length > 0) {
      encoded.push(this.encodeStoryboardLayer(fail));
    }

    encoded.push("//Storyboard Layer 2 (Pass)");

    const pass = storyboard.getLayerByType(LayerType.Pass);

    if (pass.elements.length > 0) {
      encoded.push(this.encodeStoryboardLayer(pass));
    }

    encoded.push("//Storyboard Layer 3 (Foreground)");

    const foreground = storyboard.getLayerByType(LayerType.Foreground);

    if (foreground.elements.length > 0) {
      encoded.push(this.encodeStoryboardLayer(foreground));
    }

    encoded.push("//Storyboard Layer 4 (Overlay)");

    const overlay = storyboard.getLayerByType(LayerType.Overlay);

    if (overlay.elements.length > 0) {
      encoded.push(this.encodeStoryboardLayer(overlay));
    }

    return encoded.join("\n");
  }
  static encodeStoryboardLayer(layer) {
    const encoded = [];

    layer.elements.forEach((element) => {
      let _a, _b, _c;

      encoded.push(this.encodeStoryboardElement(element, layer.name));

      const elementWithCommands = element;

      (_a =
        elementWithCommands === null || elementWithCommands === void 0
          ? void 0
          : elementWithCommands.loops) === null || _a === void 0
        ? void 0
        : _a.forEach((loop) => {
            if (loop.commands.length > 0) {
              encoded.push(this.encodeCompound(loop));
              encoded.push(this.encodeTimelineGroup(loop, 2));
            }
          });

      if (
        ((_b =
          elementWithCommands === null || elementWithCommands === void 0
            ? void 0
            : elementWithCommands.timelineGroup) === null || _b === void 0
          ? void 0
          : _b.commands.length) > 0
      ) {
        encoded.push(
          this.encodeTimelineGroup(elementWithCommands.timelineGroup)
        );
      }

      (_c =
        elementWithCommands === null || elementWithCommands === void 0
          ? void 0
          : elementWithCommands.triggers) === null || _c === void 0
        ? void 0
        : _c.forEach((trigger) => {
            if (trigger.commands.length > 0) {
              encoded.push(this.encodeCompound(trigger));
              encoded.push(this.encodeTimelineGroup(trigger, 2));
            }
          });
    });

    return encoded.join("\n");
  }
  static encodeStoryboardElement(element, layer) {
    if (element instanceof StoryboardAnimation) {
      return [
        "Animation",
        layer,
        Origins[element.origin],
        `"${element.filePath}"`,
        element.startPosition,
        element.frameCount,
        element.frameDelay,
        element.loopType,
      ].join(",");
    }

    if (element instanceof StoryboardSprite) {
      return [
        "Sprite",
        layer,
        Origins[element.origin],
        `"${element.filePath}"`,
        element.startPosition,
      ].join(",");
    }

    if (element instanceof StoryboardSample) {
      return [
        "Sample",
        element.startTime,
        layer,
        `"${element.filePath}"`,
        element.volume,
      ].join(",");
    }

    if (element instanceof StoryboardVideo) {
      return ["Video", element.startTime, element.filePath, "0,0"].join(",");
    }

    return "";
  }
  static encodeCompound(compound, depth = 1) {
    const indentation = "".padStart(depth, " ");

    if (compound instanceof CommandLoop) {
      return (
        indentation +
        [compound.type, compound.loopStartTime, compound.totalIterations].join(
          ","
        )
      );
    }

    if (compound instanceof CommandTrigger) {
      return (
        indentation +
        [
          compound.type,
          compound.triggerName,
          compound.triggerStartTime,
          compound.triggerEndTime,
          compound.groupNumber,
        ].join(",")
      );
    }

    return "";
  }
  static encodeTimelineGroup(timelineGroup, depth = 1) {
    const indentation = "".padStart(depth, " ");
    const encoded = [];
    const commands = timelineGroup.commands;
    let shouldSkip = false;

    for (let i = 0; i < commands.length; ++i) {
      if (shouldSkip) {
        shouldSkip = false;
        continue;
      }

      if (i < commands.length - 1) {
        const current = commands[i];
        const next = commands[i + 1];
        const currentMoveX = current.type === CommandType.MovementX;
        const nextMoveY = next.type === CommandType.MovementY;
        const sameEasing = current.easing === next.easing;
        const sameStartTime = current.startTime === next.startTime;
        const sameEndTime = current.endTime === next.endTime;
        const sameCommand = sameEasing && sameStartTime && sameEndTime;

        if (currentMoveX && nextMoveY && sameCommand) {
          encoded.push(indentation + this.encodeMoveCommand(current, next));
          shouldSkip = true;
          continue;
        }
      }

      encoded.push(indentation + this.encodeCommand(commands[i]));
    }

    return encoded.join("\n");
  }
  static encodeMoveCommand(moveX, moveY) {
    const encoded = [
      CommandType.Movement,
      moveX.easing,
      moveX.startTime,
      moveX.startTime !== moveX.endTime ? moveX.endTime : "",
      moveX.startValue,
      moveY.startValue,
    ];
    const equalX = moveX.startValue === moveX.endValue;
    const equalY = moveY.startValue === moveY.endValue;

    if (!equalX || !equalY) {
      encoded.push(`${moveX.endValue},${moveY.endValue}`);
    }

    return encoded.join(",");
  }
  static encodeCommand(command) {
    const encoded = [
      command.type,
      command.easing,
      command.startTime,
      command.startTime !== command.endTime ? command.endTime : "",
      this._encodeCommandParams(command),
    ];

    return encoded.join(",");
  }
  static _encodeCommandParams(command) {
    if (command.type === CommandType.Parameter) {
      return command.parameter;
    }

    if (command.type === CommandType.Color) {
      const toRGB = (c) => `${c.red},${c.green},${c.blue}`;
      const colorCommand = command;
      const start = colorCommand.startValue;
      const end = colorCommand.endValue;

      return this._areValuesEqual(command)
        ? toRGB(start)
        : toRGB(start) + "," + toRGB(end);
    }

    return this._areValuesEqual(command)
      ? `${command.startValue}`
      : `${command.startValue},${command.endValue}`;
  }
  static _areValuesEqual(command) {
    if (command.type === CommandType.VectorScale) {
      const vectorCommand = command;

      return vectorCommand.startValue.equals(vectorCommand.endValue);
    }

    if (command.type === CommandType.Color) {
      const colorCommand = command;

      return colorCommand.startValue.equals(colorCommand.endValue);
    }

    return command.startValue === command.endValue;
  }
}

class BeatmapEventEncoder {
  static encodeEventSection(beatmap) {
    const encoded = [];
    const events = beatmap.events;

    encoded.push("[Events]");
    encoded.push("//Background and Video events");

    if (events.backgroundPath) {
      encoded.push(`0,0,"${events.backgroundPath}",0,0`);
    }

    if (events.storyboard) {
      encoded.push(StoryboardEventEncoder.encodeVideos(events.storyboard));
    }

    encoded.push("//Break Periods");

    if (events.breaks && events.breaks.length > 0) {
      events.breaks.forEach((b) => {
        encoded.push(
          `${EventType[EventType.Break]},${b.startTime},${b.endTime}`
        );
      });
    }

    if (events.storyboard) {
      encoded.push(StoryboardEventEncoder.encodeStoryboard(events.storyboard));
    }

    return encoded.join("\n");
  }
}

class BeatmapGeneralEncoder {
  static encodeGeneralSection(beatmap) {
    const encoded = ["[General]"];
    const general = beatmap.general;

    encoded.push(`AudioFilename:${general.audioFilename}`);
    encoded.push(`AudioLeadIn:${general.audioLeadIn}`);

    if (general.audioHash) {
      encoded.push(`AudioHash:${general.audioHash}`);
    }

    encoded.push(`PreviewTime:${general.previewTime}`);
    encoded.push(`Countdown:${general.countdown}`);
    encoded.push(`SampleSet:${SampleSet[general.sampleSet]}`);
    encoded.push(`StackLeniency:${general.stackLeniency}`);
    encoded.push(`Mode:${beatmap.mode}`);
    encoded.push(`LetterboxInBreaks:${+general.letterboxInBreaks}`);

    if (general.storyFireInFront) {
      encoded.push(`StoryFireInFront:${+general.storyFireInFront}`);
    }

    encoded.push(`UseSkinSprites:${+general.useSkinSprites}`);

    if (general.alwaysShowPlayfield) {
      encoded.push(`AlwaysShowPlayfield:${+general.alwaysShowPlayfield}`);
    }

    encoded.push(`OverlayPosition:${general.overlayPosition}`);
    encoded.push(`SkinPreference:${general.skinPreference}`);
    encoded.push(`EpilepsyWarning:${+general.epilepsyWarning}`);
    encoded.push(`CountdownOffset:${general.countdownOffset}`);
    encoded.push(`SpecialStyle:${+general.specialStyle}`);
    encoded.push(`WidescreenStoryboard:${+general.widescreenStoryboard}`);
    encoded.push(
      `SamplesMatchPlaybackRate:${+general.samplesMatchPlaybackRate}`
    );

    return encoded.join("\n");
  }
}

class BeatmapHitObjectEncoder {
  static encodeHitObjects(beatmap) {
    const encoded = ["[HitObjects]"];
    const difficulty = beatmap.difficulty;
    const hitObjects = beatmap.hitObjects;

    hitObjects.forEach((hitObject) => {
      const general = [];
      const position = hitObject.startPosition;
      const startPosition = new Vector2(
        position ? position.x : 256,
        position ? position.y : 192
      );

      if (beatmap.mode === 3) {
        const totalColumns = Math.trunc(Math.max(1, difficulty.circleSize));
        const multiplier = Math.round((512 / totalColumns) * 100000) / 100000;
        const column = hitObject.startX;

        startPosition.x =
          Math.ceil(column * multiplier) + Math.trunc(multiplier / 2);
      }

      general.push(startPosition.toString());
      general.push(hitObject.startTime.toString());
      general.push(hitObject.hitType.toString());
      general.push(hitObject.hitSound.toString());

      const extras = [];

      if (hitObject.hitType & HitType.Slider) {
        const slider = hitObject;

        extras.push(this.encodePathData(slider, startPosition));
      } else if (hitObject.hitType & HitType.Spinner) {
        const spinner = hitObject;

        extras.push(this.encodeEndTimeData(spinner));
      } else if (hitObject.hitType & HitType.Hold) {
        const hold = hitObject;

        extras.push(this.encodeEndTimeData(hold));
      }

      const set = [];
      const normal = hitObject.samples.find(
        (s) => s.hitSound === HitSound[HitSound.Normal]
      );
      const addition = hitObject.samples.find(
        (s) => s.hitSound !== HitSound[HitSound.Normal]
      );
      let normalSet = SampleSet.None;
      let additionSet = SampleSet.None;

      if (normal) {
        normalSet = SampleSet[normal.sampleSet];
      }

      if (addition) {
        additionSet = SampleSet[addition.sampleSet];
      }

      set[0] = normalSet.toString();
      set[1] = additionSet.toString();
      set[2] = hitObject.samples[0].customIndex.toString();
      set[3] = hitObject.samples[0].volume.toString();
      set[4] = hitObject.samples[0].filename;
      extras.push(set.join(":"));

      const generalLine = general.join(",");
      const extrasLine =
        hitObject.hitType & HitType.Hold ? extras.join(":") : extras.join(",");

      encoded.push([generalLine, extrasLine].join(","));
    });

    return encoded.join("\n");
  }
  static encodePathData(slider, offset) {
    const path = [];
    let lastType;

    slider.path.controlPoints.forEach((point, i) => {
      if (point.type !== null) {
        let needsExplicitSegment =
          point.type !== lastType || point.type === PathType.PerfectCurve;

        if (i > 1) {
          const p1 = offset.add(slider.path.controlPoints[i - 1].position);
          const p2 = offset.add(slider.path.controlPoints[i - 2].position);

          if (~~p1.x === ~~p2.x && ~~p1.y === ~~p2.y) {
            needsExplicitSegment = true;
          }
        }

        if (needsExplicitSegment) {
          path.push(slider.path.curveType);
          lastType = point.type;
        } else {
          path.push(
            `${offset.x + point.position.x}:${offset.y + point.position.y}`
          );
        }
      }

      if (i !== 0) {
        path.push(
          `${offset.x + point.position.x}:${offset.y + point.position.y}`
        );
      }
    });

    const data = [];

    data.push(path.join("|"));
    data.push((slider.repeats + 1).toString());
    data.push(slider.distance.toString());

    const adds = [];
    const sets = [];

    slider.nodeSamples.forEach((node, nodeIndex) => {
      adds[nodeIndex] = HitSound.None;
      sets[nodeIndex] = [SampleSet.None, SampleSet.None];
      node.forEach((sample, sampleIndex) => {
        if (sampleIndex === 0) {
          sets[nodeIndex][0] = SampleSet[sample.sampleSet];
        } else {
          adds[nodeIndex] |= HitSound[sample.hitSound];
          sets[nodeIndex][1] = SampleSet[sample.sampleSet];
        }
      });
    });

    data.push(adds.join("|"));
    data.push(sets.map((set) => set.join(":")).join("|"));

    return data.join(",");
  }
  static encodeEndTimeData(hitObject) {
    return hitObject.endTime.toString();
  }
}

class BeatmapMetadataEncoder {
  static encodeMetadataSection(beatmap) {
    const encoded = ["[Metadata]"];
    const metadata = beatmap.metadata;

    encoded.push(`Title:${metadata.title}`);
    encoded.push(`TitleUnicode:${metadata.titleUnicode}`);
    encoded.push(`Artist:${metadata.artist}`);
    encoded.push(`ArtistUnicode:${metadata.artistUnicode}`);
    encoded.push(`Creator:${metadata.creator}`);
    encoded.push(`Version:${metadata.version}`);
    encoded.push(`Source:${metadata.source}`);
    encoded.push(`Tags:${metadata.tags.join(" ")}`);
    encoded.push(`BeatmapID:${metadata.beatmapId}`);
    encoded.push(`BeatmapSetID:${metadata.beatmapSetId}`);

    return encoded.join("\n");
  }
}

class BeatmapTimingPointEncoder {
  static encodeControlPoints(beatmap) {
    const encoded = ["[TimingPoints]"];

    beatmap.controlPoints.groups.forEach((group) => {
      const points = group.controlPoints;
      const timing = points.find((c) => c.beatLength);

      if (timing) {
        encoded.push(this.encodeGroup(group, true));
      }

      encoded.push(this.encodeGroup(group));
    });

    return encoded.join("\n");
  }
  static encodeGroup(group, useTiming = false) {
    const { difficultyPoint, effectPoint, samplePoint, timingPoint } =
      this.updateActualPoints(group);
    const startTime = group.startTime;
    let beatLength = -100;

    if (difficultyPoint !== null) {
      beatLength /= difficultyPoint.sliderVelocity;
    }

    let sampleSet = SampleSet.None;
    let customIndex = 0;
    let volume = 100;

    if (samplePoint !== null) {
      sampleSet = SampleSet[samplePoint.sampleSet];
      customIndex = samplePoint.customIndex;
      volume = samplePoint.volume;
    }

    let effects = EffectType.None;

    if (effectPoint !== null) {
      const kiai = effectPoint.kiai ? EffectType.Kiai : EffectType.None;
      const omitFirstBarLine = effectPoint.omitFirstBarLine
        ? EffectType.OmitFirstBarLine
        : EffectType.None;

      effects |= kiai | omitFirstBarLine;
    }

    let timeSignature = TimeSignature.SimpleQuadruple;
    let uninherited = 0;

    if (useTiming && timingPoint !== null) {
      beatLength = timingPoint.beatLength;
      timeSignature = timingPoint.timeSignature;
      uninherited = 1;
    }

    return [
      startTime,
      beatLength,
      timeSignature,
      sampleSet,
      customIndex,
      volume,
      uninherited,
      effects,
    ].join(",");
  }
  static updateActualPoints(group) {
    let timingPoint = null;

    group.controlPoints.forEach((point) => {
      if (
        point.pointType === ControlPointType.DifficultyPoint &&
        !point.isRedundant(this.lastDifficultyPoint)
      ) {
        this.lastDifficultyPoint = point;
      }

      if (
        point.pointType === ControlPointType.EffectPoint &&
        !point.isRedundant(this.lastEffectPoint)
      ) {
        this.lastEffectPoint = point;
      }

      if (
        point.pointType === ControlPointType.SamplePoint &&
        !point.isRedundant(this.lastSamplePoint)
      ) {
        this.lastSamplePoint = point;
      }

      if (point.pointType === ControlPointType.TimingPoint) {
        timingPoint = point;
      }
    });

    return {
      timingPoint,
      difficultyPoint: this.lastDifficultyPoint,
      effectPoint: this.lastEffectPoint,
      samplePoint: this.lastSamplePoint,
    };
  }
}
BeatmapTimingPointEncoder.lastDifficultyPoint = null;
BeatmapTimingPointEncoder.lastEffectPoint = null;
BeatmapTimingPointEncoder.lastSamplePoint = null;

class ReplayEncoder {
  static encodeLifeBar(frames) {
    if (!frames.length) {
      return "";
    }

    return frames.map((f) => `${f.startTime}|${f.health}`).join(",");
  }
  static encodeReplayFrames(frames, beatmap) {
    const encoded = [];

    if (frames) {
      let lastTime = 0;

      frames.forEach((frame) => {
        let _a, _b, _c;
        const time = Math.round(frame.startTime);
        const legacyFrame = this._getLegacyFrame(frame, beatmap);
        const encodedData = [
          time - lastTime,
          (_a =
            legacyFrame === null || legacyFrame === void 0
              ? void 0
              : legacyFrame.mouseX) !== null && _a !== void 0
            ? _a
            : 0,
          (_b =
            legacyFrame === null || legacyFrame === void 0
              ? void 0
              : legacyFrame.mouseY) !== null && _b !== void 0
            ? _b
            : 0,
          (_c =
            legacyFrame === null || legacyFrame === void 0
              ? void 0
              : legacyFrame.buttonState) !== null && _c !== void 0
            ? _c
            : ReplayButtonState.None,
        ];

        encoded.push(encodedData.join("|"));
        lastTime = time;
      });
    }

    encoded.push("-12345|0|0|0");

    return encoded.join(",");
  }
  static _getLegacyFrame(frame, beatmap) {
    if (frame instanceof LegacyReplayFrame) {
      return frame;
    }

    const convertibleFrame = frame;

    if (convertibleFrame.toLegacy) {
      return convertibleFrame.toLegacy(beatmap);
    }

    throw new Error(
      "Some of the replay frames can not be converted to the legacy format!"
    );
  }
}

const textDecoder = new TextDecoder();

function concatBufferViews(list) {
  if (list.length <= 0) {
    return new Uint8Array(0);
  }

  const bufferLength = list.reduce((len, buf) => len + buf.byteLength, 0);
  const arrayBuffer = new Uint8Array(bufferLength);

  list.reduce((offset, view) => {
    arrayBuffer.set(new Uint8Array(view.buffer), offset);

    return offset + view.byteLength;
  }, 0);

  return arrayBuffer;
}

function stringifyBuffer(data) {
  if (typeof data === "string") {
    return data;
  }

  return textDecoder.decode(data);
}

class SerializationWriter {
  constructor() {
    this._bytesWritten = 0;
    this._views = [];
  }
  get bytesWritten() {
    return this._bytesWritten;
  }
  finish() {
    return concatBufferViews(this._views);
  }
  writeByte(value) {
    return this._update(1, new Uint8Array([value]));
  }
  writeBytes(value) {
    this._bytesWritten += value.byteLength;
    this._views.push(value);

    return value.byteLength;
  }
  writeShort(value) {
    return this._update(2, new Uint16Array([value]));
  }
  writeInteger(value) {
    return this._update(4, new Int32Array([value]));
  }
  writeLong(value) {
    return this._update(8, new BigInt64Array([value]));
  }
  writeDate(date) {
    const epochTicks = BigInt(62135596800000);
    const ticks = BigInt(date.getTime());

    return this.writeLong((ticks + epochTicks) * BigInt(1e4));
  }
  writeString(value) {
    if (value.length === 0) {
      return this.writeByte(0x00);
    }

    let bytesWritten = this.writeByte(0x0b);

    bytesWritten += this.writeULEB128(value.length);

    const view = new Uint8Array(value.length);

    for (let i = 0; i < value.length; ++i) {
      view[i] = value.charCodeAt(i);
      bytesWritten++;
    }

    this._update(bytesWritten, view);

    return bytesWritten;
  }
  writeULEB128(value) {
    let byte = 0;
    let bytesWritten = 0;

    do {
      byte = value & 0x7f;
      value >>= 7;

      if (value !== 0) {
        byte |= 0x80;
      }

      bytesWritten += this.writeByte(byte);
    } while (value !== 0);

    return bytesWritten;
  }
  _update(bytesWritten, buffer) {
    this._bytesWritten += bytesWritten;
    this._views.push(buffer);

    return bytesWritten;
  }
}

class BeatmapEncoder {
  async encodeToPath(path, beatmap) {
    if (!path.endsWith(FileFormat.Beatmap)) {
      path += FileFormat.Beatmap;
    }

    try {
      await browserFSOperation(browserFSOperation(path), { recursive: true });
      await browserFSOperation(path, await this.encodeToString(beatmap));
    } catch (err) {
      const reason = err.message || err;
      throw new Error(`Failed to encode a beatmap: ${reason}`);
    }
  }
  encodeToString(beatmap) {
    let _a;

    if (
      !(beatmap === null || beatmap === void 0 ? void 0 : beatmap.fileFormat)
    ) {
      return "";
    }

    const fileFormat =
      (_a = beatmap.fileFormat) !== null && _a !== void 0
        ? _a
        : BeatmapEncoder.FIRST_LAZER_VERSION;
    const encoded = [
      `osu file format v${fileFormat}`,
      BeatmapGeneralEncoder.encodeGeneralSection(beatmap),
      BeatmapEditorEncoder.encodeEditorSection(beatmap),
      BeatmapMetadataEncoder.encodeMetadataSection(beatmap),
      BeatmapDifficultyEncoder.encodeDifficultySection(beatmap),
      BeatmapEventEncoder.encodeEventSection(beatmap),
      BeatmapTimingPointEncoder.encodeControlPoints(beatmap),
      BeatmapColorEncoder.encodeColors(beatmap),
      BeatmapHitObjectEncoder.encodeHitObjects(beatmap),
    ];

    return encoded.join("\n\n") + "\n";
  }
}
BeatmapEncoder.FIRST_LAZER_VERSION = 128;

class StoryboardEncoder {
  async encodeToPath(path, storyboard) {
    if (!path.endsWith(FileFormat.Storyboard)) {
      path += FileFormat.Storyboard;
    }

    try {
      await browserFSOperation(browserFSOperation(path), { recursive: true });
      await browserFSOperation(path, await this.encodeToString(storyboard));
    } catch (err) {
      const reason = err.message || err;
      throw new Error(`Failed to encode a storyboard: ${reason}`);
    }
  }
  encodeToString(storyboard) {
    if (!(storyboard instanceof Storyboard)) {
      return "";
    }

    const encoded = [
      `osu file format v${storyboard.fileFormat}`,
      StoryboardEventEncoder.encodeEventSection(storyboard),
    ];

    return encoded.join("\n\n") + "\n";
  }
}

class LZMA {
  static async decompress(data) {
    try {
      return await this._lzma.decompress(data);
    } catch {
      return "";
    }
  }
  static async compress(data) {
    try {
      return await this._lzma.compress(data, 1);
    } catch {
      return new Uint8Array([]);
    }
  }
}
LZMA._lzma = getLZMAInstance();

function getLZMAInstance() {
  return {
    decompress(data) {
      return new Promise((res, rej) => {
        decompress(data, (result, err) => {
          err
            ? rej(err)
            : res(
                typeof result === "string"
                  ? result
                  : new Uint8Array(result).toString()
              );
        });
      });
    },
    compress(data) {
      return new Promise((res, rej) => {
        compress(data, 1, (result, err) => {
          err ? rej(err) : res(new Uint8Array(result));
        });
      });
    },
  };
}

class ScoreEncoder {
  async encodeToPath(path, score, beatmap) {
    if (!path.endsWith(FileFormat.Replay)) {
      path += FileFormat.Replay;
    }

    const data = await this.encodeToBuffer(score, beatmap);

    try {
      await browserFSOperation(browserFSOperation(path), { recursive: true });
      await browserFSOperation(path, new Uint8Array(data));
    } catch (err) {
      const reason = err.message || err;
      throw new Error(`Failed to encode a score: ${reason}`);
    }
  }
  async encodeToBuffer(score, beatmap) {
    let _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;

    if (
      typeof ((_a =
        score === null || score === void 0 ? void 0 : score.info) === null ||
      _a === void 0
        ? void 0
        : _a.id) !== "number"
    ) {
      return new Uint8Array();
    }

    const writer = new SerializationWriter();

    try {
      writer.writeByte(score.info.rulesetId);
      writer.writeInteger(
        (_c =
          (_b = score.replay) === null || _b === void 0
            ? void 0
            : _b.gameVersion) !== null && _c !== void 0
          ? _c
          : ScoreEncoder.DEFAULT_GAME_VERSION
      );
      writer.writeString(
        (_d = score.info.beatmapHashMD5) !== null && _d !== void 0 ? _d : ""
      );
      writer.writeString(score.info.username);
      writer.writeString(
        (_f =
          (_e = score.replay) === null || _e === void 0
            ? void 0
            : _e.hashMD5) !== null && _f !== void 0
          ? _f
          : ""
      );
      writer.writeShort(score.info.count300);
      writer.writeShort(score.info.count100);
      writer.writeShort(score.info.count50);
      writer.writeShort(score.info.countGeki);
      writer.writeShort(score.info.countKatu);
      writer.writeShort(score.info.countMiss);
      writer.writeInteger(score.info.totalScore);
      writer.writeShort(score.info.maxCombo);
      writer.writeByte(Number(score.info.perfect));
      writer.writeInteger(
        ((_h =
          (_g = score.info.mods) === null || _g === void 0
            ? void 0
            : _g.bitwise) !== null && _h !== void 0
          ? _h
          : Number(score.info.rawMods)) || 0
      );
      writer.writeString(
        ReplayEncoder.encodeLifeBar(
          (_k =
            (_j = score.replay) === null || _j === void 0
              ? void 0
              : _j.lifeBar) !== null && _k !== void 0
            ? _k
            : []
        )
      );
      writer.writeDate(score.info.date);

      if (score.replay) {
        const replayData = ReplayEncoder.encodeReplayFrames(
          score.replay.frames,
          beatmap
        );
        const encodedData = await LZMA.compress(replayData);

        writer.writeInteger(encodedData.byteLength);
        writer.writeBytes(encodedData);
      } else {
        writer.writeInteger(0);
      }

      writer.writeLong(BigInt(score.info.id));

      return writer.finish();
    } catch (err) {
      const reason = err.message || err;
      throw new Error(`Failed to encode a score: '${reason}'`);
    }
  }
}
ScoreEncoder.DEFAULT_GAME_VERSION = 20230621;

class BeatmapHitObjectDecoder {
  static handleLine(line, beatmap, offset) {
    const data = line.split(",").map((v) => v.trim());
    const hitType = Parsing.parseInt(data[3]);
    const hitObject = this.createHitObject(hitType);

    hitObject.startX = Parsing.parseInt(data[0], Parsing.MAX_COORDINATE_VALUE);
    hitObject.startY = Parsing.parseInt(data[1], Parsing.MAX_COORDINATE_VALUE);
    hitObject.startTime = Parsing.parseFloat(data[2]) + offset;
    hitObject.hitType = hitType;
    hitObject.hitSound = Parsing.parseInt(data[4]);

    const bankInfo = new SampleBank();

    this.addExtras(
      data.slice(5),
      hitObject,
      bankInfo,
      offset,
      beatmap.fileFormat
    );
    this.addComboOffset(hitObject, beatmap);

    if (hitObject.samples.length === 0) {
      hitObject.samples = this.convertSoundType(hitObject.hitSound, bankInfo);
    }

    beatmap.hitObjects.push(hitObject);
  }
  static addComboOffset(hitObject, beatmap) {
    const isStandard = beatmap.originalMode === 0;
    const isCatch = beatmap.originalMode === 2;

    if (!isStandard && !isCatch) {
      return;
    }

    const comboObject = hitObject;
    const comboOffset = Math.trunc(
      (hitObject.hitType & HitType.ComboOffset) >> 4
    );
    const newCombo = !!(hitObject.hitType & HitType.NewCombo);

    if (
      hitObject.hitType & HitType.Normal ||
      hitObject.hitType & HitType.Slider
    ) {
      comboObject.isNewCombo = newCombo || this._forceNewCombo;
      comboObject.comboOffset = comboOffset + this._extraComboOffset;
      this._forceNewCombo = false;
      this._extraComboOffset = 0;
    }

    if (hitObject.hitType & HitType.Spinner) {
      this._forceNewCombo = beatmap.fileFormat <= 8 || newCombo || false;
      this._extraComboOffset += comboOffset;
    }
  }
  static addExtras(data, hitObject, bankInfo, offset, fileFormat) {
    if (hitObject.hitType & HitType.Normal && data.length > 0) {
      this.readCustomSampleBanks(data[0], bankInfo);
    }

    if (hitObject.hitType & HitType.Slider) {
      return this.addSliderExtras(data, hitObject, bankInfo, fileFormat);
    }

    if (hitObject.hitType & HitType.Spinner) {
      return this.addSpinnerExtras(data, hitObject, bankInfo, offset);
    }

    if (hitObject.hitType & HitType.Hold) {
      return this.addHoldExtras(data, hitObject, bankInfo, offset);
    }
  }
  static addSliderExtras(extras, slider, bankInfo, fileFormat) {
    const pathString = extras[0];
    const offset = slider.startPosition;
    const repeats = Parsing.parseInt(extras[1]);

    if (slider.repeats > 9000) {
      throw new Error("Repeat count is way too high");
    }

    slider.repeats = Math.max(0, repeats - 1);
    slider.path.controlPoints = this.convertPathString(
      pathString,
      offset,
      fileFormat
    );
    slider.path.curveType = slider.path.controlPoints[0].type;

    if (extras.length > 2) {
      const length = Parsing.parseFloat(
        extras[2],
        Parsing.MAX_COORDINATE_VALUE
      );

      slider.path.expectedDistance = Math.max(0, length);
    }

    if (extras.length > 5) {
      this.readCustomSampleBanks(extras[5], bankInfo);
    }

    slider.samples = this.convertSoundType(slider.hitSound, bankInfo);
    slider.nodeSamples = this.getSliderNodeSamples(extras, slider, bankInfo);
  }
  static addSpinnerExtras(extras, spinner, bankInfo, offset) {
    spinner.endTime = Parsing.parseInt(extras[0]) + offset;

    if (extras.length > 1) {
      this.readCustomSampleBanks(extras[1], bankInfo);
    }
  }
  static addHoldExtras(extras, hold, bankInfo, offset) {
    hold.endTime = hold.startTime;

    if (extras.length > 0 && extras[0]) {
      const ss = extras[0].split(":");

      hold.endTime = Math.max(hold.endTime, Parsing.parseFloat(ss[0])) + offset;
      this.readCustomSampleBanks(ss.slice(1).join(":"), bankInfo);
    }
  }
  static getSliderNodeSamples(extras, slider, bankInfo) {
    const nodes = slider.repeats + 2;
    const nodeBankInfos = [];

    for (let i = 0; i < nodes; ++i) {
      nodeBankInfos.push(bankInfo.clone());
    }

    if (extras.length > 4 && extras[4].length > 0) {
      const sets = extras[4].split("|");

      for (let i = 0; i < nodes; ++i) {
        if (i >= sets.length) {
          break;
        }

        this.readCustomSampleBanks(sets[i], nodeBankInfos[i]);
      }
    }

    const nodeSoundTypes = [];

    for (let i = 0; i < nodes; ++i) {
      nodeSoundTypes.push(slider.hitSound);
    }

    if (extras.length > 3 && extras[3].length > 0) {
      const adds = extras[3].split("|");

      for (let i = 0; i < nodes; ++i) {
        if (i >= adds.length) {
          break;
        }

        nodeSoundTypes[i] = parseInt(adds[i]) || HitSound.None;
      }
    }

    const nodeSamples = [];

    for (let i = 0; i < nodes; i++) {
      nodeSamples.push(
        this.convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])
      );
    }

    return nodeSamples;
  }
  static convertPathString(pathString, offset, fileFormat) {
    const pathSplit = pathString.split("|").map((p) => p.trim());
    const controlPoints = [];
    let startIndex = 0;
    let endIndex = 0;
    let isFirst = true;

    while (++endIndex < pathSplit.length) {
      if (pathSplit[endIndex].length > 1) {
        continue;
      }

      const points = pathSplit.slice(startIndex, endIndex);
      const endPoint =
        endIndex < pathSplit.length - 1 ? pathSplit[endIndex + 1] : null;
      const convertedPoints = this.convertPoints(
        points,
        endPoint,
        isFirst,
        offset,
        fileFormat
      );

      for (const point of convertedPoints) {
        controlPoints.push(...point);
      }

      startIndex = endIndex;
      isFirst = false;
    }

    if (endIndex > startIndex) {
      const points = pathSplit.slice(startIndex, endIndex);
      const convertedPoints = this.convertPoints(
        points,
        null,
        isFirst,
        offset,
        fileFormat
      );

      for (const point of convertedPoints) {
        controlPoints.push(...point);
      }
    }

    return controlPoints;
  }
  static *convertPoints(points, endPoint, isFirst, offset, fileFormat) {
    const readOffset = isFirst ? 1 : 0;
    const endPointLength = endPoint !== null ? 1 : 0;
    const vertices = [];

    if (readOffset === 1) {
      vertices[0] = new PathPoint();
    }

    for (let i = 1; i < points.length; ++i) {
      vertices[readOffset + i - 1] = readPoint(points[i], offset);
    }

    if (endPoint !== null) {
      vertices[vertices.length - 1] = readPoint(endPoint, offset);
    }

    let type = this.convertPathType(points[0]);

    if (type === PathType.PerfectCurve) {
      if (vertices.length !== 3) {
        type = PathType.Bezier;
      } else if (isLinear(vertices)) {
        type = PathType.Linear;
      }
    }

    vertices[0].type = type;

    let startIndex = 0;
    let endIndex = 0;

    while (++endIndex < vertices.length - endPointLength) {
      if (
        !vertices[endIndex].position.equals(vertices[endIndex - 1].position)
      ) {
        continue;
      }

      const isStableBeatmap = fileFormat < BeatmapEncoder.FIRST_LAZER_VERSION;

      if (type === PathType.Catmull && endIndex > 1 && isStableBeatmap) {
        continue;
      }

      if (endIndex === vertices.length - endPointLength - 1) {
        continue;
      }

      vertices[endIndex - 1].type = type;
      yield vertices.slice(startIndex, endIndex);
      startIndex = endIndex + 1;
    }

    if (endIndex > startIndex) {
      yield vertices.slice(startIndex, endIndex);
    }

    function readPoint(point, offset) {
      const coords = point.split(":").map((v) => {
        return Math.trunc(Parsing.parseFloat(v, Parsing.MAX_COORDINATE_VALUE));
      });
      const pos = new Vector2(coords[0], coords[1]).fsubtract(offset);

      return new PathPoint(pos);
    }

    function isLinear(p) {
      const yx =
        (p[1].position.y - p[0].position.y) *
        (p[2].position.x - p[0].position.x);
      const xy =
        (p[1].position.x - p[0].position.x) *
        (p[2].position.y - p[0].position.y);
      const acceptableDifference = 0.001;

      return Math.abs(yx - xy) < acceptableDifference;
    }
  }
  static convertPathType(type) {
    switch (type) {
      default:
      case "C":
        return PathType.Catmull;
      case "B":
        return PathType.Bezier;
      case "L":
        return PathType.Linear;
      case "P":
        return PathType.PerfectCurve;
    }
  }
  static readCustomSampleBanks(hitSample, bankInfo) {
    if (!hitSample) {
      return;
    }

    const split = hitSample.split(":");

    bankInfo.normalSet = Parsing.parseInt(split[0]);
    bankInfo.additionSet = Parsing.parseInt(split[1]);

    if (bankInfo.additionSet === SampleSet.None) {
      bankInfo.additionSet = bankInfo.normalSet;
    }

    if (split.length > 2) {
      bankInfo.customIndex = Parsing.parseInt(split[2]);
    }

    if (split.length > 3) {
      bankInfo.volume = Math.max(0, Parsing.parseInt(split[3]));
    }

    bankInfo.filename = split.length > 4 ? split[4] : "";
  }
  static convertSoundType(type, bankInfo) {
    if (bankInfo.filename) {
      const sample = new HitSample();

      sample.filename = bankInfo.filename;
      sample.volume = bankInfo.volume;

      return [sample];
    }

    const soundTypes = [new HitSample()];

    soundTypes[0].hitSound = HitSound[HitSound.Normal];
    soundTypes[0].sampleSet = SampleSet[bankInfo.normalSet];
    soundTypes[0].isLayered =
      type !== HitSound.None && !(type & HitSound.Normal);

    if (type & HitSound.Finish) {
      const sample = new HitSample();

      sample.hitSound = HitSound[HitSound.Finish];
      soundTypes.push(sample);
    }

    if (type & HitSound.Whistle) {
      const sample = new HitSample();

      sample.hitSound = HitSound[HitSound.Whistle];
      soundTypes.push(sample);
    }

    if (type & HitSound.Clap) {
      const sample = new HitSample();

      sample.hitSound = HitSound[HitSound.Clap];
      soundTypes.push(sample);
    }

    soundTypes.forEach((sound, i) => {
      sound.sampleSet =
        i !== 0
          ? SampleSet[bankInfo.additionSet]
          : SampleSet[bankInfo.normalSet];

      sound.volume = bankInfo.volume;
      sound.customIndex = 0;

      if (bankInfo.customIndex >= 2) {
        sound.customIndex = bankInfo.customIndex;
      }
    });

    return soundTypes;
  }
  static createHitObject(hitType) {
    if (hitType & HitType.Normal) {
      return new HittableObject();
    }

    if (hitType & HitType.Slider) {
      return new SlidableObject();
    }

    if (hitType & HitType.Spinner) {
      return new SpinnableObject();
    }

    if (hitType & HitType.Hold) {
      return new HoldableObject();
    }

    throw new Error(`Unknown hit object type: ${hitType}!`);
  }
}
BeatmapHitObjectDecoder._forceNewCombo = false;
BeatmapHitObjectDecoder._extraComboOffset = 0;

class BeatmapMetadataDecoder {
  static handleLine(line, beatmap) {
    const [key, ...values] = line.split(":");
    const value = values.join(":").trim();

    switch (key.trim()) {
      case "Title":
        beatmap.metadata.title = value;
        break;
      case "TitleUnicode":
        beatmap.metadata.titleUnicode = value;
        break;
      case "Artist":
        beatmap.metadata.artist = value;
        break;
      case "ArtistUnicode":
        beatmap.metadata.artistUnicode = value;
        break;
      case "Creator":
        beatmap.metadata.creator = value;
        break;
      case "Version":
        beatmap.metadata.version = value;
        break;
      case "Source":
        beatmap.metadata.source = value;
        break;
      case "Tags":
        beatmap.metadata.tags = value.split(" ");
        break;
      case "BeatmapID":
        beatmap.metadata.beatmapId = Parsing.parseInt(value);
        break;
      case "BeatmapSetID":
        beatmap.metadata.beatmapSetId = Parsing.parseInt(value);
    }
  }
}

class BeatmapTimingPointDecoder {
  static handleLine(line, beatmap, offset) {
    this.controlPoints = beatmap.controlPoints;

    const data = line.split(",");
    let timeSignature = TimeSignature.SimpleQuadruple;
    let sampleSet = SampleSet[SampleSet.None];
    let customIndex = 0;
    let volume = 100;
    let timingChange = true;
    let effects = EffectType.None;

    if (data.length > 2) {
      switch (data.length) {
        default:
        case 8:
          effects = Parsing.parseInt(data[7]);
        case 7:
          timingChange = data[6] === "1";
        case 6:
          volume = Parsing.parseInt(data[5]);
        case 5:
          customIndex = Parsing.parseInt(data[4]);
        case 4:
          sampleSet = SampleSet[Parsing.parseInt(data[3])];
        case 3:
          timeSignature = Parsing.parseInt(data[2]);
      }
    }

    if (timeSignature < 1) {
      throw new Error("The numerator of a time signature must be positive.");
    }

    const startTime = Parsing.parseFloat(data[0]) + offset;
    const beatLength = Parsing.parseFloat(
      data[1],
      Parsing.MAX_PARSE_VALUE,
      true
    );
    let bpmMultiplier = 1;
    let speedMultiplier = 1;

    if (beatLength < 0) {
      speedMultiplier = 100 / -beatLength;
      bpmMultiplier = Math.min(Math.fround(-beatLength), 10000);
      bpmMultiplier = Math.max(10, bpmMultiplier) / 100;
    }

    if (timingChange && Number.isNaN(beatLength)) {
      throw new Error("Beat length cannot be NaN in a timing control point");
    }

    if (timingChange) {
      const timingPoint = new TimingPoint();

      timingPoint.beatLength = beatLength;
      timingPoint.timeSignature = timeSignature;
      this.addControlPoint(timingPoint, startTime, true);
    }

    const difficultyPoint = new DifficultyPoint();

    difficultyPoint.bpmMultiplier = bpmMultiplier;
    difficultyPoint.sliderVelocity = speedMultiplier;
    difficultyPoint.generateTicks = !Number.isNaN(beatLength);
    difficultyPoint.isLegacy = true;
    this.addControlPoint(difficultyPoint, startTime, timingChange);

    const effectPoint = new EffectPoint();

    effectPoint.kiai = (effects & EffectType.Kiai) > 0;
    effectPoint.omitFirstBarLine = (effects & EffectType.OmitFirstBarLine) > 0;

    if (beatmap.originalMode === 1 || beatmap.originalMode === 3) {
      effectPoint.scrollSpeed = speedMultiplier;
    }

    this.addControlPoint(effectPoint, startTime, timingChange);

    const samplePoint = new SamplePoint();

    samplePoint.sampleSet = sampleSet;
    samplePoint.customIndex = customIndex;
    samplePoint.volume = volume;
    this.addControlPoint(samplePoint, startTime, timingChange);
  }
  static addControlPoint(point, time, timingChange) {
    if (time !== this.pendingTime) {
      this.flushPendingPoints();
    }

    timingChange
      ? this.pendingPoints.unshift(point)
      : this.pendingPoints.push(point);

    this.pendingTime = time;
  }
  static flushPendingPoints() {
    const pendingTime = this.pendingTime;
    const pendingPoints = this.pendingPoints;
    const controlPoints = this.controlPoints;
    const pendingTypes = this.pendingTypes;
    let i = pendingPoints.length;

    while (--i >= 0) {
      if (pendingTypes.includes(pendingPoints[i].pointType)) {
        continue;
      }

      pendingTypes.push(pendingPoints[i].pointType);
      controlPoints.add(pendingPoints[i], pendingTime);
    }

    this.pendingPoints = [];
    this.pendingTypes = [];
  }
}
BeatmapTimingPointDecoder.pendingTime = 0;
BeatmapTimingPointDecoder.pendingTypes = [];
BeatmapTimingPointDecoder.pendingPoints = [];

class ReplayDecoder {
  static decodeLifeBar(data) {
    if (!data) {
      return [];
    }

    const lifeBarFrames = [];
    const frames = data.split(",");

    for (let i = 0; i < frames.length; ++i) {
      if (!frames[i]) {
        continue;
      }

      const frameData = frames[i].split("|");

      if (frameData.length < 2) {
        continue;
      }

      const frame = this.handleLifeBarFrame(frameData);

      lifeBarFrames.push(frame);
    }

    return lifeBarFrames;
  }
  static handleLifeBarFrame(frameData) {
    return new LifeBarFrame(
      Parsing.parseInt(frameData[0]),
      Parsing.parseFloat(frameData[1])
    );
  }
  static decodeReplayFrames(data) {
    if (!data) {
      return [];
    }

    let lastTime = 0;
    let lastKeyPress = null;
    const replayFrames = [];
    const frames = data.split(",");

    for (let i = 0; i < frames.length; ++i) {
      if (!frames[i]) {
        continue;
      }

      const frameData = frames[i].split("|");

      if (frameData.length < 4) {
        continue;
      }

      if (frameData[0] === "-12345") {
        continue;
      }

      let currentKeyPress = Parsing.parseInt(frameData[3]);
      if (currentKeyPress === lastKeyPress) {
        currentKeyPress = 0; // Set to 0 if key press state hasn't changed
      } else {
        lastKeyPress = currentKeyPress; // Update lastKeyPress with new state
      }

      const replayFrame = new LegacyReplayFrame(
          0.0,
          Parsing.parseFloat(frameData[0]),
          new Vector2(
              Parsing.parseFloat(frameData[1], Parsing.MAX_COORDINATE_VALUE),
              Parsing.parseFloat(frameData[2], Parsing.MAX_COORDINATE_VALUE)
          ),
          currentKeyPress
      );

      lastTime += replayFrame.interval;

      if (i < 2 && replayFrame.mouseX === 256 && replayFrame.mouseY === -500) {
        continue;
      }

      if (replayFrame.interval < 0) {
        continue;
      }

      replayFrame.startTime = lastTime;
      replayFrames.push(replayFrame);
    }

    return replayFrames;
  }

}

class SerializationReader {
  constructor(buffer) {
    this._bytesRead = 0;
    this.view = new DataView(new Uint8Array(buffer).buffer);
  }
  get bytesRead() {
    return this._bytesRead;
  }
  get remainingBytes() {
    return this.view.byteLength - this._bytesRead;
  }
  readByte() {
    return this.view.getUint8(this._bytesRead++);
  }
  readBytes(length) {
    const bytes = this.view.buffer.slice(
      this._bytesRead,
      this._bytesRead + length
    );

    this._bytesRead += length;

    return new Uint8Array(bytes);
  }
  readShort() {
    const value = this.view.getUint16(this._bytesRead, true);

    this._bytesRead += 2;

    return value;
  }
  readInteger() {
    const value = this.view.getInt32(this._bytesRead, true);

    this._bytesRead += 4;

    return value;
  }
  readLong() {
    const value = this.view.getBigInt64(this._bytesRead, true);

    this._bytesRead += 8;

    return value;
  }
  readDate() {
    const epochTicks = 62135596800000;

    return new Date(Number(this.readLong() / BigInt(1e4)) - epochTicks);
  }
  readULEB128() {
    let val = 0;
    let shift = 0;
    let byte = 0;

    do {
      byte = this.readByte();
      val |= (byte & 0x7f) << shift;
      shift += 7;
    } while ((byte & 0x80) !== 0);

    return val;
  }
  readString() {
    if (this.readByte() !== 0x0b) {
      return "";
    }

    const length = this.readULEB128();

    return length > 0 ? stringifyBuffer(this.readBytes(length)) : "";
  }
}

class StoryboardGeneralDecoder {
  static handleLine(line, storyboard) {
    const [key, ...values] = line.split(":").map((v) => v.trim());
    const value = values.join(" ");

    switch (key) {
      case "UseSkinSprites":
        storyboard.useSkinSprites = value === "1";
    }
  }
}

class StoryboardVariableDecoder {
  static handleLine(line, variables) {
    if (!line.startsWith("$")) {
      return;
    }

    const pair = line.split("=");

    if (pair.length === 2) {
      variables.set(pair[0], pair[1].trimEnd());
    }
  }
  static decodeVariables(line, variables) {
    if (!line.includes("$") || !variables.size) {
      return line;
    }

    variables.forEach((value, key) => {
      line = line.replace(key, value);
    });

    return line;
  }
}

class Decoder {
  async _getFileBuffer(path) {
    try {
      await browserFSOperation(path);
    } catch {
      throw new Error("File doesn't exist!");
    }

    try {
      return await browserFSOperation(path);
    } catch {
      throw new Error("File can't be read!");
    }
  }
  async _getFileUpdateDate(path) {
    try {
      return (await browserFSOperation(path)).mtime;
    } catch {
      throw new Error("Failed to get last file update date!");
    }
  }
}

class SectionMap extends Map {
  constructor() {
    super(...arguments);
    this.currentSection = null;
  }
  get(section) {
    let _a;

    return (_a = super.get(section)) !== null && _a !== void 0 ? _a : false;
  }
  set(section, state = true) {
    return super.set(section, state);
  }
  reset() {
    this.forEach((_, key, map) => {
      map.set(key, true);
    });

    this.currentSection = null;

    return this;
  }
  get hasEnabledSections() {
    for (const state of this.values()) {
      if (state) {
        return true;
      }
    }

    return false;
  }
  get isSectionEnabled() {
    return this.currentSection ? this.get(this.currentSection) : false;
  }
}

class SectionDecoder extends Decoder {
  constructor() {
    super(...arguments);
    this._lines = null;
    this._sectionMap = new SectionMap();
  }
  _getLines(data) {
    let lines = null;

    if (data.constructor === Array) {
      lines = data;
    }

    if (!lines || !lines.length) {
      throw new Error("Data not found!");
    }

    return lines;
  }
  _parseLine(line, output) {
    if (this._shouldSkipLine(line)) {
      return LineType.Empty;
    }

    line = this._preprocessLine(line);

    if (line.includes("osu file format v")) {
      return LineType.FileFormat;
    }

    if (line.startsWith("[") && line.endsWith("]")) {
      const section = line.slice(1, -1);

      if (this._sectionMap.currentSection) {
        this._sectionMap.set(this._sectionMap.currentSection, false);
        this._sectionMap.currentSection = null;
      }

      if (!this._sectionMap.hasEnabledSections) {
        return LineType.Break;
      }

      if (section in Section) {
        this._sectionMap.currentSection = section;
      }

      return LineType.Section;
    }

    if (!this._sectionMap.isSectionEnabled) {
      return LineType.Empty;
    }

    try {
      this._parseSectionData(line, output);

      return LineType.Data;
    } catch {
      return LineType.Empty;
    }
  }
  _parseSectionData(line, output) {
    const outputWithColors = output;

    if (this._sectionMap.currentSection !== Section.Colours) {
      return;
    }

    if (
      !(outputWithColors === null || outputWithColors === void 0
        ? void 0
        : outputWithColors.colors)
    ) {
      return;
    }

    BeatmapColorDecoder.handleLine(line, outputWithColors);
  }
  _preprocessLine(line) {
    if (this._sectionMap.currentSection !== Section.Metadata) {
      line = this._stripComments(line);
    }

    return line.trimEnd();
  }
  _shouldSkipLine(line) {
    return typeof line !== "string" || !line || line.startsWith("//");
  }
  _stripComments(line) {
    const index = line.indexOf("//");

    return index > 0 ? line.substring(0, index) : line;
  }
  _reset() {
    this._sectionMap.reset();
    this._lines = null;
  }
  _setEnabledSections(options) {
    this._sectionMap.set(
      Section.Colours,
      options === null || options === void 0 ? void 0 : options.parseColours
    );
  }
}

class StoryboardDecoder extends SectionDecoder {
  constructor() {
    super(...arguments);
    this._variables = new Map();
  }
  async decodeFromPath(firstPath, secondPath) {
    if (
      !firstPath.endsWith(FileFormat.Beatmap) &&
      !firstPath.endsWith(FileFormat.Storyboard)
    ) {
      throw new Error(
        `Wrong format of the first file! Only ${FileFormat.Beatmap} and ${FileFormat.Storyboard} files are supported!`
      );
    }

    if (typeof secondPath === "string") {
      if (!secondPath.endsWith(FileFormat.Storyboard)) {
        throw new Error(
          `Wrong format of the second file! Only ${FileFormat.Storyboard} files are supported as a second argument!`
        );
      }
    }

    try {
      const firstData = await this._getFileBuffer(firstPath);
      const secondData =
        typeof secondPath === "string"
          ? await this._getFileBuffer(firstPath)
          : undefined;

      return this.decodeFromBuffer(firstData, secondData);
    } catch (err) {
      const reason = err.message || err;
      throw new Error(`Failed to decode a storyboard: '${reason}'`);
    }
  }
  decodeFromBuffer(firstBuffer, secondBuffer) {
    const firstString = stringifyBuffer(firstBuffer);
    const secondString = secondBuffer
      ? stringifyBuffer(secondBuffer)
      : undefined;

    return this.decodeFromString(firstString, secondString);
  }
  decodeFromString(firstString, secondString) {
    if (typeof firstString !== "string") {
      firstString = String(firstString);
    }

    if (
      typeof secondString !== "string" &&
      typeof secondString !== "undefined"
    ) {
      secondString = String(secondString);
    }

    const firstData = firstString.split(/\r?\n/);
    const secondData =
      secondString === null || secondString === void 0
        ? void 0
        : secondString.split(/\r?\n/);

    return this.decodeFromLines(firstData, secondData);
  }
  decodeFromLines(firstData, secondData) {
    const storyboard = new Storyboard();

    this._reset();
    this._setEnabledSections();
    this._lines = [
      ...this._getLines(firstData),
      ...(secondData ? this._getLines(secondData) : []),
    ];

    for (let i = 0; i < this._lines.length; ++i) {
      const type = this._parseLine(this._lines[i], storyboard);

      if (type === LineType.Break) {
        break;
      }
    }

    storyboard.variables = this._variables;

    return storyboard;
  }
  _parseLine(line, storyboard) {
    if (line.includes("osu file format v")) {
      storyboard.fileFormat = Parsing.parseInt(line.split("v")[1]);

      return LineType.FileFormat;
    }

    return super._parseLine(line, storyboard);
  }
  _parseSectionData(line, storyboard) {
    switch (this._sectionMap.currentSection) {
      case Section.General:
        return StoryboardGeneralDecoder.handleLine(line, storyboard);
      case Section.Events:
        return StoryboardEventDecoder.handleLine(line, storyboard);
      case Section.Variables:
        return StoryboardVariableDecoder.handleLine(line, this._variables);
    }

    super._parseSectionData(line, storyboard);
  }
  _setEnabledSections() {
    super._setEnabledSections();
    this._sectionMap.set(Section.General);
    this._sectionMap.set(Section.Variables);
    this._sectionMap.set(Section.Events);
  }
  _preprocessLine(line) {
    line = StoryboardVariableDecoder.decodeVariables(line, this._variables);

    return super._preprocessLine(line);
  }
  _reset() {
    super._reset();
    this._sectionMap.reset();
    this._sectionMap.currentSection = Section.Events;
  }
}

class BeatmapDecoder extends SectionDecoder {
  constructor() {
    super(...arguments);
    this._offset = 0;
    this._sbLines = null;
  }
  async decodeFromPath(path, options) {
    if (!path.endsWith(FileFormat.Beatmap)) {
      throw new Error(
        `Wrong file format! Only ${FileFormat.Beatmap} files are supported!`
      );
    }

    try {
      const data = await this._getFileBuffer(path);
      const beatmap = this.decodeFromBuffer(data, options);

      beatmap.fileUpdateDate = await this._getFileUpdateDate(path);

      return beatmap;
    } catch (err) {
      const reason = err.message || err;
      throw new Error(`Failed to decode a beatmap: ${reason}`);
    }
  }
  decodeFromBuffer(data, options) {
    return this.decodeFromString(stringifyBuffer(data), options);
  }
  decodeFromString(str, options) {
    str = typeof str !== "string" ? String(str) : str;

    return this.decodeFromLines(str.split(/\r?\n/), options);
  }
  decodeFromLines(data, options) {
    const beatmap = new Beatmap();

    this._reset();
    this._lines = this._getLines(data);
    this._setEnabledSections(typeof options !== "boolean" ? options : {});
    this._sbLines = this._shouldParseStoryboard(options) ? [] : null;

    const fileFormatLine = this._lines[0].trim();

    if (!fileFormatLine.startsWith("osu file format v")) {
      throw new Error("Not a valid beatmap!");
    }

    for (let i = 0; i < this._lines.length; ++i) {
      const type = this._parseLine(this._lines[i], beatmap);

      if (type === LineType.Break) {
        break;
      }
    }

    BeatmapTimingPointDecoder.flushPendingPoints();

    for (let i = 0; i < beatmap.hitObjects.length; ++i) {
      beatmap.hitObjects[i].applyDefaults(
        beatmap.controlPoints,
        beatmap.difficulty
      );
    }

    beatmap.hitObjects.sort((a, b) => a.startTime - b.startTime);

    if (this._sbLines && this._sbLines.length) {
      const storyboardDecoder = new StoryboardDecoder();

      beatmap.events.storyboard = storyboardDecoder.decodeFromLines(
        this._sbLines
      );
      beatmap.events.storyboard.useSkinSprites = beatmap.general.useSkinSprites;
      beatmap.events.storyboard.colors = beatmap.colors;
      beatmap.events.storyboard.fileFormat = beatmap.fileFormat;
    }

    return beatmap;
  }
  _parseLine(line, beatmap) {
    if (line.includes("osu file format v")) {
      beatmap.fileFormat = Parsing.parseInt(line.split("v")[1]);

      return LineType.FileFormat;
    }

    return super._parseLine(line, beatmap);
  }
  _parseSectionData(line, beatmap) {
    switch (this._sectionMap.currentSection) {
      case Section.General:
        return BeatmapGeneralDecoder.handleLine(line, beatmap, this._offset);
      case Section.Editor:
        return BeatmapEditorDecoder.handleLine(line, beatmap);
      case Section.Metadata:
        return BeatmapMetadataDecoder.handleLine(line, beatmap);
      case Section.Difficulty:
        return BeatmapDifficultyDecoder.handleLine(line, beatmap);
      case Section.Events:
        return BeatmapEventDecoder.handleLine(
          line,
          beatmap,
          this._sbLines,
          this._offset
        );
      case Section.TimingPoints:
        return BeatmapTimingPointDecoder.handleLine(
          line,
          beatmap,
          this._offset
        );
      case Section.HitObjects:
        return BeatmapHitObjectDecoder.handleLine(line, beatmap, this._offset);
    }

    super._parseSectionData(line, beatmap);
  }
  _setEnabledSections(options) {
    super._setEnabledSections(options);
    this._sectionMap.set(
      Section.General,
      options === null || options === void 0 ? void 0 : options.parseGeneral
    );
    this._sectionMap.set(
      Section.Editor,
      options === null || options === void 0 ? void 0 : options.parseEditor
    );
    this._sectionMap.set(
      Section.Metadata,
      options === null || options === void 0 ? void 0 : options.parseMetadata
    );
    this._sectionMap.set(
      Section.Difficulty,
      options === null || options === void 0 ? void 0 : options.parseDifficulty
    );
    this._sectionMap.set(
      Section.Events,
      options === null || options === void 0 ? void 0 : options.parseEvents
    );
    this._sectionMap.set(
      Section.TimingPoints,
      options === null || options === void 0
        ? void 0
        : options.parseTimingPoints
    );
    this._sectionMap.set(
      Section.HitObjects,
      options === null || options === void 0 ? void 0 : options.parseHitObjects
    );
  }
  _shouldParseStoryboard(options) {
    let _a, _b;
    const parsingOptions = options;
    const storyboardFlag =
      (_a =
        parsingOptions === null || parsingOptions === void 0
          ? void 0
          : parsingOptions.parseStoryboard) !== null && _a !== void 0
        ? _a
        : options;
    const parseSb = typeof storyboardFlag === "boolean" ? storyboardFlag : true;
    const parseEvents =
      (_b =
        parsingOptions === null || parsingOptions === void 0
          ? void 0
          : parsingOptions.parseEvents) !== null && _b !== void 0
        ? _b
        : true;

    return parseEvents && parseSb;
  }
}
BeatmapDecoder.EARLY_VERSION_TIMING_OFFSET = 24;

class ScoreDecoder extends Decoder {
  async decodeFromPath(path, parseReplay = true) {
    if (!path.endsWith(FileFormat.Replay)) {
      throw new Error(
        `Wrong file format! Only ${FileFormat.Replay} files are supported!`
      );
    }

    try {
      const data = await this._getFileBuffer(path);

      return await this.decodeFromBuffer(data, parseReplay);
    } catch (err) {
      const reason = err.message || err;
      throw new Error(`Failed to decode a score: '${reason}'`);
    }
  }
  async decodeFromBuffer(buffer, parseReplay = true) {
    const scoreInfo = new ScoreInfo();

    scoreInfo.id = 2398933774;
    scoreInfo.accuracy = 0;
    scoreInfo.count50 = 0;
    scoreInfo.count100 = 0;
    scoreInfo.count300 = 0;
    scoreInfo.countGeki = 0;
    scoreInfo.countKatu = 0;
    scoreInfo.countMiss = 0;
    scoreInfo.totalScore = 0;
    scoreInfo.maxCombo = 0;
    scoreInfo.perfect = false;
    scoreInfo.rawMods = 0;
    scoreInfo.date = new Date();
    scoreInfo.beatmapHashMD5 = "d41d8cd98f00b204e9800998ecf8427e";
    scoreInfo.username = "Unknown";
    scoreInfo.rulesetId = 0;

    let replay = null;

    replay = new Replay();

    replay.rawFrames = buffer;
    replay.frames = ReplayDecoder.decodeReplayFrames(buffer);


    return new Score(scoreInfo, replay);
  }
  _parseScoreId(version, reader) {
    if (version >= 20140721) {
      return Number(reader.readLong());
    }

    if (version >= 20121008) {
      return reader.readInteger();
    }

    return 0;
  }
}

export {
  BeatmapDecoder,
  BeatmapEncoder,
  HittableObject,
  HoldableObject,
  ScoreDecoder,
  ScoreEncoder,
  SlidableObject,
  SpinnableObject,
  StoryboardDecoder,
  StoryboardEncoder,
};
