package org.jpc.emulator.peripheral;

import org.jpc.emulator.AbstractHardwareComponent;
import org.jpc.emulator.HardwareComponent;
import org.jpc.emulator.motherboard.IODevice;
import org.jpc.emulator.motherboard.IOPortHandler;
import org.jpc.j2se.Option;
import org.jpc.support.Clock;

public class Adlib extends AbstractHardwareComponent implements IODevice
{
    static private final int HW_OPL2 = 0;
    static private final int HW_DUALOPL2 = 1;
    static private final int HW_OPL3 = 2;

    private boolean ioportRegistered;
    private boolean single;
    private Clock timeSource;

    static private class RawHeader {
        /*Bit8u*/byte[] id=new byte[8];				/* 0x00, "DBRAWOPL" */
        /*Bit16u*/int versionHigh;			/* 0x08, size of the data following the m */
        /*Bit16u*/int versionLow;			/* 0x0a, size of the data following the m */
        /*Bit32u*/long commands;			/* 0x0c, Bit32u amount of command/data pairs */
        /*Bit32u*/long milliseconds;		/* 0x10, Bit32u Total milliseconds of data in this chunk */
        /*Bit8u*/short hardware;				/* 0x14, Bit8u Hardware Type 0=opl2,1=dual-opl2,2=opl3 */
        /*Bit8u*/short format;				/* 0x15, Bit8u Format 0=cmd/data interleaved, 1 maybe all cdms, followed by all data */
        /*Bit8u*/short compression;			/* 0x16, Bit8u Compression Type, 0 = No Compression */
        /*Bit8u*/short delay256;				/* 0x17, Bit8u Delay 1-256 msec command */
        /*Bit8u*/short delayShift8;			/* 0x18, Bit8u (delay + 1)*256 */
        /*Bit8u*/short conversionTableSize;	/* 0x191, Bit8u Raw Conversion Table size */
    }

    static final private class Timer {
        double start;
        double delay;
        boolean enabled, overflow, masked;
        /*Bit8u*/short counter;
        Timer() {
            masked = false;
            overflow = false;
            enabled = false;
            counter = 0;
            delay = 0;
        }
        //Call update before making any further changes
        void Update( double time ) {
            if ( !enabled || delay==0 )
                return;
            double deltaStart = time - start;
            //Only set the overflow flag when not masked
            if ( deltaStart >= 0 && !masked ) {
                overflow = true;
            }
        }
        //On a reset make sure the start is in sync with the next cycle
        void Reset(double time ) {
            overflow = false;
            if ( delay==0 || !enabled )
                return;
            double delta = (time - start);
            double rem = delta % delay;
            double next = delay - rem;
            start = time + next;
        }
        void Stop( ) {
            enabled = false;
        }
        void Start(double time, /*Bits*/int scale ) {
            //Don't enable again
            if ( enabled ) {
                return;
            }
            enabled = true;
            delay = 0.001 * (256 - counter ) * scale;
            start = time + delay;
        }

    }

//    static private final class Capture {
//        //127 entries to go from raw data to registers
//        /*Bit8u*/short ToReg[127];
//        //How many entries in the ToPort are used
//        /*Bit8u*/short RawUsed;
//        //256 entries to go from port index to raw data
//        /*Bit8u*/short ToRaw[256];
//        /*Bit8u*/short delay256;
//        /*Bit8u*/short delayShift8;
//        RawHeader header;
//
//        FILE*	handle;				//File used for writing
//        /*Bit32u*/long	startTicks;			//Start used to check total raw length on end
//        /*Bit32u*/long	lastTicks;			//Last ticks when last last cmd was added
//        /*Bit8u*/short	buf[1024];	//16 added for delay commands and what not
//        /*Bit32u*/long	bufUsed;
//        /*Bit8u*/short	cmd[2];				//Last cmd's sent to either ports
//        boolean	doneOpl3;
//        boolean	doneDualOpl2;
//
//        RegisterCache* cache;
//
//        void MakeEntry( /*Bit8u*/short reg, /*Bit8u*/short& raw ) {
//            ToReg[ raw ] = reg;
//            ToRaw[ reg ] = raw;
//            raw++;
//        }
//        void MakeTables( void ) {
//            /*Bit8u*/short index = 0;
//            memset( ToReg, 0xff, sizeof ( ToReg ) );
//            memset( ToRaw, 0xff, sizeof ( ToRaw ) );
//            //Select the entries that are valid and the index is the mapping to the index entry
//            MakeEntry( 0x01, index );					//0x01: Waveform select
//            MakeEntry( 0x04, index );					//104: Four-Operator Enable
//            MakeEntry( 0x05, index );					//105: OPL3 Mode Enable
//            MakeEntry( 0x08, index );					//08: CSW / NOTE-SEL
//            MakeEntry( 0xbd, index );					//BD: Tremolo Depth / Vibrato Depth / Percussion Mode / BD/SD/TT/CY/HH On
//            //Add the 32 byte range that hold the 18 operators
//            for ( int i = 0 ; i < 24; i++ ) {
//                if ( (i & 7) < 6 ) {
//                    MakeEntry(0x20 + i, index );		//20-35: Tremolo / Vibrato / Sustain / KSR / Frequency Multiplication Facto
//                    MakeEntry(0x40 + i, index );		//40-55: Key Scale Level / Output Level
//                    MakeEntry(0x60 + i, index );		//60-75: Attack Rate / Decay Rate
//                    MakeEntry(0x80 + i, index );		//80-95: Sustain Level / Release Rate
//                    MakeEntry(0xe0 + i, index );		//E0-F5: Waveform Select
//                }
//            }
//            //Add the 9 byte range that hold the 9 channels
//            for ( int i = 0 ; i < 9; i++ ) {
//                MakeEntry(0xa0 + i, index );			//A0-A8: Frequency Number
//                MakeEntry(0xb0 + i, index );			//B0-B8: Key On / Block Number / F-Number(hi /*Bits*/int)
//                MakeEntry(0xc0 + i, index );			//C0-C8: FeedBack Modulation Factor / Synthesis Type
//            }
//            //Store the amount of bytes the table contains
//            RawUsed = index;
//    //		assert( RawUsed <= 127 );
//            delay256 = RawUsed;
//            delayShift8 = RawUsed+1;
//        }
//
//        void ClearBuf( void ) {
//            fwrite( buf, 1, bufUsed, handle );
//            header.commands += bufUsed / 2;
//            bufUsed = 0;
//        }
//        void AddBuf( /*Bit8u*/short raw, /*Bit8u*/short val ) {
//            buf[bufUsed++] = raw;
//            buf[bufUsed++] = val;
//            if ( bufUsed >= sizeof( buf ) ) {
//                ClearBuf();
//            }
//        }
//        void AddWrite( /*Bit32u*/long regFull, /*Bit8u*/short val ) {
//            /*Bit8u*/short regMask = regFull & 0xff;
//            /*
//                Do some special checks if we're doing opl3 or dualopl2 commands
//                Although you could pretty much just stick to always doing opl3 on the player side
//            */
//            //Enabling opl3 4op modes will make us go into opl3 mode
//            if ( header.hardware != HW_OPL3 && regFull == 0x104 && val && (*cache)[0x105] ) {
//                header.hardware = HW_OPL3;
//            }
//            //Writing a keyon to a 2nd address enables dual opl2 otherwise
//            //Maybe also check for rhythm
//            if ( header.hardware == HW_OPL2 && regFull >= 0x1b0 && regFull <=0x1b8 && val ) {
//                header.hardware = HW_DUALOPL2;
//            }
//            /*Bit8u*/short raw = ToRaw[ regMask ];
//            if ( raw == 0xff )
//                return;
//            if ( regFull & 0x100 )
//                raw |= 128;
//            AddBuf( raw, val );
//        }
//        void WriteCache( void  ) {
//            /*Bitu*/int i, val;
//            /* Check the registers to add */
//            for (i=0;i<256;i++) {
//                //Skip the note on entries
//                if (i>=0xb0 && i<=0xb8)
//                    continue;
//                val = (*cache)[ i ];
//                if (val) {
//                    AddWrite( i, val );
//                }
//                val = (*cache)[ 0x100 + i ];
//                if (val) {
//                    AddWrite( 0x100 + i, val );
//                }
//            }
//        }
//        void InitHeader( void ) {
//            memset( &header, 0, sizeof( header ) );
//            memcpy( header.id, "DBRAWOPL", 8 );
//            header.versionLow = 0;
//            header.versionHigh = 2;
//            header.delay256 = delay256;
//            header.delayShift8 = delayShift8;
//            header.conversionTableSize = RawUsed;
//        }
//        void CloseFile( void ) {
//            if ( handle ) {
//                ClearBuf();
//                /* Endianize the header and write it to beginning of the file */
//                var_write( &header.versionHigh, header.versionHigh );
//                var_write( &header.versionLow, header.versionLow );
//                var_write( &header.commands, header.commands );
//                var_write( &header.milliseconds, header.milliseconds );
//                fseek( handle, 0, SEEK_SET );
//                fwrite( &header, 1, sizeof( header ), handle );
//                fclose( handle );
//                handle = 0;
//            }
//        }
//    public:
//        boolean DoWrite( /*Bit32u*/long regFull, /*Bit8u*/short val ) {
//            /*Bit8u*/short regMask = regFull & 0xff;
//            //Check the raw index for this register if we actually have to save it
//            if ( handle ) {
//                /*
//                    Check if we actually care for this to be logged, else just ignore it
//                */
//                /*Bit8u*/short raw = ToRaw[ regMask ];
//                if ( raw == 0xff ) {
//                    return true;
//                }
//                /* Check if this command will not just replace the same value
//                   in a reg that doesn't do anything with it
//                */
//                if ( (*cache)[ regFull ] == val )
//                    return true;
//                /* Check how much time has passed */
//                /*Bitu*/int passed = PIC_Ticks - lastTicks;
//                lastTicks = PIC_Ticks;
//                header.milliseconds += passed;
//
//                //if ( passed > 0 ) LOG_MSG( "Delay %d", passed ) ;
//
//                // If we passed more than 30 seconds since the last command, we'll restart the the capture
//                if ( passed > 30000 ) {
//                    CloseFile();
//                    goto skipWrite;
//                }
//                while (passed > 0) {
//                    if (passed < 257) {			//1-256 millisecond delay
//                        AddBuf( delay256, passed - 1 );
//                        passed = 0;
//                    } else {
//                        /*Bitu*/int shift = (passed >> 8);
//                        passed -= shift << 8;
//                        AddBuf( delayShift8, shift - 1 );
//                    }
//                }
//                AddWrite( regFull, val );
//                return true;
//            }
//    skipWrite:
//            //Not yet capturing to a file here
//            //Check for commands that would start capturing, if it's not one of them return
//            if ( !(
//                //note on in any channel
//                ( regMask>=0xb0 && regMask<=0xb8 && (val&0x020) ) ||
//                //Percussion mode enabled and a note on in any percussion instrument
//                ( regMask == 0xbd && ( (val&0x3f) > 0x20 ) )
//            )) {
//                return true;
//            }
//            handle = OpenCaptureFile("Raw Opl",".dro");
//            if (!handle)
//                return false;
//            InitHeader();
//            //Prepare space at start of the file for the header
//            fwrite( &header, 1, sizeof(header), handle );
//            /* write the Raw To Reg table */
//            fwrite( &ToReg, 1, RawUsed, handle );
//            /* Write the cache of last commands */
//            WriteCache( );
//            /* Write the command that triggered this */
//            AddWrite( regFull, val );
//            //Init the timing information for the next commands
//            lastTicks = PIC_Ticks;
//            startTicks = PIC_Ticks;
//            return true;
//        }
//        Capture( RegisterCache* _cache ) {
//            cache = _cache;
//            handle = 0;
//            bufUsed = 0;
//            MakeTables();
//        }
//        ~Capture() {
//            CloseFile();
//        }
//
//    };

    private final class Chip {
        //Last selected register
        Timer[] timer = new Timer[2];
        public Chip() {
            for (int i=0;i<timer.length;i++) {
                timer[i] = new Timer();
            }
        }
        //Check for it being a write to the timer
        boolean Write(/*Bit32u*/int addr, /*Bit8u*/short val) {
            switch ( addr ) {
            case 0x02:
                timer[0].counter = val;
                return true;
            case 0x03:
                timer[1].counter = val;
                return true;
            case 0x04:
                double time = timeSource.getEmulatedNanos();//Pic.PIC_FullIndex();
                if ((val & 0x80)!=0) {
                    timer[0].Reset( time );
                    timer[1].Reset( time );
                } else {
                    timer[0].Update( time );
                    timer[1].Update( time );
                    if ((val & 0x1)!=0) {
                        timer[0].Start( time, 80 );
                    } else {
                        timer[0].Stop( );
                    }
                    timer[0].masked = (val & 0x40) > 0;
                    if ( timer[0].masked )
                        timer[0].overflow = false;
                    if ((val & 0x2)!=0) {
                        timer[1].Start( time, 320 );
                    } else {
                        timer[1].Stop( );
                    }
                    timer[1].masked = (val & 0x20) > 0;
                    if ( timer[1].masked )
                        timer[1].overflow = false;

                }
                return true;
            }
            return false;
        }
        //Read the current timer state, will use current double
        /*Bit8u*/short Read() {
            double time = timeSource.getEmulatedNanos();//Pic.PIC_FullIndex();
            timer[0].Update( time );
            timer[1].Update( time );
            /*Bit8u*/short ret = 0;
            //Overflow won't be set if a channel is masked
            if ( timer[0].overflow ) {
                ret |= 0x40;
                ret |= 0x80;
            }
            if ( timer[1].overflow ) {
                ret |= 0x20;
                ret |= 0x80;
            }
            return ret;
        }
    }

//The type of handler this is
    static final private int MODE_OPL2 = 0;
    static final private int MODE_DUALOPL2 = 1;
    static final private int MODE_OPL3 = 2;

    static public interface Handler {
        //Write an address to a chip, returns the address the chip sets
        public /*Bit32u*/long WriteAddr( /*Bit32u*/int port, /*Bit8u*/short val );
        //Write to a specific register in the chip
        public void WriteReg( /*Bit32u*/int addr, /*Bit8u*/short val );
        //Generate a certain amount of samples
        public void Generate( Mixer.MixerChannel chan, /*Bitu*/int samples );
        //Initialize at a specific sample rate and mode
        public void Init( /*Bitu*/long rate );
    }

//The cache for 2 chips or an opl3
//    typedef /*Bit8u*/short RegisterCache[512];

    private class Module
    {
//        private IoHandler.IO_ReadHandleObject[] ReadHandler = new IoHandler.IO_ReadHandleObject[3];
//        private IoHandler.IO_WriteHandleObject[] WriteHandler = new IoHandler.IO_WriteHandleObject[3];
        private Mixer.MixerObject mixerObject = new Mixer.MixerObject();

        public Module()
        {
//            for (int i=0;i<ReadHandler.length;i++)
//                ReadHandler[i] = new IoHandler.IO_ReadHandleObject();
//            for (int i=0;i<WriteHandler.length;i++)
//                WriteHandler[i] = new IoHandler.IO_WriteHandleObject();
            for (int i=0;i<chip.length;i++) {
                chip[i] = new Chip();
            }
            reg.normal = 0;
//            capture = null;

            /*Bitu*/int base = Option.sbbase.intValue(SBlaster.BASE, 16);
            /*Bitu*/int rate = Option.oplrate.intValue(SBlaster.OPL_RATE);
            //Make sure we can't select lower than 8000 to prevent fixed point issues
            if ( rate < 8000 )
                rate = 8000;
            String oplemu = Option.oplemu.value(SBlaster.OPLEMU);

            mixerChan = mixerObject.Install(OPL_CallBack,rate,"FM");
            mixerChan.SetScale(2.0f);
            if (oplemu.equals("fast")) {
                handler = new DbOPL.Handler();
            } else if (oplemu.equals("compat")) {
                System.out.println("OPLEMU compat not implemented");
//                if ( oplmode == OPL_opl2 ) {
//                    handler = new OPL2::Handler();
//                } else {
//                    handler = new OPL3::Handler();
//                }
                handler = new DbOPL.Handler();
            } else {
                handler = new DbOPL.Handler();
            }
            handler.Init( rate );
            switch (SBlaster.oplmode) {
                case 2:
                    single = true;
                    Init(MODE_OPL2);
                    break;
                case 3:
                    Init(MODE_DUALOPL2);
                    single = false;
                    break;
                case 4:
                    Init(MODE_OPL3);
                    single = false;
                    break;
                default:
                    single = false;
            }
            //0x388 range
//            WriteHandler[0].Install(0x388,OPL_Write,IoHandler.IO_MB, 4 );
//            ReadHandler[0].Install(0x388,OPL_Read,IoHandler.IO_MB, 4 );
//            //0x220 range
//            if ( !single ) {
//                WriteHandler[1].Install(base,OPL_Write,IoHandler.IO_MB, 4 );
//                ReadHandler[1].Install(base,OPL_Read,IoHandler.IO_MB, 4 );
//            }
//            //0x228 range
//            WriteHandler[2].Install(base+8,OPL_Write,IoHandler.IO_MB, 2);
//            ReadHandler[2].Install(base+8,OPL_Read,IoHandler.IO_MB, 1);

//            MAPPER_AddHandler(OPL_SaveRawEvent,MK_f7,MMOD1|MMOD2,"caprawopl","Cap OPL");
        }

                //Mode we're running in
        private int mode;
        //Last selected address in the chip for the different modes
        private class Reg {
            /*Bit32u*/int normal;
            /*Bit8u*/short dual(int index) {
                if (index == 0)
                    return (short)(normal & 0xFF);
                else
                    return (short)((normal >> 8) & 0xFF);
            }
            /*Bit8u*/void dual(int index, int value) {
                if (index == 0) {
                    normal &= 0xFFFFFF00;
                    normal |= value & 0xFF;
                } else {
                    normal &= 0xFFFF00FF;
                    normal |= (value << 8) & 0xFF;
                }
            }
        }

        private Reg reg = new Reg();

        private void CacheWrite( /*Bit32u*/int reg, /*Bit8u*/short val )
        {
            //Store it into the cache
            cache[ reg ] = val;
        }

        private void DualWrite( /*Bit8u*/short index, /*Bit8u*/short reg, /*Bit8u*/short val )
        {
            //Make sure you don't use opl3 features
            //Don't allow write to disable opl3		
            if ( reg == 5 ) {
                return;
            }
            //Only allow 4 waveforms
            if ( reg >= 0xE0 ) {
                val &= 3;
            }
            //Write to the timer?
            if ( chip[index].Write( reg, val ) )
                return;
            //Enabling panning
            if ( reg >= 0xc0 && reg <=0xc8 ) {
                val &= 0x0f;
                val |= index!=0 ? 0xA0 : 0x50;
            }
            /*Bit32u*/int fullReg = reg + (index!=0 ? 0x100 : 0);
            handler.WriteReg( fullReg, val );
            CacheWrite( fullReg, val );
        }
        public Mixer.MixerChannel mixerChan;
        public /*Bit32u*/long lastUsed;				//Ticks when adlib was last used to turn of mixing after a few second

        public Handler handler;				//Handler that will generate the sound
//        public RegisterCache cache;
        public short[] cache = new short[512];
//        public Capture capture;
        public Chip[] chip = new Chip[2];

        //Handle port writes
        public void PortWrite(/*Bitu*/int port, /*Bitu*/short val) {
            //Keep track of last write time
            lastUsed = timeSource.getEmulatedMicros();//Pic.PIC_Ticks;
            //Maybe only enable with a keyon?
            if ( !mixerChan.enabled ) {
                mixerChan.Enable(true);
            }
            if ((port & 1)!=0) {
                switch ( mode ) {
                case MODE_OPL2:
                case MODE_OPL3:
                    if ( !chip[0].Write( reg.normal, val ) ) {
                        handler.WriteReg( reg.normal, val );
                        CacheWrite( reg.normal, val );
                    }
                    break;
                case MODE_DUALOPL2:
                    //Not a 0x??8 port, then write to a specific port
                    if ((port & 0x8)==0) {
                        /*Bit8u*/short index = (short)(( port & 2 ) >> 1);
                        DualWrite( index, reg.dual(index), val );
                    } else {
                        //Write to both ports
                        DualWrite( (short)0, reg.dual(0), val );
                        DualWrite( (short)1, reg.dual(1), val );
                    }
                    break;
                }
            } else {
                //Ask the handler to write the address
                //Make sure to clip them in the right range
                switch ( mode ) {
                case MODE_OPL2:
                    reg.normal = (int)handler.WriteAddr( port, val ) & 0xff;
                    break;
                case MODE_OPL3:
                    reg.normal = (int)handler.WriteAddr( port, val ) & 0x1ff;
                    break;
                case MODE_DUALOPL2:
                    //Not a 0x?88 port, when write to a specific side
                    if ((port & 0x8)==0) {
                        /*Bit8u*/int index = ( port & 2 ) >> 1;
                        reg.dual(index, val & 0xff);
                    } else {
                        reg.dual(0, val & 0xff);
                        reg.dual(1, val & 0xff);
                    }
                    break;
                }
            }
        }

        public /*Bitu*/int PortRead(/*Bitu*/int port)  {
            switch ( mode ) {
            case MODE_OPL2:
                //We allocated 4 ports, so just return -1 for the higher ones
                if ((port & 3)==0) {
                    //Make sure the low /*Bits*/int are 6 on opl2
                    return chip[0].Read() | 0x6;
                } else {
                    return 0xff;
                }
            case MODE_OPL3:
                //We allocated 4 ports, so just return -1 for the higher ones
                if ((port & 3)==0) {
                    return chip[0].Read();
                } else {
                    return 0xff;
                }
            case MODE_DUALOPL2:
                //Only return for the lower ports
                if ((port & 1)!=0) {
                    return 0xff;
                }
                //Make sure the low /*Bits*/int are 6 on opl2
                return chip[ (port >> 1) & 1].Read() | 0x6;
            }
            return 0;
        }
        public void Init(int m) {
            mode = m;
            switch ( mode ) {
            case MODE_OPL3:
            case MODE_OPL2:
                break;
            case MODE_DUALOPL2:
                //Setup opl3 mode in the hander
                handler.WriteReg(0x105, (short)1);
                //Also set it up in the cache so the capturing will start opl3
                CacheWrite(0x105, (short)1);
                break;
            }
        }
    }

    private static Module module = null;

    private final Mixer.MIXER_Handler OPL_CallBack = new Mixer.MIXER_Handler() {
        public void call(/*Bitu*/int len) {
            module.handler.Generate( module.mixerChan, len );
            //Disable the sound generation after 30 seconds of silence
            if ((timeSource.getEmulatedMicros() - module.lastUsed) > 30000000) {
                /*Bitu*/int i;
                for (i=0xb0;i<0xb9;i++) if ((module.cache[i] &0x20)!=0 || (module.cache[i+0x100] & 0x20)!=0) break;
                if (i==0xb9) module.mixerChan.Enable(false);
                else module.lastUsed = timeSource.getEmulatedMicros();//Pic.PIC_Ticks;
            }
        }
    };

    public Adlib()
    {
        // soundblaster must be instantiated first!
        module = new Module();
    }

    public int[] ioPortsRequested()
    {
        int[] ports = new int[6 + (single ? 0:4)];
        int base = Option.sbbase.intValue(SBlaster.BASE, 16);
        int i=0;
        for (;i <4 ; i++)
            ports[i] = 0x388 + i;
        if (!single)
            for (int j=0;j <4 ; j++)
                ports[i++] = base + j;
        ports[i++] = base+8;
        ports[i++] = base+9;
        return ports;
    }

    public void acceptComponent(HardwareComponent component)
    {
        if ((component instanceof Clock) && component.initialised())
            timeSource = (Clock) component;
        if ((component instanceof IOPortHandler) && component.initialised()) {
            ((IOPortHandler) component).registerIOPortCapable(this);
            ioportRegistered = true;
        }
    }

    public boolean initialised()
    {
        return (timeSource != null) && ioportRegistered;
    }

    public int ioPortRead8(int address)
    {
        return ioPortRead32(address);
    }

    public int ioPortRead16(int address)
    {
        return ioPortRead32(address);
    }

    public int ioPortRead32(int port)
    {
        return module.PortRead(port);
    }

    public void ioPortWrite8(int address, int data)
    {
        ioPortWrite32(address, data);
    }

    public void ioPortWrite16(int address, int data)
    {
        ioPortWrite32(address, data);
    }

    public void ioPortWrite32(int port, int data)
    {
        module.PortWrite( port, (short)data);
    }
}
