#pragma once
#include "stdafx.h"
#include "Snapshotable.h"
#include "APU.h"
#include "BaseExpansionAudio.h"

class Namco163Audio : public BaseExpansionAudio
{
public:
	static constexpr uint32_t AudioRamSize = 0x80;

private:
	uint8_t _internalRam[Namco163Audio::AudioRamSize];
	int16_t _channelOutput[8];
	uint8_t _ramPosition;
	bool _autoIncrement;
	uint8_t _updateCounter;
	int8_t _currentChannel;
	int16_t _lastOutput;
	bool _disableSound;

	enum SoundReg
	{
		FrequencyLow = 0x00,
		PhaseLow = 0x01,
		FrequencyMid = 0x02,
		PhaseMid = 0x03,
		FrequencyHigh = 0x04,
		WaveLength = 0x04,
		PhaseHigh = 0x05,
		WaveAddress = 0x06,
		Volume = 0x07
	};

	uint32_t GetFrequency(int channel)
	{
		uint8_t baseAddr = 0x40 + channel * 0x08;
		return ((_internalRam[baseAddr + SoundReg::FrequencyHigh] & 0x03) << 16) | (_internalRam[baseAddr + SoundReg::FrequencyMid] << 8) | _internalRam[baseAddr + SoundReg::FrequencyLow];
	}

	uint32_t GetPhase(int channel)
	{
		uint8_t baseAddr = 0x40 + channel * 0x08;
		return (_internalRam[baseAddr + SoundReg::PhaseHigh] << 16) | (_internalRam[baseAddr + SoundReg::PhaseMid] << 8) | _internalRam[baseAddr + SoundReg::PhaseLow];
	}

	void SetPhase(int channel, uint32_t phase)
	{
		uint8_t baseAddr = 0x40 + channel * 0x08;
		_internalRam[baseAddr + SoundReg::PhaseHigh] = (phase >> 16) & 0xFF;
		_internalRam[baseAddr + SoundReg::PhaseMid] = (phase >> 8) & 0xFF;
		_internalRam[baseAddr + SoundReg::PhaseLow] = phase & 0xFF;
	}

	uint8_t GetWaveAddress(int channel)
	{
		uint8_t baseAddr = 0x40 + channel * 0x08;
		return _internalRam[baseAddr + SoundReg::WaveAddress];
	}

	uint8_t GetWaveLength(int channel)
	{
		uint8_t baseAddr = 0x40 + channel * 0x08;
		return 256 - (_internalRam[baseAddr + SoundReg::WaveLength] & 0xFC);
	}

	uint8_t GetVolume(int channel)
	{
		uint8_t baseAddr = 0x40 + channel * 0x08;
		return _internalRam[baseAddr + SoundReg::Volume] & 0x0F;
	}
	
	uint8_t GetNumberOfChannels()
	{
		return (_internalRam[0x7F] >> 4) & 0x07;
	}

	void UpdateChannel(int channel)
	{
		uint32_t phase = GetPhase(channel);
		uint32_t freq = GetFrequency(channel);
		uint8_t length = GetWaveLength(channel);
		uint8_t offset = GetWaveAddress(channel);
		uint8_t volume = GetVolume(channel);

		if(length == 0) {
			phase = 0;
		} else {
			phase = (phase + freq) % (length << 16);
		}
		
		uint8_t samplePosition = ((phase >> 16) + offset) & 0xFF;
		int8_t sample;
		if((samplePosition & 0x01)) {
			sample = _internalRam[samplePosition / 2] >> 4;
		} else {
			sample = _internalRam[samplePosition / 2] & 0x0F;
		}

		_channelOutput[channel] = (sample - 8) * volume;
		UpdateOutputLevel();
		SetPhase(channel, phase);
	}

	void UpdateOutputLevel()
	{
		int16_t summedOutput = 0;
		for(int i = 7, min = 7 - GetNumberOfChannels(); i >= min; i--) {
			summedOutput += _channelOutput[i];
		}
		summedOutput /= GetNumberOfChannels() + 1;

		_console->GetApu()->AddExpansionAudioDelta(AudioChannel::Namco163, summedOutput - _lastOutput);
		_lastOutput = summedOutput;
	}

protected:
	void StreamState(bool saving) override
	{
		BaseExpansionAudio::StreamState(saving);

		ArrayInfo<uint8_t> internalRam{ _internalRam, 0x80 };
		ArrayInfo<int16_t> channelOutput{ _channelOutput, 8 };
		Stream(internalRam, channelOutput, _ramPosition, _autoIncrement, _updateCounter, _currentChannel, _lastOutput, _disableSound);
	}

	void ClockAudio() override
	{
		if(!_disableSound) {
			_updateCounter++;
			if(_updateCounter == 15) {
				UpdateChannel(_currentChannel);

				_updateCounter = 0;
				_currentChannel--;
				if(_currentChannel < 7 - GetNumberOfChannels()) {
					_currentChannel = 7;
				}
			}
		}
	}

public:
	Namco163Audio(shared_ptr<Console> console) : BaseExpansionAudio(console)
	{
		memset(_internalRam, 0, sizeof(_internalRam));
		memset(_channelOutput, 0, sizeof(_channelOutput));
		_ramPosition = 0;
		_autoIncrement = false;
		_updateCounter = 0;
		_currentChannel = 7;
		_lastOutput = 0;
		_disableSound = false;
	}

	uint8_t* GetInternalRam()
	{
		return _internalRam;
	}

	void WriteRegister(uint16_t addr, uint8_t value)
	{
		switch(addr & 0xF800) {
			case 0x4800:
				_internalRam[_ramPosition] = value;
				if(_autoIncrement) {
					_ramPosition = (_ramPosition + 1) & 0x7F;
				}
				break;
			case 0xE000:
				_disableSound = (value & 0x40) == 0x40;
				break;
			case 0xF800:
				_ramPosition = value & 0x7F;
				_autoIncrement = (value & 0x80) == 0x80;
				break;

		}
	}

	uint8_t ReadRegister(uint16_t addr)
	{
		uint8_t value = 0;
		switch(addr & 0xF800) {
			case 0x4800: {
				value = _internalRam[_ramPosition];
				if(_autoIncrement) {
					_ramPosition = (_ramPosition + 1) & 0x7F;
				}
				break;
			}
		}

		return value;
	}
};