#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "x50ng.h"
#include "s3c2410.h"
#include "s3c2410_intc.h"

extern int do_trace;

typedef struct {
    int sel_shift;
    uint32_t mode_bit;
    int index;
    int req[ 6 ];
} s3c2410_arb_t;

typedef struct {
    uint32_t sel;
    uint32_t mode;
} s3c2410_arb_data_t;

static const int s3c2410_arb_order[ 4 ][ 6 ] = {
    {0, 1, 2, 3, 4, 5},
    {0, 2, 3, 4, 1, 5},
    {0, 3, 4, 1, 2, 5},
    {0, 4, 1, 2, 3, 5}
};

static const s3c2410_arb_t s3c2410_arb_table[] = {
    [0] = {ARB0_SEL_SHIFT, ARB0_MODE, 0, { -1, EINT0, EINT1, EINT2, EINT3, -1 }                                   },
    [1] = {ARB1_SEL_SHIFT, ARB1_MODE, 1, { EINT4_7, EINT8_23, -1, nBATT_FLT, INT_TICK, INT_WDT }                  },
    [2] = {ARB2_SEL_SHIFT, ARB2_MODE, 2, { INT_TIMER0, INT_TIMER1, INT_TIMER2, INT_TIMER3, INT_TIMER4, INT_UART2 }},
    [3] = {ARB3_SEL_SHIFT, ARB3_MODE, 3, { INT_LCD, INT_DMA0, INT_DMA1, INT_DMA2, INT_DMA3, INT_SDI }             },
    [4] = {ARB4_SEL_SHIFT, ARB4_MODE, 4, { INT_SPI0, INT_UART1, -1, INT_USBD, INT_USBH, INT_IIC }                 },
    [5] = {ARB5_SEL_SHIFT,
           ARB5_MODE,                 5,
           {
                -1,
                INT_UART0,
                INT_SPI1,
                INT_RTC,
                INT_ADC,
                -1,
            }                                                                                                     },
    [6] = {ARB6_SEL_SHIFT, ARB6_MODE, 6, { 0, 1, 2, 3, 4, 5 }                                                     },
};
#define INTC_NR_ARB ( sizeof( s3c2410_arb_table ) / sizeof( s3c2410_arb_table[ 0 ] ) )

typedef struct {
    uint32_t srcpnd;
    uint32_t intmod;
    uint32_t intmsk;
    uint32_t priority;
    uint32_t intpnd;
    uint32_t intoffset;
    uint32_t subsrcpnd;
    uint32_t intsubmsk;

    s3c2410_arb_data_t arb_data[ INTC_NR_ARB ];

    x50ng_t* x50ng;

    uint32_t src_pending;
    uint32_t subsrc_pending;

    unsigned int nr_regs;
    s3c2410_offset_t* regs;
} s3c2410_intc_t;

static void s3c2410_intc_gen_int( s3c2410_intc_t* intc );
static void s3c2410_intc_gen_int_from_sub_int( s3c2410_intc_t* intc );

static int s3c2410_intc_data_init( s3c2410_intc_t* intc )
{
    unsigned int i;

    s3c2410_offset_t regs[] = {
        S3C2410_OFFSET( INTC, SRCPND, 0x00000000, intc->srcpnd ),       S3C2410_OFFSET( INTC, INTMOD, 0x00000000, intc->intmod ),
        S3C2410_OFFSET( INTC, INTMSK, 0xffffffff, intc->intmsk ),       S3C2410_OFFSET( INTC, PRIORITY, 0x0000007f, intc->priority ),
        S3C2410_OFFSET( INTC, INTPND, 0x00000000, intc->intpnd ),       S3C2410_OFFSET( INTC, INTOFFSET, 0x00000000, intc->intoffset ),
        S3C2410_OFFSET( INTC, SUBSRCPND, 0x00000000, intc->subsrcpnd ), S3C2410_OFFSET( INTC, INTSUBMSK, 0x000007ff, intc->intsubmsk ) };

    memset( intc, 0, sizeof( s3c2410_intc_t ) );

    intc->regs = malloc( sizeof( regs ) );
    if ( NULL == intc->regs ) {
        fprintf( stderr, "%s:%u: Out of memory\n", __func__, __LINE__ );
        return -ENOMEM;
    }

    memcpy( intc->regs, regs, sizeof( regs ) );
    intc->nr_regs = sizeof( regs ) / sizeof( regs[ 0 ] );

    for ( i = 0; i < INTC_NR_ARB; i++ ) {
        intc->arb_data[ i ].sel = 0;
        intc->arb_data[ i ].mode = s3c2410_arb_table[ i ].mode_bit;
    }

    return 0;
}

static void srcpnd_put_word( s3c2410_intc_t* intc, uint32_t data )
{
    intc->srcpnd &= ~( data );
    intc->srcpnd |= intc->src_pending;

    if ( intc->src_pending & data ) {
        s3c2410_intc_gen_int( intc );
    }
}

static void intmod_put_word( s3c2410_intc_t* intc, uint32_t data )
{
    intc->intmod = data & 0xfeffffbf;

    s3c2410_intc_gen_int( intc );
}

static void intmsk_put_word( s3c2410_intc_t* intc, uint32_t data )
{
#ifdef DEBUG_X50NG_ENABLE_IRQ
    uint32_t change;
    int i;

    change = intc->intmsk ^ data;
#endif

    intc->intmsk = data | 0x01000040;

#ifdef DEBUG_X50NG_ENABLE_IRQ
    for ( i = 0; i < 32; i++ ) {
        if ( ( change & ( 1 << i ) ) && !( intc->intmsk & ( 1 << i ) ) ) {
            printf( "INTC: Enable IRQ %u\n", i );
        }
    }
#endif

    s3c2410_intc_gen_int( intc );
}

static uint32_t priority_get_word( s3c2410_intc_t* intc )
{
    const s3c2410_arb_t* arb;
    s3c2410_arb_data_t* arb_data;
    unsigned int i;

    intc->priority = 0;

    for ( i = 0; i < INTC_NR_ARB; i++ ) {
        arb = &s3c2410_arb_table[ i ];
        arb_data = &intc->arb_data[ i ];

        intc->priority |= ( arb_data->sel << arb->sel_shift ) | arb_data->mode;
    }

    return intc->priority;
}

static void priority_put_word( s3c2410_intc_t* intc, uint32_t data )
{
    const s3c2410_arb_t* arb;
    s3c2410_arb_data_t* arb_data;
    unsigned int i;

    intc->priority = data & 0x001fffff;

    for ( i = 0; i < INTC_NR_ARB; i++ ) {
        arb = &s3c2410_arb_table[ i ];
        arb_data = &intc->arb_data[ i ];

        arb_data->sel = ( intc->priority >> arb->sel_shift ) & ARBx_SEL_MASK;
        arb_data->mode = intc->priority & arb->mode_bit;
    }

    s3c2410_intc_gen_int( intc );
}

static void intpnd_put_word( s3c2410_intc_t* intc, uint32_t data )
{
    intc->intpnd &= ~( data );

    s3c2410_intc_gen_int( intc );
}

static void subsrcpnd_put_word( s3c2410_intc_t* intc, uint32_t data )
{
    intc->subsrcpnd &= ~( data );
    intc->subsrcpnd |= intc->subsrc_pending;

    if ( intc->subsrc_pending & data ) {
        s3c2410_intc_gen_int_from_sub_int( intc );
    }
}

static void intsubmsk_put_word( s3c2410_intc_t* intc, uint32_t data )
{
    intc->intsubmsk = data & 0x000007ff;

    s3c2410_intc_gen_int_from_sub_int( intc );
}

static uint32_t s3c2410_intc_select_int( s3c2410_intc_t* intc, const s3c2410_arb_t* arb, uint32_t service, int* offset )
{
    s3c2410_arb_data_t* arb_data = &intc->arb_data[ arb->index ];
    const int* order;
    int i, req;

    order = s3c2410_arb_order[ arb_data->sel ];

    for ( i = 0; i < 6; i++ ) {
        req = order[ i ];

        if ( -1 == arb->req[ req ] )
            continue;

        if ( service & ( 1 << arb->req[ req ] ) ) {
            if ( arb_data->mode )
                arb_data->sel = ( arb_data->sel + 1 ) & ARBx_SEL_MASK;
            *offset = arb->req[ req ];
            return ( 1 << arb->index );
        }
    }

    *offset = -1;
    return 0;
}

void s3c2410_FIQ( CPUState* env ) { cpu_interrupt( env, CPU_INTERRUPT_FIQ ); }

void s3c2410_IRQ( CPUState* env ) { cpu_interrupt( env, CPU_INTERRUPT_HARD ); }

static void s3c2410_intc_gen_int( s3c2410_intc_t* intc )
{
    x50ng_t* x50ng = intc->x50ng;
    uint32_t fiq, service;
    int offset[ 6 ], index;
    const s3c2410_arb_t* arb;
    uint32_t request;
    int i;

    fiq = intc->srcpnd & intc->intmod;

#ifdef DEBUG_S3C2410_INTC0
    printf( "INTC: FIQ service request: %08x\n", fiq );
#endif

    if ( fiq ) {
        /*
         * Generate FIQ.
         */
#ifdef DEBUG_S3C2410_INTC
        printf( "INTC: vector to %08x\n", 0x1c );
#endif
        cpu_interrupt( x50ng->env, CPU_INTERRUPT_FIQ );

        x50ng_set_idle( x50ng, 0 );
        return;
    } else {
        cpu_reset_interrupt( x50ng->env, CPU_INTERRUPT_FIQ );
    }

#ifdef DEBUG_S3C2410_INTC0
    printf( "INTC: IRQ pending request: %08x\n", intc->intpnd );
#endif

    if ( intc->intpnd ) {
        /*
         * Generate IRQ.
         */
#ifdef DEBUG_S3C2410_INTC
        printf( "INTC: vector to %08x\n", 0x18 );
#endif
        cpu_interrupt( x50ng->env, CPU_INTERRUPT_HARD );

        x50ng_set_idle( x50ng, 0 );
        return;
    }

#ifdef DEBUG_S3C2410_INTC0
    printf( "INTC: srcpnd %08x, intmsk: %08x\n", intc->srcpnd, intc->intmsk );
#endif

    service = intc->srcpnd & ~( intc->intmsk );

#ifdef DEBUG_S3C2410_INTC0
    printf( "INTC: IRQ service request: %08x\n", service );
#endif

    if ( 0 == service ) {
        cpu_reset_interrupt( x50ng->env, CPU_INTERRUPT_HARD );
        return;
    }

    request = 0;
    for ( i = 0; i < 6; i++ ) {
        arb = &s3c2410_arb_table[ i ];

        request |= s3c2410_intc_select_int( intc, arb, service, &offset[ i ] );

#ifdef DEBUG_S3C2410_INTC0
        printf( "INTC: ARB%u highest %d\n", i, offset[ i ] );
#endif
    }

    arb = &s3c2410_arb_table[ 6 ];

#ifdef DEBUG_S3C2410_INTC0
    printf( "INTC: ARB%u request: %08x\n", 6, request );
#endif

    if ( s3c2410_intc_select_int( intc, arb, request, &index ) ) {
        intc->intoffset = offset[ index ];
        intc->intpnd |= ( 1 << intc->intoffset );

#ifdef DEBUG_S3C2410_INTC
        printf( "INTC: irq pending: %u (%08x)\n", intc->intoffset, intc->intpnd );
#endif
        /*
         * Generate IRQ.
         */
#ifdef DEBUG_S3C2410_INTC
        printf( "INTC: vector to %08x\n", 0x18 );
#endif
        cpu_interrupt( x50ng->env, CPU_INTERRUPT_HARD );

        x50ng_set_idle( x50ng, 0 );
        return;
    }

#ifdef DEBUG_S3C2410_INTC0
    printf( "INTC: No irq pending\n" );
#endif

    cpu_reset_interrupt( x50ng->env, CPU_INTERRUPT_HARD );
}

void s3c2410_intc_assert( x50ng_t* x50ng, int irq, int level )
{
    s3c2410_intc_t* intc = x50ng->s3c2410_intc;

    if ( irq > 31 )
        return;

#ifdef DEBUG_S3C2410_INTC
    printf( "INTC: assert irq %u (%08x)\n", irq, 1 << irq );
#endif

    if ( !( intc->src_pending & ( 1 << irq ) ) ) {
        if ( level )
            intc->src_pending |= ( 1 << irq );
        intc->srcpnd |= ( 1 << irq );

        s3c2410_intc_gen_int( intc );
    }

    if ( x50ng->arm_idle == 2 ) {
        if ( irq == EINT0 || irq == INT_RTC )
            x50ng_set_idle( x50ng, 0 );
    }
}

void s3c2410_intc_deassert( x50ng_t* x50ng, int irq )
{
    s3c2410_intc_t* intc = x50ng->s3c2410_intc;

    if ( irq > 31 )
        return;

#ifdef DEBUG_S3C2410_INTC
    printf( "INTC: deassert irq %u (%08x)\n", irq, 1 << irq );
#endif

    intc->src_pending &= ~( 1 << irq );
}

static void s3c2410_intc_gen_int_from_sub_int( s3c2410_intc_t* intc )
{
    x50ng_t* x50ng = intc->x50ng;
    uint32_t service;

    service = intc->subsrcpnd & ~( intc->intsubmsk );

#ifdef DEBUG_S3C2410_INTC
    printf( "INTC: subirq service request: %08x\n", service );
#endif

    if ( service & ( ( 1 << SUB_INT_ERR0 ) | ( 1 << SUB_INT_TXD0 ) | ( 1 << SUB_INT_RXD0 ) ) ) {
        s3c2410_intc_assert( x50ng, INT_UART0, 1 );
    } else {
        s3c2410_intc_deassert( x50ng, INT_UART0 );
    }

    if ( service & ( ( 1 << SUB_INT_ERR1 ) | ( 1 << SUB_INT_TXD1 ) | ( 1 << SUB_INT_RXD1 ) ) ) {
        s3c2410_intc_assert( x50ng, INT_UART1, 1 );
    } else {
        s3c2410_intc_deassert( x50ng, INT_UART1 );
    }

    if ( service & ( ( 1 << SUB_INT_ERR2 ) | ( 1 << SUB_INT_TXD2 ) | ( 1 << SUB_INT_RXD2 ) ) ) {
        s3c2410_intc_assert( x50ng, INT_UART2, 1 );
    } else {
        s3c2410_intc_deassert( x50ng, INT_UART2 );
    }

    if ( service & ( ( 1 << SUB_INT_ADC ) | ( 1 << SUB_INT_TC ) ) ) {
        s3c2410_intc_assert( x50ng, INT_ADC, 1 );
    } else {
        s3c2410_intc_deassert( x50ng, INT_ADC );
    }

    intc->subsrcpnd = intc->subsrc_pending;
}

void s3c2410_intc_sub_assert( x50ng_t* x50ng, int sub_irq, int level )
{
    s3c2410_intc_t* intc = x50ng->s3c2410_intc;

    if ( sub_irq > 31 )
        return;

#ifdef DEBUG_S3C2410_INTC
    printf( "INTC: assert subirq %u (%08x)\n", sub_irq, 1 << sub_irq );
#endif

    if ( !( intc->subsrc_pending & ( 1 << sub_irq ) ) ) {
        if ( level )
            intc->subsrc_pending |= ( 1 << sub_irq );
        intc->subsrcpnd |= ( 1 << sub_irq );

        s3c2410_intc_gen_int_from_sub_int( intc );
    }
}

void s3c2410_intc_sub_deassert( x50ng_t* x50ng, int sub_irq )
{
    s3c2410_intc_t* intc = x50ng->s3c2410_intc;

    if ( sub_irq > 31 )
        return;

#ifdef DEBUG_S3C2410_INTC
    printf( "INTC: deassert subirq %u (%08x)\n", sub_irq, 1 << sub_irq );
#endif

    intc->subsrc_pending &= ~( 1 << sub_irq );
}

static uint32_t s3c2410_intc_read( void* opaque, target_phys_addr_t offset )
{
    s3c2410_intc_t* intc = opaque;
    s3c2410_offset_t* reg;
    uint32_t data;

    if ( !S3C2410_OFFSET_OK( intc, offset ) ) {
        return ~( 0 );
    }

    reg = S3C2410_OFFSET_ENTRY( intc, offset );

    switch ( offset ) {
        case S3C2410_INTC_PRIORITY:
            data = priority_get_word( intc );
            break;
        default:
            data = *( reg->datap );
            break;
    }

#ifdef DEBUG_S3C2410_INTC
    printf( "read  %s [%08x] %s [%08lx] data %08x\n", "s3c2410-intc", S3C2410_INTC_BASE, reg->name, ( unsigned long )offset, data );
#endif

    return data;
}

static void s3c2410_intc_write( void* opaque, target_phys_addr_t offset, uint32_t data )
{
#ifdef DEBUG_S3C2410_INTC
    s3c2410_offset_t* reg;
#endif
    s3c2410_intc_t* intc = opaque;

    if ( !S3C2410_OFFSET_OK( intc, offset ) ) {
        return;
    }

#ifdef DEBUG_S3C2410_INTC
    reg = S3C2410_OFFSET_ENTRY( intc, offset );

    printf( "write %s [%08x] %s [%08lx] data %08x\n", "s3c2410-intc", S3C2410_INTC_BASE, reg->name, ( unsigned long )offset, data );
#endif

    switch ( offset ) {
        case S3C2410_INTC_SRCPND:
            srcpnd_put_word( intc, data );
            break;
        case S3C2410_INTC_INTMOD:
            intmod_put_word( intc, data );
            break;
        case S3C2410_INTC_INTMSK:
            intmsk_put_word( intc, data );
            break;
        case S3C2410_INTC_PRIORITY:
            priority_put_word( intc, data );
            break;
        case S3C2410_INTC_INTPND:
            intpnd_put_word( intc, data );
            break;
        case S3C2410_INTC_SUBSRCPND:
            subsrcpnd_put_word( intc, data );
            break;
        case S3C2410_INTC_INTSUBMSK:
            intsubmsk_put_word( intc, data );
            break;
        default:
            break;
    }
}

static int s3c2410_intc_load( x50ng_module_t* module, GKeyFile* key )
{
    s3c2410_intc_t* intc = module->user_data;
    s3c2410_offset_t* reg;
    int error = 0;
    unsigned int i;

#ifdef DEBUG_X50NG_MODULES
    printf( "%s: %s:%u\n", module->name, __func__, __LINE__ );
#endif

    for ( i = 0; i < intc->nr_regs; i++ ) {
        reg = &intc->regs[ i ];

        if ( NULL == reg->name )
            continue;

        if ( x50ng_module_get_u32( module, key, reg->name, reg->reset, reg->datap ) )
            error = -EAGAIN;
    }

    intc->src_pending = intc->srcpnd;
    intc->subsrc_pending = intc->subsrcpnd;
    intc->srcpnd = 0;
    intc->subsrcpnd = 0;

    priority_put_word( intc, intc->priority );
    intsubmsk_put_word( intc, intc->intsubmsk );
    intmsk_put_word( intc, intc->intmsk );

    return error;
}

static int s3c2410_intc_save( x50ng_module_t* module, GKeyFile* key )
{
    s3c2410_intc_t* intc = module->user_data;
    s3c2410_offset_t* reg;
    unsigned int i;

#ifdef DEBUG_X50NG_MODULES
    printf( "%s: %s:%u\n", module->name, __func__, __LINE__ );
#endif

    intc->srcpnd = intc->src_pending;
    intc->subsrcpnd = intc->subsrc_pending;

    for ( i = 0; i < intc->nr_regs; i++ ) {
        reg = &intc->regs[ i ];

        if ( NULL == reg->name )
            continue;

        x50ng_module_set_u32( module, key, reg->name, *( reg->datap ) );
    }

    return 0;
}

static int s3c2410_intc_reset( x50ng_module_t* module, x50ng_reset_t reset )
{
    s3c2410_intc_t* intc = module->user_data;
    s3c2410_offset_t* reg;
    unsigned int i;

#ifdef DEBUG_X50NG_MODULES
    printf( "%s: %s:%u\n", module->name, __func__, __LINE__ );
#endif

    if ( reset == X50NG_RESET_POWER_OFF ) {
        return 0;
    }

    for ( i = 0; i < intc->nr_regs; i++ ) {
        reg = &intc->regs[ i ];

        if ( NULL == reg->name )
            continue;

        *( reg->datap ) = reg->reset;
    }

    return 0;
}

static CPUReadMemoryFunc* s3c2410_intc_readfn[] = { s3c2410_intc_read, s3c2410_intc_read, s3c2410_intc_read };

static CPUWriteMemoryFunc* s3c2410_intc_writefn[] = { s3c2410_intc_write, s3c2410_intc_write, s3c2410_intc_write };

static int s3c2410_intc_init( x50ng_module_t* module )
{
    s3c2410_intc_t* intc;
    int iotype;

#ifdef DEBUG_X50NG_MODULES
    printf( "%s: %s:%u\n", module->name, __func__, __LINE__ );
#endif

    intc = malloc( sizeof( s3c2410_intc_t ) );
    if ( NULL == intc ) {
        fprintf( stderr, "%s:%u: Out of memory\n", __func__, __LINE__ );
        return -ENOMEM;
    }
    if ( s3c2410_intc_data_init( intc ) ) {
        free( intc );
        return -ENOMEM;
    }

    module->user_data = intc;

    intc->x50ng = module->x50ng;
    intc->x50ng->s3c2410_intc = intc;

    iotype = cpu_register_io_memory( s3c2410_intc_readfn, s3c2410_intc_writefn, intc );
#ifdef DEBUG_S3C2410_INTC
    printf( "%s: iotype %08x\n", __func__, iotype );
#endif
    cpu_register_physical_memory( S3C2410_INTC_BASE, S3C2410_MAP_SIZE, iotype );
    return 0;
}

static int s3c2410_intc_exit( x50ng_module_t* module )
{
    s3c2410_intc_t* intc;

#ifdef DEBUG_X50NG_MODULES
    printf( "%s: %s:%u\n", module->name, __func__, __LINE__ );
#endif

    if ( module->user_data ) {
        intc = module->user_data;
        if ( intc->regs )
            free( intc->regs );
        free( intc );
    }

    x50ng_module_unregister( module );
    free( module );

    return 0;
}

int x50ng_s3c2410_intc_init( x50ng_t* x50ng )
{
    x50ng_module_t* module;

    if ( x50ng_module_init( x50ng, "s3c2410-intc", s3c2410_intc_init, s3c2410_intc_exit, s3c2410_intc_reset, s3c2410_intc_load,
                            s3c2410_intc_save, NULL, &module ) )
        return -1;

    return x50ng_module_register( module );
}
