#### RemiAudio
#### Copyright (C) 2022-2024 Remilia Scarlet <remilia@posteo.jp>
####   Based on https://github.com/superctr/adpcm/blob/master/yma_codec.c
####   Copyright (C) 2019 Ian Karlsson
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU Affero General Public License as published by
#### the Free Software Foundation, either version 3 of the License, or (at your
#### option) any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
#### License for more details.
####
#### You should have received a copy of the GNU Affero General Public License
#### along with this program.  If not, see <https://www.gnu.org/licenses/>.

####
#### YM2610 ADPCM-A Codec
####

module RemiAudio::Codecs
  # A codec for YM2610 ADPCM-A samples.
  class YM2610Codec
    # :nodoc:
    YM2610_STEP_TABLE = [ 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66, 73, 80, 88,
                          97, 107,118,130,143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408,
                          449, 494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552 ]
    YM2610_STEP_ADJUST = [ -1, -1, -1, -1, 2, 5, 7, 9]
    YM2610_DELTAS = [  1,  3,  5,  7,  9,  11,  13,  15,
                      -1, -3, -5, -7, -9, -11, -13, -15 ]

    @history : Int16 = 0
    @stepHistory : UInt8 = 0
    @nib : UInt8 = 0
    @bufSamp : UInt8 = 0
    @srcIdx : UInt8 = 0

    private def reset
      @history = 0
      @stepHistory = 0u8
      @nib = 0u8
      @bufSamp = 0u8
    end

    private def doStep(step : UInt8) : Int16
      stepSize : UInt16 = YM2610_STEP_TABLE[@stepHistory].to_u16!
      delta : Int16 = (YM2610_DELTAS[step & 15] * stepSize.to_i16!).to_i16!.tdiv(8)
      ret : Int16 = (@history + delta) & 0xfff
      newStep : Int8 = 0

      unless (ret & 0x800) == 0
        ret |= ~0xfff
      end

      @history = ret
      newStep = @stepHistory.to_i8! + YM2610_STEP_ADJUST[step & 7]

      @stepHistory = if newStep > 48
                       48u8
                     elsif newStep < 0
                       0u8
                     else
                       newStep.to_u8!
                     end

      ret
    end

    private def doEncStep(input : Int16) : UInt8
      stepSize : UInt16 = YM2610_STEP_TABLE[@stepHistory].to_u16!
      delta : Int16 = input - @history
      samp : UInt8 = 0
      bit : Int8 = 3

      samp = 8 if delta < 0
      delta = delta.abs

      until bit == 0
        bit -= 1
        if delta >= stepSize
          samp |= (1 << bit)
          delta -= stepSize
        end
        stepSize = stepSize >> 1
      end

      doStep(samp)
      samp
    end

    # Converts two signed 16-bit integer samples into one YM2610 ADPCM-A sample.
    def int16ToAdpcm(sample1 : Int16, sample2 : Int16) : UInt8
      ret : UInt8 = 0
      step : UInt8 = 0

      2.times do |i|
        smp = (i == 0 ? sample1 : sample2)
        smp += 8 if smp < 0x7ff8 # Round up
        smp = smp >> 4
        step = doEncStep(smp)

        if @nib != 0
          ret = @bufSamp | (step & 15)
          @nib = 0
        else
          @bufSamp = (step & 15) << 4
          @nib = 1
        end
      end

      ret
    end

    # Converts an array of signed 16-bit integer samples into an array of YM2610
    # ADPCM-A samples.
    def int16ToAdpcm(samples : Array(Int16)) : Array(UInt8)
      reset
      samples.map { |samp| int16ToAdpcm(samp) }
    end

    # Convert a YM2610 ADPCM-A sample into two signed 16-bit integer samples.
    def adpcmToInt16(sample : UInt8) : Tuple(Int16, Int16)
      step : Int8 = 0
      ret1 : Int16 = 0
      ret2 : Int16 = 0

      2.times do |i|
        step = sample.to_i8! << @nib
        step = step >> 4

        if @nib == 0
          @nib = 4
        else
          @nib = 0
        end

        if i == 0
          ret1 = doStep(step.to_u8!) << 4
        else
          ret2 = doStep(step.to_u8!) << 4
        end
      end

      {ret1, ret2}
    end

    # Converts an array of YM2610 ADPCM-A samples into an array of signed 16-bit
    # integer samples.
    def adpcmToInt16(samples : Array(UInt8)) : Array(Int16)
      reset
      samples.map { |samp| adpcmToInt16(samp) }
    end
  end
end
