#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>

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

typedef struct {
    uint32_t reload_bit;
    uint32_t update_bit;
    uint32_t start_bit;
    unsigned int pre_shift;
    unsigned int mux_shift;
    int irq;
} s3c2410_timer_config_t;

struct __s3c2410_timer_s__;
typedef struct __s3c2410_timer_s__ s3c2410_timer_t;

struct __s3c2410_timer_s__ {
    uint32_t tcfg0;
    uint32_t tcfg1;
    uint32_t tcon;
    uint32_t prev_tcon;

    x50ng_t* x50ng;

    unsigned int nr_regs;
    s3c2410_offset_t* regs;

    struct s3c2410_timeout {
        uint32_t tcntb;
        uint32_t tcmpb;
        uint32_t tcnt;
        uint32_t tcmp;

        const s3c2410_timer_config_t* tconfig;
        int index;

        s3c2410_timer_t* main;

        unsigned long interval;
        x50ng_timer_t* timer;
    } timeout[ 5 ];
};

static const s3c2410_timer_config_t s3c2410_timer_config[] = {
    {TCON_TIMER0_RELOAD, TCON_TIMER0_UPDATE, TCON_TIMER0_START, TCFG0_PRE0_SHIFT, TCFG1_MUX0_SHIFT, INT_TIMER0},
    {TCON_TIMER1_RELOAD, TCON_TIMER1_UPDATE, TCON_TIMER1_START, TCFG0_PRE0_SHIFT, TCFG1_MUX1_SHIFT, INT_TIMER1},
    {TCON_TIMER2_RELOAD, TCON_TIMER2_UPDATE, TCON_TIMER2_START, TCFG0_PRE1_SHIFT, TCFG1_MUX2_SHIFT, INT_TIMER2},
    {TCON_TIMER3_RELOAD, TCON_TIMER3_UPDATE, TCON_TIMER3_START, TCFG0_PRE1_SHIFT, TCFG1_MUX3_SHIFT, INT_TIMER3},
    {TCON_TIMER4_RELOAD, TCON_TIMER4_UPDATE, TCON_TIMER4_START, TCFG0_PRE1_SHIFT, TCFG1_MUX4_SHIFT, INT_TIMER4},
};

static int s3c2410_timer_data_init( s3c2410_timer_t* timer )
{
    s3c2410_offset_t regs[] = { S3C2410_OFFSET( TIMER, TCFG0, 0, timer->tcfg0 ),
                                S3C2410_OFFSET( TIMER, TCFG1, 0, timer->tcfg1 ),
                                S3C2410_OFFSET( TIMER, TCON, 0, timer->tcon ),
                                S3C2410_OFFSET( TIMER, TCNTB0, 0, timer->timeout[ 0 ].tcntb ),
                                S3C2410_OFFSET( TIMER, TCMPB0, 0, timer->timeout[ 0 ].tcmpb ),
                                S3C2410_OFFSET( TIMER, TCNTO0, 0, timer->timeout[ 0 ].tcnt ),
                                S3C2410_OFFSET( TIMER, TCNTB1, 0, timer->timeout[ 1 ].tcntb ),
                                S3C2410_OFFSET( TIMER, TCMPB1, 0, timer->timeout[ 1 ].tcmpb ),
                                S3C2410_OFFSET( TIMER, TCNTO1, 0, timer->timeout[ 1 ].tcnt ),
                                S3C2410_OFFSET( TIMER, TCNTB2, 0, timer->timeout[ 2 ].tcntb ),
                                S3C2410_OFFSET( TIMER, TCMPB2, 0, timer->timeout[ 2 ].tcmpb ),
                                S3C2410_OFFSET( TIMER, TCNTO2, 0, timer->timeout[ 2 ].tcnt ),
                                S3C2410_OFFSET( TIMER, TCNTB3, 0, timer->timeout[ 3 ].tcntb ),
                                S3C2410_OFFSET( TIMER, TCMPB3, 0, timer->timeout[ 3 ].tcmpb ),
                                S3C2410_OFFSET( TIMER, TCNTO3, 0, timer->timeout[ 3 ].tcnt ),
                                S3C2410_OFFSET( TIMER, TCNTB4, 0, timer->timeout[ 4 ].tcntb ),
                                S3C2410_OFFSET( TIMER, TCNTO4, 0, timer->timeout[ 4 ].tcnt ) };

    memset( timer, 0, sizeof( s3c2410_timer_t ) );

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

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

    return 0;
}

static void s3c2410_timer_timeout( void* data )
{
    struct s3c2410_timeout* t = data;
    s3c2410_timer_t* timer = t->main;
    x50ng_t* x50ng = timer->x50ng;
    int64_t timeout;

#ifdef DEBUG_S3C2410_TIMER
    printf( "s3c2410-timer: assert TIMER%u interrupt\n", t->index );
#endif

    s3c2410_intc_assert( timer->x50ng, t->tconfig->irq, 0 );

    if ( timer->tcon & t->tconfig->reload_bit ) {
        t->tcnt = t->tcntb;
        t->tcmp = t->tcmpb;
    } else {
        timer->tcon &= ~( t->tconfig->start_bit );
        return;
    }

    timeout = 1000000LL * t->tcnt * t->interval / x50ng->PCLK;
#ifdef DEBUG_S3C2410_TIMER
    printf( "s3c2410-timer: reload TIMER%u: CNT %u (%lu PCLKs): %llu us\n", t->index, t->tcnt, t->interval, ( unsigned long long )timeout );
#endif
    x50ng_mod_timer( t->timer, x50ng_get_clock() + timeout );
}

unsigned long s3c2410_timer_next_interrupt( x50ng_t* x50ng )
{
    s3c2410_timer_t* timer = x50ng->s3c2410_timer;
    struct s3c2410_timeout* t;
    unsigned long irq, next;
    unsigned long ticks;
    int i;

    ticks = x50ng_get_clock();

    next = ~( 0 );
    for ( i = 0; i < 5; i++ ) {
        t = &timer->timeout[ i ];

        if ( !( timer->tcon & t->tconfig->start_bit ) )
            continue;

        if ( x50ng_timer_pending( t->timer ) )
            irq = x50ng_timer_expires( t->timer ) - ticks;
        else
            irq = 0;

        if ( t->tcnt )
            irq += ( t->tcnt - 1 ) * t->interval;
        else {
            if ( !( timer->tcon & t->tconfig->reload_bit ) )
                continue;
            irq += t->tcntb * t->interval;
        }

        if ( irq < next )
            next = irq;

#ifdef DEBUG_S3C2410_TIMER
        printf( "s3c2410-timer: TIMER%u: tcnt %u, interval %lu, pending %u, next irq %lu\n", t->index, t->tcnt, t->interval,
                x50ng_timer_pending( t->timer ), irq );
#endif
    }

    return next;
}

static void s3c2410_update_tcfg( s3c2410_timer_t* timer )
{
    struct s3c2410_timeout* t;
    x50ng_t* x50ng = timer->x50ng;
    uint32_t pre, mux;
    int64_t timeout;
    int i;

    for ( i = 0; i < 5; i++ ) {
        t = &timer->timeout[ i ];

        pre = ( timer->tcfg0 >> t->tconfig->pre_shift ) & TCFG0_PREx_MASK;
        mux = ( timer->tcfg1 >> t->tconfig->mux_shift ) & TCFG1_MUXx_MASK;

        if ( mux > 3 ) {
            printf( "s3c2410-timer: can't handle MUX %02x for TIMER%u\n", mux, t->index );
            mux = 3;
        }

        t->interval = ( pre + 1 ) * ( 2 << mux );

#ifdef DEBUG_S3C2410_TIMER
        printf( "s3c2410-timer: TIMER%u: pre %u, mux %u, tick %lu PCLKs\n", t->index, pre, mux, t->interval );
#endif
        if ( x50ng_timer_pending( t->timer ) ) {
            timeout = 1000000LL * t->tcnt * t->interval / x50ng->PCLK;
#ifdef DEBUG_S3C2410_TIMER
            printf( "s3c2410-timer: mod TIMER%u: CNT %u (%lu PCLKs): %llu us\n", t->index, t->tcnt, t->interval,
                    ( unsigned long long )timeout );
#endif
            x50ng_mod_timer( t->timer, x50ng_get_clock() + timeout );
        }
    }
}

static void s3c2410_update_tcon( s3c2410_timer_t* timer )
{
    struct s3c2410_timeout* t;
    x50ng_t* x50ng = timer->x50ng;
    int64_t timeout;
    uint32_t change;
    int i;

    change = timer->prev_tcon ^ timer->tcon;
    timer->prev_tcon = timer->tcon;

    for ( i = 0; i < 5; i++ ) {
        t = &timer->timeout[ i ];

        if ( timer->tcon & t->tconfig->update_bit ) {
            t->tcnt = t->tcntb;
            t->tcmp = t->tcmpb;

#ifdef DEBUG_S3C2410_TIMER
            printf( "s3c2410-timer: update TIMER%u tcnt %u, tcmp %u\n", t->index, t->tcnt, t->tcmp );
#endif
        }

        if ( change & t->tconfig->start_bit ) {
            if ( timer->tcon & t->tconfig->start_bit ) {
                timeout = 1000000LL * t->tcnt * t->interval / x50ng->PCLK;
#ifdef DEBUG_S3C2410_TIMER
                printf( "s3c2410-timer: start TIMER%u: CNT %u (%lu PCLKs): %llu us\n", t->index, t->tcnt, t->interval,
                        ( unsigned long long )timeout );
#endif
                x50ng_mod_timer( t->timer, x50ng_get_clock() + timeout );
            } else {
                x50ng_del_timer( t->timer );
#ifdef DEBUG_S3C2410_TIMER
                printf( "s3c2410-timer: stop TIMER%u\n", t->index );
#endif
            }
        }
    }
}

static uint32_t s3c2410_read_tcnt( s3c2410_timer_t* timer, int index )
{
    struct s3c2410_timeout* t = &timer->timeout[ index ];
    x50ng_t* x50ng = timer->x50ng;
    int64_t now, expires, timeout;

    if ( !( timer->tcon & t->tconfig->start_bit ) )
        return t->tcnt;

    if ( x50ng_timer_pending( t->timer ) ) {
        now = x50ng_get_clock();
        expires = x50ng_timer_expires( t->timer );

        timeout = expires - now;
        if ( timeout <= 0 )
            return 0;

        t->tcnt = timeout * x50ng->PCLK / ( 1000000LL * t->interval );
    }

    return t->tcnt;
}

static uint32_t s3c2410_timer_read( void* opaque, target_phys_addr_t offset )
{
    s3c2410_timer_t* timer = opaque;
    s3c2410_offset_t* reg;
    uint32_t data;

    if ( !S3C2410_OFFSET_OK( timer, offset ) )
        return ~( 0 );

    reg = S3C2410_OFFSET_ENTRY( timer, offset );

    switch ( offset ) {
        case S3C2410_TIMER_TCNTO0:
            data = s3c2410_read_tcnt( timer, 0 );
            break;
        case S3C2410_TIMER_TCNTO1:
            data = s3c2410_read_tcnt( timer, 1 );
            break;
        case S3C2410_TIMER_TCNTO2:
            data = s3c2410_read_tcnt( timer, 2 );
            break;
        case S3C2410_TIMER_TCNTO3:
            data = s3c2410_read_tcnt( timer, 3 );
            break;
        case S3C2410_TIMER_TCNTO4:
            data = s3c2410_read_tcnt( timer, 4 );
            break;
        default:
            data = *( reg->datap );
            break;
    }

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

    return data;
}

static void s3c2410_timer_write( void* opaque, target_phys_addr_t offset, uint32_t data )
{
    s3c2410_timer_t* timer = opaque;
    s3c2410_offset_t* reg;

    if ( !S3C2410_OFFSET_OK( timer, offset ) )
        return;

    reg = S3C2410_OFFSET_ENTRY( timer, offset );

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

    switch ( offset ) {
        case S3C2410_TIMER_TCFG0:
            *( reg->datap ) = data;
            s3c2410_update_tcfg( timer );
            break;
        case S3C2410_TIMER_TCFG1:
            *( reg->datap ) = data;
            s3c2410_update_tcfg( timer );
            break;
        case S3C2410_TIMER_TCON:
            *( reg->datap ) = data;
            s3c2410_update_tcon( timer );
            break;
        case S3C2410_TIMER_TCNTO0:
        case S3C2410_TIMER_TCNTO1:
        case S3C2410_TIMER_TCNTO2:
        case S3C2410_TIMER_TCNTO3:
        case S3C2410_TIMER_TCNTO4:
            /* read only */
            break;
        default:
            *( reg->datap ) = data;
            break;
    }
}

static int s3c2410_timer_load( x50ng_module_t* module, GKeyFile* key )
{
    s3c2410_timer_t* timer = 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 < timer->nr_regs; i++ ) {
        reg = &timer->regs[ i ];

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

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

    s3c2410_update_tcon( timer );
    s3c2410_update_tcfg( timer );

    return error;
}

static int s3c2410_timer_save( x50ng_module_t* module, GKeyFile* key )
{
    s3c2410_timer_t* timer = module->user_data;
    s3c2410_offset_t* reg;
    unsigned int i;

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

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

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

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

    return 0;
}

static int s3c2410_timer_reset( x50ng_module_t* module, x50ng_reset_t reset )
{
    s3c2410_timer_t* timer = module->user_data;
    s3c2410_offset_t* reg;
    unsigned int i;

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

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

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

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

    s3c2410_update_tcon( timer );
    s3c2410_update_tcfg( timer );

    return 0;
}

static CPUReadMemoryFunc* s3c2410_timer_readfn[] = { s3c2410_timer_read, s3c2410_timer_read, s3c2410_timer_read };

static CPUWriteMemoryFunc* s3c2410_timer_writefn[] = { s3c2410_timer_write, s3c2410_timer_write, s3c2410_timer_write };

static int s3c2410_timer_init( x50ng_module_t* module )
{
    s3c2410_timer_t* timer;
    struct s3c2410_timeout* t;
    int iotype;
    int i;

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

    timer = malloc( sizeof( s3c2410_timer_t ) );
    if ( NULL == timer ) {
        fprintf( stderr, "%s: %s:%u: Out of memory\n", module->x50ng->progname, __func__, __LINE__ );
        return -ENOMEM;
    }
    if ( s3c2410_timer_data_init( timer ) ) {
        free( timer );
        return -ENOMEM;
    }

    module->user_data = timer;

    timer->x50ng = module->x50ng;
    module->x50ng->s3c2410_timer = timer;

    for ( i = 0; i < 5; i++ ) {
        t = &timer->timeout[ i ];

        t->tconfig = &s3c2410_timer_config[ i ];
        t->index = i;

        t->main = timer;

        t->timer = x50ng_new_timer( X50NG_TIMER_VIRTUAL, s3c2410_timer_timeout, t );
    }

    iotype = cpu_register_io_memory( s3c2410_timer_readfn, s3c2410_timer_writefn, timer );
#ifdef DEBUG_S3C2410_TIMER
    printf( "%s: iotype %08x\n", __func__, iotype );
#endif
    cpu_register_physical_memory( S3C2410_TIMER_BASE, S3C2410_MAP_SIZE, iotype );
    return 0;
}

static int s3c2410_timer_exit( x50ng_module_t* module )
{
    s3c2410_timer_t* timer;

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

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

    x50ng_module_unregister( module );
    free( module );

    return 0;
}

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

    if ( x50ng_module_init( x50ng, "s3c2410-timer", s3c2410_timer_init, s3c2410_timer_exit, s3c2410_timer_reset, s3c2410_timer_load,
                            s3c2410_timer_save, NULL, &module ) )
        return -1;

    return x50ng_module_register( module );
}
