using System;

namespace VirtualNes.Core
{
    public class APU_MMC5 : APU_INTERFACE
    {
        public const int RECTANGLE_VOL_SHIFT = 8;
        public const int DAOUT_VOL_SHIFT = 6;

        SYNCRECTANGLE sch0 = new SYNCRECTANGLE();
        SYNCRECTANGLE sch1 = new SYNCRECTANGLE();
        RECTANGLE ch0 = new RECTANGLE();
        RECTANGLE ch1 = new RECTANGLE();

        byte reg5010;
        byte reg5011;
        byte reg5015;
        byte sync_reg5015;
        int FrameCycle;
        float cpu_clock;
        int cycle_rate;

        // Tables
        static int[] vbl_length = new int[32];
        static int[] duty_lut = new int[4];

        static int[] decay_lut = new int[16];
        static int[] vbl_lut = new int[32];

        public APU_MMC5()
        {
            // 仮設定
            Reset(APU_INTERFACE.APU_CLOCK, 22050);
        }

        public override void Reset(float fClock, int nRate)
        {
            sch0.ZeroMemory();
            sch1.ZeroMemory();

            reg5010 = reg5011 = reg5015 = 0;

            sync_reg5015 = 0;
            FrameCycle = 0;

            Setup(fClock, nRate);

            for (ushort addr = 0x5000; addr <= 0x5015; addr++)
            {
                Write(addr, 0);
            }
        }

        public override void Setup(float fClock, int nRate)
        {
            cpu_clock = fClock;
            cycle_rate = (int)(fClock * 65536.0f / nRate);

            // Create Tables
            int i;
            int samples = (int)(nRate / 60.0f);
            for (i = 0; i < 16; i++)
                decay_lut[i] = (i + 1) * samples * 5;
            for (i = 0; i < 32; i++)
                vbl_lut[i] = vbl_length[i] * samples * 5;
        }

        public override void Write(ushort addr, byte data)
        {
            switch (addr)
            {
                // MMC5 CH0 rectangle
                case 0x5000:
                    ch0.reg[0] = data;
                    ch0.volume = (byte)(data & 0x0F);
                    ch0.holdnote = (byte)(data & 0x20);
                    ch0.fixed_envelope = (byte)(data & 0x10);
                    ch0.env_decay = decay_lut[data & 0x0F];
                    ch0.duty_flip = duty_lut[data >> 6];
                    break;
                case 0x5001:
                    ch0.reg[1] = data;
                    break;
                case 0x5002:
                    ch0.reg[2] = data;
                    ch0.freq = INT2FIX(((ch0.reg[3] & 0x07) << 8) + data + 1);
                    break;
                case 0x5003:
                    ch0.reg[3] = data;
                    ch0.vbl_length = vbl_lut[data >> 3];
                    ch0.env_vol = 0;
                    ch0.freq = INT2FIX(((data & 0x07) << 8) + ch0.reg[2] + 1);
                    if ((reg5015 & 0x01) != 0)
                        ch0.enable = 0xFF;
                    break;
                // MMC5 CH1 rectangle
                case 0x5004:
                    ch1.reg[0] = data;
                    ch1.volume = (byte)(data & 0x0F);
                    ch1.holdnote = (byte)(data & 0x20);
                    ch1.fixed_envelope = (byte)(data & 0x10);
                    ch1.env_decay = decay_lut[data & 0x0F];
                    ch1.duty_flip = duty_lut[data >> 6];
                    break;
                case 0x5005:
                    ch1.reg[1] = data;
                    break;
                case 0x5006:
                    ch1.reg[2] = data;
                    ch1.freq = INT2FIX(((ch1.reg[3] & 0x07) << 8) + data + 1);
                    break;
                case 0x5007:
                    ch1.reg[3] = data;
                    ch1.vbl_length = vbl_lut[data >> 3];
                    ch1.env_vol = 0;
                    ch1.freq = INT2FIX(((data & 0x07) << 8) + ch1.reg[2] + 1);
                    if ((reg5015 & 0x02) != 0)
                        ch1.enable = 0xFF;
                    break;
                case 0x5010:
                    reg5010 = data;
                    break;
                case 0x5011:
                    reg5011 = data;
                    break;
                case 0x5012:
                case 0x5013:
                case 0x5014:
                    break;
                case 0x5015:
                    reg5015 = data;
                    if ((reg5015 & 0x01) != 0)
                    {
                        ch0.enable = 0xFF;
                    }
                    else
                    {
                        ch0.enable = 0;
                        ch0.vbl_length = 0;
                    }
                    if ((reg5015 & 0x02) != 0)
                    {
                        ch1.enable = 0xFF;
                    }
                    else
                    {
                        ch1.enable = 0;
                        ch1.vbl_length = 0;
                    }
                    break;
            }
        }

        internal void SyncWrite(ushort addr, byte data)
        {
            switch (addr)
            {
                // MMC5 CH0 rectangle
                case 0x5000:
                    sch0.reg[0] = data;
                    sch0.holdnote = (byte)(data & 0x20);
                    break;
                case 0x5001:
                case 0x5002:
                    sch0.reg[addr & 3] = data;
                    break;
                case 0x5003:
                    sch0.reg[3] = data;
                    sch0.vbl_length = vbl_length[data >> 3];
                    if ((sync_reg5015 & 0x01) != 0)
                        sch0.enable = 0xFF;
                    break;
                // MMC5 CH1 rectangle
                case 0x5004:
                    sch1.reg[0] = data;
                    sch1.holdnote = (byte)(data & 0x20);
                    break;
                case 0x5005:
                case 0x5006:
                    sch1.reg[addr & 3] = data;
                    break;
                case 0x5007:
                    sch1.reg[3] = data;
                    sch1.vbl_length = vbl_length[data >> 3];
                    if ((sync_reg5015 & 0x02) != 0)
                        sch1.enable = 0xFF;
                    break;
                case 0x5010:
                case 0x5011:
                case 0x5012:
                case 0x5013:
                case 0x5014:
                    break;
                case 0x5015:
                    sync_reg5015 = data;
                    if ((sync_reg5015 & 0x01) != 0)
                    {
                        sch0.enable = 0xFF;
                    }
                    else
                    {
                        sch0.enable = 0;
                        sch0.vbl_length = 0;
                    }
                    if ((sync_reg5015 & 0x02) != 0)
                    {
                        sch1.enable = 0xFF;
                    }
                    else
                    {
                        sch1.enable = 0;
                        sch1.vbl_length = 0;
                    }
                    break;
            }
        }

        internal byte SyncRead(ushort addr)
        {
            byte data = 0;

            if (addr == 0x5015)
            {
                if ((sch0.enable != 0) && sch0.vbl_length > 0) data |= (1 << 0);
                if ((sch1.enable != 0) && sch1.vbl_length > 0) data |= (1 << 1);
            }

            return data;
        }

        public override bool Sync(int cycles)
        {
            FrameCycle += cycles;
            if (FrameCycle >= 7457 * 5 / 2)
            {
                FrameCycle -= 7457 * 5 / 2;

                if (sch0.enable != 0 && sch0.holdnote == 0)
                {
                    if ((sch0.vbl_length) != 0)
                    {
                        sch0.vbl_length--;
                    }
                }
                if (sch1.enable != 0 && sch1.holdnote == 0)
                {
                    if ((sch1.vbl_length) != 0)
                    {
                        sch1.vbl_length--;
                    }
                }
            }

            return false;
        }

        public override int Process(int channel)
        {
            switch (channel)
            {
                case 0:
                    return RectangleRender(ch0);
                case 1:
                    return RectangleRender(ch1);
                case 2:
                    return reg5011 << DAOUT_VOL_SHIFT;
            }

            return 0;
        }

        public override int GetFreq(int channel)
        {
            if (channel == 0 || channel == 1)
            {
                RECTANGLE ch = null;
                if (channel == 0) ch = ch0;
                else ch = ch1;

                if (ch.enable == 0 || ch.vbl_length <= 0)
                    return 0;
                if (ch.freq < INT2FIX(8))
                    return 0;
                if (ch.fixed_envelope != 0)
                {
                    if (ch.volume == 0)
                        return 0;
                }
                else
                {
                    if ((0x0F - ch.env_vol) == 0)
                        return 0;
                }

                return (int)(256.0f * cpu_clock / (FIX2INT(ch.freq) * 16.0f));
            }

            return 0;
        }

        private int RectangleRender(RECTANGLE ch)
        {
            if (ch.enable == 0 || ch.vbl_length <= 0)
                return 0;

            // vbl length counter
            if (ch.holdnote == 0)
                ch.vbl_length -= 5;

            // envelope unit
            ch.env_phase -= 5 * 4;
            while (ch.env_phase < 0)
            {
                ch.env_phase += ch.env_decay;
                if ((ch.holdnote) != 0)
                    ch.env_vol = (byte)((ch.env_vol + 1) & 0x0F);
                else if (ch.env_vol < 0x0F)
                    ch.env_vol++;
            }

            if (ch.freq < INT2FIX(8))
                return 0;

            int volume;
            if ((ch.fixed_envelope) != 0)
                volume = ch.volume;
            else
                volume = (0x0F - ch.env_vol);

            int output = volume << RECTANGLE_VOL_SHIFT;

            ch.phaseacc -= cycle_rate;
            if (ch.phaseacc >= 0)
            {
                if (ch.adder < ch.duty_flip)
                    ch.output_vol = output;
                else
                    ch.output_vol = -output;
                return ch.output_vol;
            }

            if (ch.freq > cycle_rate)
            {
                ch.phaseacc += ch.freq;
                ch.adder = (ch.adder + 1) & 0x0F;
                if (ch.adder < ch.duty_flip)
                    ch.output_vol = output;
                else
                    ch.output_vol = -output;
            }
            else
            {
                // 加重平均
                int num_times, total;
                num_times = total = 0;
                while (ch.phaseacc < 0)
                {
                    ch.phaseacc += ch.freq;
                    ch.adder = (ch.adder + 1) & 0x0F;
                    if (ch.adder < ch.duty_flip)
                        total += output;
                    else
                        total -= output;
                    num_times++;
                }
                ch.output_vol = total / num_times;
            }

            return ch.output_vol;
        }

        public override uint GetSize()
        {
            //3*sizeof(BYTE) + sizeof(ch0) + sizeof(ch1) + sizeof(sch0) + sizeof(sch1); 源代码似乎少了sync_reg5015的大小
            return 3 + ch0.GetSize() + ch1.GetSize() + 1 + sch0.GetSize() + sch1.GetSize();
        }

        public override void SaveState(StateBuffer buffer)
        {
            buffer.Write(reg5010);
            buffer.Write(reg5011);
            buffer.Write(reg5015);

            ch0.SaveState(buffer);
            ch1.SaveState(buffer);

            buffer.Write(sync_reg5015);
            sch0.SaveState(buffer);
            sch1.SaveState(buffer);
        }

        public class SYNCRECTANGLE : IStateBufferObject
        {
            // For sync
            public byte[] reg = new byte[4];
            public byte enable;
            public byte holdnote;
            public byte[] dummy = new byte[2];
            public int vbl_length;

            public void ZeroMemory()
            {
                Array.Clear(reg, 0, reg.Length);
                enable = 0;
                holdnote = 0;
                Array.Clear(dummy, 0, dummy.Length);
                vbl_length = 0;
            }

            public uint GetSize()
            {
                return 12;
            }

            public void SaveState(StateBuffer buffer)
            {
                buffer.Write(reg);
                buffer.Write(enable);
                buffer.Write(holdnote);
                buffer.Write(dummy);
                buffer.Write(vbl_length);
            }

            public void LoadState(StateReader buffer)
            {
                reg = buffer.Read_bytes(4);
                enable = buffer.Read_byte();
                holdnote = buffer.Read_byte();
                dummy = buffer.Read_bytes(2);
                vbl_length = buffer.Read_int();
            }
        }

        public class RECTANGLE : IStateBufferObject
        {
            public byte[] reg = new byte[4];
            public byte enable;

            public int vbl_length;

            public int phaseacc;
            public int freq;

            public int output_vol;
            public byte fixed_envelope;
            public byte holdnote;
            public byte volume;

            public byte env_vol;
            public int env_phase;
            public int env_decay;

            public int adder;
            public int duty_flip;

            public uint GetSize()
            {
                return 45;
            }

            public void SaveState(StateBuffer buffer)
            {
                buffer.Write(reg);
                buffer.Write(enable);
                buffer.Write(vbl_length);
                buffer.Write(phaseacc);
                buffer.Write(freq);
                buffer.Write(output_vol);
                buffer.Write(fixed_envelope);
                buffer.Write(holdnote);
                buffer.Write(volume);
                buffer.Write(env_vol);
                buffer.Write(env_phase);
                buffer.Write(env_decay);
                buffer.Write(adder);
                buffer.Write(duty_flip);
            }

            public void LoadState(StateReader buffer)
            {
                reg = buffer.Read_bytes(4);
                enable = buffer.Read_byte();
                vbl_length = buffer.Read_int();
                phaseacc = buffer.Read_int();
                freq = buffer.Read_int();
                output_vol = buffer.Read_int();
                fixed_envelope = buffer.Read_byte();
                holdnote = buffer.Read_byte();
                volume = buffer.Read_byte();
                env_vol = buffer.Read_byte();
                env_phase = buffer.Read_int();
                env_decay = buffer.Read_int();
                adder = buffer.Read_int();
                duty_flip = buffer.Read_int();
            }
        }
    }
}