#include <md/md.hpp>

namespace higan::MegaDrive {

YM2612 ym2612;
#include "io.cpp"
#include "timer.cpp"
#include "channel.cpp"
#include "constants.cpp"
#include "serialization.cpp"

auto YM2612::load(Node::Object parent) -> void {
  node = parent->append<Node::Component>("YM2612");

  stream = node->append<Node::Stream>("YM2612");
  stream->setChannels(2);
  stream->setFrequency(system.frequency() / 7.0 / 144.0);
  stream->addHighPassFilter(  20.0, 1);
  stream->addLowPassFilter (2840.0, 1);
}

auto YM2612::unload() -> void {
  node = {};
  stream = {};
}

auto YM2612::main() -> void {
  sample();

  timerA.run();
  timerB.run();

  if(lfo.enable && ++lfo.divider == lfoDividers[lfo.rate]) {
    lfo.divider = 0;
    lfo.clock++;
    for(auto& channel : channels) {
      for(auto& op : channel.operators) {
        op.updatePhase();  //due to vibrato
        op.updateLevel();  //due to tremolo
      }
    }
  }

  if(++envelope.divider == 3) {
    envelope.divider = 0;
    envelope.clock++;
  }

  for(auto& channel : channels) {
    for(auto& op : channel.operators) {
      op.runPhase();
      if(envelope.divider) continue;
      op.runEnvelope();
    }
  }

  step(144);
}

auto YM2612::sample() -> void {
  int left = 0;
  int right = 0;

  for(auto& channel : channels) {
    auto& op = channel.operators;

    const int modMask = -(1 << 1);
    const int sumMask = -(1 << 5);
    const int outMask = -(1 << 5);

    auto old = [&](uint n) -> int { return op[n].prior  & modMask; };
    auto mod = [&](uint n) -> int { return op[n].output & modMask; };
    auto out = [&](uint n) -> int { return op[n].output & sumMask; };

    auto wave = [&](uint n, uint modulation) -> int {
      int x = (modulation >> 1) + (op[n].phase.value >> 10);
      int y = sine[x & 0x3ff] + op[n].outputLevel;
      return y < 0x2000 ? pow2[y & 0x1ff] << 2 >> (y >> 9) : 0;
    };

    int feedback = modMask & op[0].output + op[0].prior >> 9 - channel.feedback;
    int accumulator = 0;

    for(auto n : range(4)) op[n].prior = op[n].output;

    op[0].output = wave(0, feedback * (channel.feedback > 0));

    if(channel.algorithm == 0) {
      //0 -> 1 -> 2 -> 3
      op[1].output = wave(1, mod(0));
      op[2].output = wave(2, old(1));
      op[3].output = wave(3, mod(2));
      accumulator += out(3);
    }

    if(channel.algorithm == 1) {
      //(0 + 1) -> 2 -> 3
      op[1].output = wave(1, 0);
      op[2].output = wave(2, mod(0) + old(1));
      op[3].output = wave(3, mod(2));
      accumulator += out(3);
    }

    if(channel.algorithm == 2) {
      //0 + (1 -> 2) -> 3
      op[1].output = wave(1, 0);
      op[2].output = wave(2, old(1));
      op[3].output = wave(3, mod(0) + mod(2));
      accumulator += out(3);
    }

    if(channel.algorithm == 3) {
      //(0 -> 1) + 2 -> 3
      op[1].output = wave(1, mod(0));
      op[2].output = wave(2, 0);
      op[3].output = wave(3, mod(1) + mod(2));
      accumulator += out(3);
    }

    if(channel.algorithm == 4) {
      //(0 -> 1) + (2 -> 3)
      op[1].output = wave(1, mod(0));
      op[2].output = wave(2, 0);
      op[3].output = wave(3, mod(2));
      accumulator += out(1) + out(3);
    }

    if(channel.algorithm == 5) {
      //0 -> (1 + 2 + 3)
      op[1].output = wave(1, mod(0));
      op[2].output = wave(2, old(0));
      op[3].output = wave(3, mod(0));
      accumulator += out(1) + out(2) + out(3);
    }

    if(channel.algorithm == 6) {
      //(0 -> 1) + 2 + 3
      op[1].output = wave(1, mod(0));
      op[2].output = wave(2, 0);
      op[3].output = wave(3, 0);
      accumulator += out(1) + out(2) + out(3);
    }

    if(channel.algorithm == 7) {
      //0 + 1 + 2 + 3
      op[1].output = wave(1, 0);
      op[2].output = wave(2, 0);
      op[3].output = wave(3, 0);
      accumulator += out(0) + out(1) + out(2) + out(3);
    }

    int voiceData = sclamp<14>(accumulator) & outMask;
    if(dac.enable && (&channel == &channels[5])) voiceData = (int)dac.sample - 0x80 << 6;

    if(channel.leftEnable ) left  += voiceData;
    if(channel.rightEnable) right += voiceData;
  }

  stream->sample(sclamp<16>(left) / 32768.0, sclamp<16>(right) / 32768.0);
}

auto YM2612::step(uint clocks) -> void {
  Thread::step(clocks);
  Thread::synchronize(cpu, apu);
}

auto YM2612::power(bool reset) -> void {
  Thread::create(system.frequency() / 7.0, {&YM2612::main, this});

  io = {};
  lfo = {};
  dac = {};
  envelope = {};
  timerA = {};
  timerB = {};
  for(auto& channel : channels) channel.power();

  const uint positive = 0;
  const uint negative = 1;

  for(int x = 0; x <= 0xff; x++) {
    int y = -256 * log(sin((2 * x + 1) * Math::Pi / 1024)) / log(2) + 0.5;
    sine[0x000 + x] = positive + (y << 1);
    sine[0x1ff - x] = positive + (y << 1);
    sine[0x200 + x] = negative + (y << 1);
    sine[0x3ff - x] = negative + (y << 1);
  }

  for(int y = 0; y <= 0xff; y++) {
    int z = 1024 * pow(2, (0xff - y) / 256.0) + 0.5;
    pow2[positive + (y << 1)] = +z;
    pow2[negative + (y << 1)] = ~z;  //not -z
  }
}

}
