#pragma once

//subchannel processor
//note: this code is not tolerant to subchannel data that violates the Redbook standard

namespace nall::CD {

enum : int { InvalidLBA = 100 * 60 * 75 };

struct BCD {
  static auto encode(uint8_t value) -> uint8_t { return value / 10 << 4 | value % 10; }
  static auto decode(uint8_t value) -> uint8_t { return (value >> 4) * 10 + (value & 15); }
};

struct MSF {
  uint8_t minute;      //00-99
  uint8_t second;      //00-59
  uint8_t frame = -1;  //00-74

  MSF() = default;
  MSF(uint8_t m, uint8_t s, uint8_t f) : minute(m), second(s), frame(f) {}
  MSF(int lba) { *this = fromLBA(lba); }

  explicit operator bool() const {
    return minute <= 99 && second <= 59 && frame <= 74;
  }

  static auto fromBCD(uint8_t minute, uint8_t second, uint8_t frame) -> MSF {
    return {BCD::decode(minute), BCD::decode(second), BCD::decode(frame)};
  }

  static auto fromLBA(int lba) -> MSF {
    if(lba < 0) lba = 100 * 60 * 75 + lba;
    if(lba >= 100 * 60 * 75) return {};
    uint8_t minute = lba / 75 / 60 % 100;
    uint8_t second = lba / 75 % 60;
    uint8_t frame  = lba % 75;
    return {minute, second, frame};
  }

  auto toLBA() const -> int {
    int lba = minute * 60 * 75 + second * 75 + frame;
    if(minute < 90) return lba;
    return -(100 * 60 * 75 - lba);
  }

  //for debugging purposes
  auto toString() const -> string {
    if(!operator bool()) return "??:??:??";
    return {pad(minute, 2, '0'), ":", pad(second, 2, '0'), ":", pad(frame, 2, '0')};
  }
};

struct Index {
  int lba = InvalidLBA;
  int end = InvalidLBA;  //inclusive range

  explicit operator bool() const {
    return lba != InvalidLBA;
  }

  auto inRange(int sector) const -> bool {
    if(lba == InvalidLBA || end == InvalidLBA) return false;
    return sector >= lba && sector <= end;
  }
};

struct Track {
  uint8_t control = 0b1111;  //4-bit
  uint8_t address = 0b1111;  //4-bit
  Index indices[100];
  uint8_t firstIndex = -1;
  uint8_t lastIndex  = -1;

  explicit operator bool() const {
    return (bool)indices[1];
  }

  auto emphasis() const -> bool {
    return control & 1;
  }

  auto copyable() const -> bool {
    return control & 2;
  }

  auto channels() const -> uint {
    if((control & 0b1100) == 0b0000) return 2;
    if((control & 0b1100) == 0b1000) return 4;
    return 0;  //data track or reserved
  }

  auto pregap() const -> int {
    if(!indices[0] || !indices[1]) return InvalidLBA;
    return indices[1].lba - indices[0].lba;
  }

  auto isAudio() const -> bool {
    return channels() != 0;
  }

  auto isData() const -> bool {
    return (control & 0b1100) == 0b0100;
  }

  auto inIndex(int lba) const -> maybe<uint8_t> {
    for(uint8_t index : range(100)) {
      if(indices[index].inRange(lba)) return index;
    }
    return {};
  }

  auto inRange(int lba) const -> bool {
    if(firstIndex > 99 || lastIndex > 99) return false;
    return lba >= indices[firstIndex].lba && lba <= indices[lastIndex].end;
  }

  auto index(uint8_t indexID) -> maybe<Index&> {
    if(indexID < 100 && indices[indexID]) return indices[indexID];
    return {};
  }
};

struct Session {
  Index leadIn;       //00
  Track tracks[100];  //01-99
  Index leadOut;      //aa
  uint8_t firstTrack = -1;
  uint8_t lastTrack  = -1;

  auto inLeadIn(int lba) const -> bool {
    return lba < 0;
  }

  auto inTrack(int lba) const -> maybe<uint8_t> {
    for(uint8_t trackID : range(100)) {
      auto& track = tracks[trackID];
      if(track && track.inRange(lba)) return trackID;
    }
    return {};
  }

  auto inLeadOut(int lba) const -> bool {
    return lba >= leadOut.lba;
  }

  auto track(uint8_t trackID) -> maybe<Track&> {
    if(trackID < 100 && tracks[trackID]) return tracks[trackID];
    return {};
  }

  auto encode(uint sectors) const -> vector<uint8_t> {
    if(sectors < abs(leadIn.lba) + leadOut.lba) return {};  //not enough sectors

    vector<uint8_t> data;
    data.resize(sectors * 96 + 96);  //add one sector for P shift

    auto toP = [&](int lba) -> array_span<uint8_t> {
      //P is encoded one sector later than Q
      return {&data[(lba + abs(leadIn.lba) + 1) * 96], 12};
    };

    auto toQ = [&](int lba) -> array_span<uint8_t> {
      return {&data[(lba + abs(leadIn.lba)) * 96 + 12], 12};
    };

    //lead-in
    int lba = leadIn.lba;
    while(lba < 0) {
      //tracks
      for(uint trackID : range(100)) {
      for(uint repeat : range(3)) {
        auto& track = tracks[trackID];
        if(!track) continue;
        auto q = toQ(lba);
        q[0u] = track.control << 4 | track.address << 0;
        q[1u] = 0x00;
        q[2u] = BCD::encode(trackID);
        auto msf = MSF(lba);
        q[3u] = BCD::encode(msf.minute);
        q[4u] = BCD::encode(msf.second);
        q[5u] = BCD::encode(msf.frame);
        q[6u] = 0x00;
        msf = MSF(track.indices[1].lba);
        q[7u] = BCD::encode(msf.minute);
        q[8u] = BCD::encode(msf.second);
        q[9u] = BCD::encode(msf.frame);
        auto crc16 = CRC16({q, 10});
        q[10u] = crc16 >> 8;
        q[11u] = crc16 >> 0;
        if(++lba >= 0) break;
      }}if(  lba >= 0) break;

      //first track
      for(uint repeat : range(3)) {
        auto q = toQ(lba);
        q[0u] = 0x01;  //control value unverified; address = 1
        q[1u] = 0x00;  //track# = 00 (TOC)
        q[2u] = 0xa0;  //first track
        auto msf = MSF(lba);
        q[3u] = BCD::encode(msf.minute);
        q[4u] = BCD::encode(msf.second);
        q[5u] = BCD::encode(msf.frame);
        q[6u] = 0x00;
        q[7u] = BCD::encode(firstTrack);
        q[8u] = 0x00;
        q[9u] = 0x00;
        auto crc16 = CRC16({q, 10});
        q[10u] = crc16 >> 8;
        q[11u] = crc16 >> 0;
        if(++lba >= 0) break;
      } if(  lba >= 0) break;

      //last track
      for(uint repeat : range(3)) {
        auto q = toQ(lba);
        q[0u] = 0x01;
        q[1u] = 0x00;
        q[2u] = 0xa1;  //last track
        auto msf = MSF(lba);
        q[3u] = BCD::encode(msf.minute);
        q[4u] = BCD::encode(msf.second);
        q[5u] = BCD::encode(msf.frame);
        q[6u] = 0x00;
        q[7u] = BCD::encode(lastTrack);
        q[8u] = 0x00;
        q[9u] = 0x00;
        auto crc16 = CRC16({q, 10});
        q[10u] = crc16 >> 8;
        q[11u] = crc16 >> 0;
        if(++lba >= 0) break;
      } if(  lba >= 0) break;

      //lead-out point
      for(uint repeat : range(3)) {
        auto q = toQ(lba);
        q[0u] = 0x01;
        q[1u] = 0x00;
        q[2u] = 0xa2;  //lead-out point
        auto msf = MSF(lba);
        q[3u] = BCD::encode(msf.minute);
        q[4u] = BCD::encode(msf.second);
        q[5u] = BCD::encode(msf.frame);
        q[6u] = 0x00;
        msf = MSF(leadOut.lba);
        q[7u] = BCD::encode(msf.minute);
        q[8u] = BCD::encode(msf.second);
        q[9u] = BCD::encode(msf.frame);
        auto crc16 = CRC16({q, 10});
        q[10u] = crc16 >> 8;
        q[11u] = crc16 >> 0;
        if(++lba >= 0) break;
      } if(  lba >= 0) break;
    }

    //tracks
    int end = leadOut.lba;
    for(uint8_t trackID : reverse(range(100))) {
      auto& track = tracks[trackID];
      if(!track) continue;

      //indices
      for(uint8_t indexID : reverse(range(100))) {
        auto& index = track.indices[indexID];
        if(!index) continue;

        for(int lba = index.lba; lba < end; lba++) {
          auto p = toP(lba);
          uint8_t byte = indexID == 0 ? 0xff : 0x00;
          for(uint index : range(12)) p[index] = byte;

          auto q = toQ(lba);
          q[0u] = track.control << 4 | track.address << 0;
          q[1u] = BCD::encode(trackID);
          q[2u] = BCD::encode(indexID);
          auto msf = MSF(lba - track.indices[1].lba);
          q[3u] = BCD::encode(msf.minute);
          q[4u] = BCD::encode(msf.second);
          q[5u] = BCD::encode(msf.frame);
          q[6u] = 0x00;
          msf = MSF(lba);
          q[7u] = BCD::encode(msf.minute);
          q[8u] = BCD::encode(msf.second);
          q[9u] = BCD::encode(msf.frame);
          auto crc16 = CRC16({q, 10});
          q[10u] = crc16 >> 8;
          q[11u] = crc16 >> 0;
        }

        end = index.lba;
      }
    }

    //lead-out
    for(int lba : range(sectors - abs(leadIn.lba) - leadOut.lba)) {
      auto p = toP(leadOut.lba + lba);
      uint8_t byte;
      if(lba < 150) {
        //2s start (standard specifies 2-3s start)
        byte = 0x00;
      } else {
        //2hz duty cycle; rounded downward (standard specifies 2% tolerance)
        byte = (lba - 150) / (75 >> 1) & 1 ? 0xff : 0x00;
      }
      for(uint index : range(12)) p[index] = byte;

      auto q = toQ(leadOut.lba + lba);
      q[0u] = 0x01;
      q[1u] = 0xaa;  //lead-out track#
      q[2u] = 0x01;  //lead-out index#
      auto msf = MSF(lba);
      q[3u] = BCD::encode(msf.minute);
      q[4u] = BCD::encode(msf.second);
      q[5u] = BCD::encode(msf.frame);
      q[6u] = 0x00;
      msf = MSF(leadOut.lba + lba);
      q[7u] = BCD::encode(msf.minute);
      q[8u] = BCD::encode(msf.second);
      q[9u] = BCD::encode(msf.frame);
      auto crc16 = CRC16({q, 10});
      q[10u] = crc16 >> 8;
      q[11u] = crc16 >> 0;
    }

    data.resize(data.size() - 96);  //remove padding for P shift
    return data;
  }

  auto decode(array_view<uint8_t> data, uint size, uint leadOutSectors = 0) -> bool {
    *this = {};  //reset session
    //three data[] types supported: subcode Q only, subcode P-W only, data+subcode complete image
    if(size != 12 && size != 96 && size != 2448) return false;

    //determine lead-in sector count
    for(int lba : range(7500)) {  //7500 max sectors scanned
      uint offset = lba * size;
      if(size ==   96) offset += 12;
      if(size == 2448) offset += 12 + 2352;
      if(offset + 12 > data.size()) break;
      auto q = array_view<uint8_t>{&data[offset], 12};
      auto crc16 = CRC16({q, 10});
      if(q[10u] != uint8_t(crc16 >> 8)) continue;
      if(q[11u] != uint8_t(crc16 >> 0)) continue;

      uint8_t control = q[0u] >> 4;
      uint8_t address = q[0u] & 15;
      uint8_t trackID = q[1u];
      if(address != 1) continue;
      if(trackID != 0) continue;

      auto msf = MSF::fromBCD(q[3u], q[4u], q[5u]);
      leadIn.lba = msf.toLBA() - lba;
      break;
    }
    if(leadIn.lba == InvalidLBA || leadIn.lba >= 0) return false;

    auto toQ = [&](int lba) -> array_view<uint8_t> {
      uint offset = (lba + abs(leadIn.lba)) * size;
      if(size ==   96) offset += 12;
      if(size == 2448) offset += 12 + 2352;
      if(offset + 12 > data.size()) return {};
      return {&data[offset], 12};
    };

    //lead-in
    for(int lba = leadIn.lba; lba < 0; lba++) {
      auto q = toQ(lba);
      if(!q) break;
      auto crc16 = CRC16({q, 10});
      if(q[10u] != uint8_t(crc16 >> 8)) continue;
      if(q[11u] != uint8_t(crc16 >> 0)) continue;

      uint8_t control = q[0u] >> 4;
      uint8_t address = q[0u] & 15;
      uint8_t trackID = q[1u];
      if(address != 1) continue;
      if(trackID != 0) continue;

      trackID = BCD::decode(q[2u]);

      if(trackID <=  99) {  //00-99
        auto& track = tracks[trackID];
        track.control = control;
        track.address = address;
        track.indices[1].lba = MSF::fromBCD(q[7u], q[8u], q[9u]).toLBA();
      }

      if(trackID == 100) {  //a0
        firstTrack = BCD::decode(q[7u]);
      }

      if(trackID == 101) {  //a1
        lastTrack = BCD::decode(q[7u]);
      }

      if(trackID == 102) {  //a2
        leadOut.lba = MSF::fromBCD(q[7u], q[8u], q[9u]).toLBA();
      }
    }
    if(leadOut.lba == InvalidLBA) return false;

    //tracks
    for(int lba = 0; lba < leadOut.lba; lba++) {
      auto q = toQ(lba);
      if(!q) break;
      auto crc16 = CRC16({q, 10});
      if(q[10u] != uint8_t(crc16 >> 8)) continue;
      if(q[11u] != uint8_t(crc16 >> 0)) continue;

      uint8_t control = q[0u] >> 4;
      uint8_t address = q[0u] & 15;
      uint8_t trackID = BCD::decode(q[1u]);
      uint8_t indexID = BCD::decode(q[2u]);
      if(address != 1) continue;
      if(trackID > 99) continue;
      if(indexID > 99) continue;

      auto& track = tracks[trackID];
      if(!track) continue;  //track not found?
      auto& index = track.indices[indexID];
      if(index) continue;   //index already decoded?

      index.lba = MSF::fromBCD(q[7u], q[8u], q[9u]).toLBA();
    }

    synchronize(leadOutSectors);
    return true;
  }

  //calculates Index::end variables:
  //needed for Session::isTrack() and Track::isIndex() to function.
  auto synchronize(uint leadOutSectors = 0) -> void {
    leadIn.end = -1;
    int end = leadOut.lba - 1;
    for(uint trackID : reverse(range(100))) {
      auto& track = tracks[trackID];
      if(!track) continue;

      for(uint indexID : reverse(range(100))) {
        auto& index = track.indices[indexID];
        if(!index) continue;

        index.end = end;
        end = index.lba - 1;
      }

      for(uint indexID : range(100)) {
        auto& index = track.indices[indexID];
        if(index) { track.firstIndex = indexID; break; }
      }

      for(uint indexID : reverse(range(100))) {
        auto& index = track.indices[indexID];
        if(index) { track.lastIndex = indexID; break; }
      }
    }
    leadOut.end = leadOut.lba + leadOutSectors - 1;
  }

  //for diagnostic use only
  auto serialize() const -> string {
    string s;
    s.append("session\n");
    s.append("  leadIn: ");
    s.append(MSF(leadIn.lba).toString(), " - ", MSF(leadIn.end).toString(), "\n");
    for(uint trackID : range(100)) {
      auto& track = tracks[trackID];
      if(!track) continue;
      s.append("  track", pad(trackID, 2, '0'));
      if(trackID == firstTrack) s.append(" first");
      if(trackID ==  lastTrack) s.append( " last");
      s.append("\n");
      s.append("    control: ", binary(track.control, 4, '0'), "\n");
      s.append("    address: ", binary(track.address, 4, '0'), "\n");
      for(uint indexID : range(100)) {
        auto& index = track.indices[indexID];
        if(!index) continue;
        s.append("    index", pad(indexID, 2, '0'), ": ");
        s.append(MSF(index.lba).toString(), " - ", MSF(index.end).toString(), "\n");
      }
    }
    s.append("  leadOut: ");
    s.append(MSF(leadOut.lba).toString(), " - ", MSF(leadOut.end).toString(), "\n");
    return s;
  }
};

}
