import 'dart:math' as math;

/// States used by the simplified ADSR envelope inside the OPL2 emulator.
enum EnvelopeState { off, attack, decay, sustain, release }

/// One OPL2 operator, combining waveform generation and envelope progression.
class Opl2Operator {
  double phase = 0.0;
  double phaseIncrement = 0.0;

  double attackRate = 0.01;
  double decayRate = 0.005;
  double sustainLevel = 0.5;
  double releaseRate = 0.005;
  EnvelopeState envState = EnvelopeState.off;
  double envVolume = 0.0;

  double multiplier = 1.0;
  double volume = 1.0;

  /// Selected waveform index as exposed by the OPL2 register set.
  int waveform = 0;

  /// Recomputes oscillator increment from the shared channel base frequency.
  void updateFrequency(double baseFreq) {
    phaseIncrement =
        (baseFreq * multiplier * 2 * math.pi) / Opl2Emulator.sampleRate;
  }

  /// Starts a new note by resetting the phase and entering attack.
  void triggerOn() {
    phase = 0.0;
    envState = EnvelopeState.attack;
  }

  /// Releases the note so the envelope decays back to silence.
  void triggerOff() {
    envState = EnvelopeState.release;
  }

  // Waveform handling mirrors the small set of shapes exposed by the OPL2 chip.
  double _getOscillatorOutput(double currentPhase) {
    // Normalize phase between 0 and 2*pi
    double p = currentPhase % (2 * math.pi);
    if (p < 0) p += 2 * math.pi;

    switch (waveform) {
      case 1: // Half-Sine (silence for the second half)
        return p < math.pi ? math.sin(p) : 0.0;
      case 2: // Absolute-Sine (fully rectified)
        return math.sin(p).abs();
      case 3: // Quarter-Sine / Pulse (raspy pulse)
        return (p % math.pi) < (math.pi / 2.0) ? math.sin(p) : 0.0;
      case 0:
      default: // Standard Sine
        return math.sin(p);
    }
  }

  /// Produces one sample for this operator using [phaseOffset] modulation.
  double getSample(double phaseOffset) {
    switch (envState) {
      case EnvelopeState.attack:
        envVolume += attackRate;
        if (envVolume >= 1.0) {
          envVolume = 1.0;
          envState = EnvelopeState.decay;
        }
        break;
      case EnvelopeState.decay:
        envVolume -= decayRate;
        if (envVolume <= sustainLevel) {
          envVolume = sustainLevel;
          envState = EnvelopeState.sustain;
        }
        break;
      case EnvelopeState.sustain:
        envVolume = sustainLevel;
        break;
      case EnvelopeState.release:
        envVolume -= releaseRate;
        if (envVolume <= 0.0) {
          envVolume = 0.0;
          envState = EnvelopeState.off;
        }
        break;
      case EnvelopeState.off:
        envVolume = 0.0;
        return 0.0;
    }

    // Modulation is expressed as a phase offset, which is how the carrier is
    // driven by the modulator in two-operator FM synthesis.
    double out = _getOscillatorOutput(phase + phaseOffset);

    phase += phaseIncrement;
    if (phase >= 2 * math.pi) phase -= 2 * math.pi;

    return out * envVolume * volume;
  }
}

/// Two-operator OPL2 channel with optional additive mode and self-feedback.
class Opl2Channel {
  Opl2Operator modulator = Opl2Operator();
  Opl2Operator carrier = Opl2Operator();

  int fNum = 0;
  int block = 0;
  bool keyOn = false;

  bool isAdditive = false;
  int feedbackStrength = 0;

  double _prevModOutput1 = 0.0;
  double _prevModOutput2 = 0.0;

  /// Updates both operators after frequency register changes.
  void updateFrequency() {
    double baseFreq = (fNum * math.pow(2, block)) * (49716.0 / 1048576.0);
    modulator.updateFrequency(baseFreq);
    carrier.updateFrequency(baseFreq);
  }

  /// Mixes one audio sample from the channel's current operator state.
  double getSample() {
    if (!keyOn &&
        carrier.envState == EnvelopeState.off &&
        modulator.envState == EnvelopeState.off) {
      return 0.0;
    }

    double feedbackPhase = 0.0;
    if (feedbackStrength > 0) {
      // Feedback reuses the previous modulator outputs to create the harsher
      // timbres that classic OPL instruments rely on.
      double averageMod = (_prevModOutput1 + _prevModOutput2) / 2.0;
      double feedbackFactor =
          math.pow(2, feedbackStrength - 1) * (math.pi / 16.0);
      feedbackPhase = averageMod * feedbackFactor;
    }

    double modOutput = modulator.getSample(feedbackPhase);

    _prevModOutput2 = _prevModOutput1;
    _prevModOutput1 = modOutput;

    double channelOutput = 0.0;

    if (isAdditive) {
      // Additive mode mixes both operators as audible oscillators.
      double carOutput = carrier.getSample(0.0);
      channelOutput = modOutput + carOutput;
    } else {
      // Standard FM mode feeds the modulator into the carrier's phase.
      double carOutput = carrier.getSample(modOutput * 2.0);
      channelOutput = carOutput;
    }

    return channelOutput * 0.1;
  }
}

/// Lightweight pseudo-random noise source for percussion voices.
class Opl2Noise {
  int _seed = 0xFFFF;

  // Simple 16-bit LFSR to match OPL2 noise characteristics
  double next() {
    int bit = ((_seed >> 0) ^ (_seed >> 2) ^ (_seed >> 3) ^ (_seed >> 5)) & 1;
    _seed = (_seed >> 1) | (bit << 15);
    return ((_seed & 1) == 1) ? 1.0 : -1.0;
  }
}

/// Simplified register-driven OPL2 emulator used for IMF playback.
///
/// The implementation focuses on the subset of FM behavior needed by the game
/// assets: melodic channels, rhythm mode, waveform selection, and a practical
/// ADSR envelope approximation.
class Opl2Emulator {
  static const int sampleRate = 44100;

  bool rhythmMode = false;

  // Rhythm mode steals the final three channels and exposes them as drum bits.
  bool bassDrumKey = false;
  bool snareDrumKey = false;
  bool tomTomKey = false;
  bool topCymbalKey = false;
  bool hiHatKey = false;

  final Opl2Noise _noise = Opl2Noise();

  final List<Opl2Channel> channels = List.generate(9, (_) => Opl2Channel());

  // The chip only honors waveform writes after the global enable bit is set.
  bool _waveformSelectionEnabled = false;

  static const List<int> _operatorMap = [
    0,
    1,
    2,
    0,
    1,
    2,
    -1,
    -1,
    3,
    4,
    5,
    3,
    4,
    5,
    -1,
    -1,
    6,
    7,
    8,
    6,
    7,
    8,
  ];

  /// Resolves a register offset to the affected operator, if any.
  Opl2Operator? _getOperator(int offset) {
    if (offset < 0 ||
        offset >= _operatorMap.length ||
        _operatorMap[offset] == -1) {
      return null;
    }
    int channelIdx = _operatorMap[offset];
    bool isCarrier = (offset % 8) >= 3;
    return isCarrier
        ? channels[channelIdx].carrier
        : channels[channelIdx].modulator;
  }

  /// Applies a single OPL2 register write.
  void writeRegister(int reg, int data) {
    // --- 0x01: Test / Waveform Enable ---
    if (reg == 0x01) {
      // Bit 5 must be set to 1 to allow waveform changes
      _waveformSelectionEnabled = (data & 0x20) != 0;
    }
    // --- 0x20 - 0x35: Multiplier ---
    else if (reg >= 0x20 && reg <= 0x35) {
      var op = _getOperator(reg - 0x20);
      if (op != null) {
        int mult = data & 0x0F;
        op.multiplier = mult == 0 ? 0.5 : mult.toDouble();
      }
    }
    // --- 0x40 - 0x55: Total Level (Volume) ---
    else if (reg >= 0x40 && reg <= 0x55) {
      var op = _getOperator(reg - 0x40);
      if (op != null) {
        op.volume = 1.0 - ((data & 0x3F) / 63.0);
      }
    }
    // --- 0x60 - 0x75: Attack & Decay ---
    else if (reg >= 0x60 && reg <= 0x75) {
      var op = _getOperator(reg - 0x60);
      if (op != null) {
        int attack = (data & 0xF0) >> 4;
        int decay = data & 0x0F;
        op.attackRate = attack == 0 ? 0.0 : (attack / 15.0) * 0.05;
        op.decayRate = decay == 0 ? 0.0 : (decay / 15.0) * 0.005;
      }
    }
    // --- 0x80 - 0x95: Sustain & Release ---
    else if (reg >= 0x80 && reg <= 0x95) {
      var op = _getOperator(reg - 0x80);
      if (op != null) {
        int sustain = (data & 0xF0) >> 4;
        int release = data & 0x0F;
        op.sustainLevel = 1.0 - (sustain / 15.0);
        op.releaseRate = release == 0 ? 0.0 : (release / 15.0) * 0.005;
      }
    }
    // --- 0xA0 - 0xA8: F-Number (Lower 8 bits) ---
    else if (reg >= 0xA0 && reg <= 0xA8) {
      int channelIdx = reg - 0xA0;
      channels[channelIdx].fNum = (channels[channelIdx].fNum & 0x300) | data;
      channels[channelIdx].updateFrequency();
    }
    // --- 0xB0 - 0xB8: Key-On, Block, F-Number (Upper 2 bits) ---
    else if (reg >= 0xB0 && reg <= 0xB8) {
      int channelIdx = reg - 0xB0;
      Opl2Channel channel = channels[channelIdx];

      bool newKeyOn = (data & 0x20) != 0;
      channel.block = (data & 0x1C) >> 2;
      channel.fNum = (channel.fNum & 0xFF) | ((data & 0x03) << 8);
      channel.updateFrequency();

      if (newKeyOn && !channel.keyOn) {
        channel.keyOn = true;
        channel.modulator.triggerOn();
        channel.carrier.triggerOn();
      } else if (!newKeyOn && channel.keyOn) {
        channel.keyOn = false;
        channel.modulator.triggerOff();
        channel.carrier.triggerOff();
      }
    }
    // --- 0xC0 - 0xC8: Feedback & Connection ---
    else if (reg >= 0xC0 && reg <= 0xC8) {
      int channelIdx = reg - 0xC0;
      channels[channelIdx].isAdditive = (data & 0x01) != 0;
      channels[channelIdx].feedbackStrength = (data & 0x0E) >> 1;
    }
    // --- 0xE0 - 0xF5: Waveform Selection ---
    else if (reg >= 0xE0 && reg <= 0xF5) {
      // The chip ignores this register if the master enable bit wasn't set in 0x01
      if (_waveformSelectionEnabled) {
        var op = _getOperator(reg - 0xE0);
        if (op != null) {
          // Bottom 2 bits determine the waveform (0-3)
          op.waveform = data & 0x03;
        }
      }
    } else if (reg == 0xBD) {
      rhythmMode = (data & 0x20) != 0; // Bit 5: Rhythm Enable

      // Bits 0-4: Key-On for individual drums
      bassDrumKey = (data & 0x10) != 0;
      snareDrumKey = (data & 0x08) != 0;
      tomTomKey = (data & 0x04) != 0;
      topCymbalKey = (data & 0x02) != 0;
      hiHatKey = (data & 0x01) != 0;
    }
  }

  /// Generates one normalized mono sample from the current register state.
  double generateSample() {
    double mixedOutput = 0.0;

    // Channels 0-5 always act as normal melodic FM channels
    for (int i = 0; i < 6; i++) {
      mixedOutput += channels[i].getSample();
    }

    if (!rhythmMode) {
      // Standard mode keeps the final channels melodic.
      for (int i = 6; i < 9; i++) {
        mixedOutput += channels[i].getSample();
      }
    } else {
      // Rhythm mode repurposes the last three channels into drum voices.
      mixedOutput += _generateBassDrum();
      mixedOutput += _generateSnareAndHiHat();
      mixedOutput += _generateTomAndCymbal();
    }

    return mixedOutput.clamp(-1.0, 1.0);
  }

  /// Generates the bass drum voice routed through channel 6.
  double _generateBassDrum() {
    if (!bassDrumKey) return 0.0;
    // Bass drum uses standard FM (Mod -> Car) but usually with very low frequency
    return channels[6].getSample();
  }

  /// Generates the combined snare and hi-hat voices from channel 7.
  double _generateSnareAndHiHat() {
    double snareOut = 0.0;
    double hiHatOut = 0.0;

    // Snare uses Channel 7 Modulator
    if (snareDrumKey) {
      // The Snare is a mix of a periodic tone and white noise
      double noise = _noise.next();
      snareOut = channels[7].modulator.getSample(0.0) * noise;
    }

    // Hi-Hat uses Channel 7 Carrier
    if (hiHatKey) {
      // Hi-Hats are almost pure high-frequency noise
      hiHatOut =
          _noise.next() *
          channels[7].carrier.envVolume *
          channels[7].carrier.volume;
    }

    return (snareOut + hiHatOut) * 0.1;
  }

  /// Generates the combined tom and cymbal voices from channel 8.
  double _generateTomAndCymbal() {
    double tomOut = 0.0;
    double cymbalOut = 0.0;

    // Tom-tom uses Channel 8 Modulator
    if (tomTomKey) {
      // Toms are basically just a melodic sine wave with a fast decay
      tomOut = channels[8].modulator.getSample(0.0);
    }

    // Top Cymbal uses Channel 8 Carrier
    if (topCymbalKey) {
      // Cymbals use the carrier but are usually phase-modulated by noise
      double noise = _noise.next();
      cymbalOut = channels[8].carrier.getSample(noise * 2.0);
    }

    return (tomOut + cymbalOut) * 0.1;
  }
}
