using Codice.CM.Client.Differences;
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;
        }

        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 class SYNCRECTANGLE
        {
            // 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 class RECTANGLE
        {
            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;
        }
    }
}