/*
[SCE CONFIDENTIAL]
$PSDocId$
                  Copyright (C) 2002-4 Sony Computer Entertainment America
                                                     All Rights Reserved
//  ------------------------------------------------------------------------

File: libezctl.c

//  ------------------------------------------------------------------------
*/

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

#include <eekernel.h>
#include <sif.h>
#include <sifdev.h>
#include <sifrpc.h>

#include <libeenet.h>
#include <eenetctl.h>
#include <eenetctlerrno.h>
#include <eenetctlstat.h>
#include <sys/socket.h>
#include <net/if.h>

#include <libver.h>

#include "../include/eznetcnf.h"
#include "../include/eznetctl_i18n.h"
#include "internal.h"

#define EENETCTL_STATE_WORKAROUND   1

// timeout in millisecs
#define kWaitAttachTimeout  (2 * 1000)


/*  --------------------------------------------------------
 *  global and static variables
 *  --------------------------------------------------------
 */

    // RPC client data
static rpcRequest_t     m_rpcBuf __attribute__((aligned(64)));
static sceSifClientData m_rpcCd;

static int m_modId;
static int m_rpcRunning;

#ifndef INET_VERSION
static ezNetCtlStatus_t m_status;
static int m_ievSema;
static volatile int m_ievLast;
#endif  // !INET_VERSION

static ezNetCtlIfstat_u m_ifstat;


/*  --------------------------------------------------------
 *  prototypes
 *  --------------------------------------------------------
 */

#ifndef INET_VERSION
static u_long alarmHandler(int, u_long, u_long, void *, void *);
static void ievHandler(const char *, int, int);
#endif  // !INET_VERSION


/*  --------------------------------------------------------
 *  PUBLIC library initialization and finalization
 *  --------------------------------------------------------
 */

/*
 *  initialize eznetctl state, and bind to RPC service.
 *
 *  NOTE: eznetctl_eenet can run with limited functionality without RPC.
 *  to support the possibility of re-initialization, skip RPC binding
 *  if IOP service module is not resident.
 *
 *  r = 0 if all initialization succeeds.
 *  r < 0 if any part fails.
 */
int
ezNetCtlInit()
{
    int err;
#ifndef INET_VERSION
    u_short flags;
    int iftype, len, num;
    struct sceEENetIfname ifnames[kMaxIfnum], *ifn;
    struct SemaParam sparam = { 0 };
#endif  // !INET_VERSION

    /*
     *  bind to RPC server on IOP
     */
    m_modId = err = sceSifSearchModuleByName(kModName_ezNetCtl);
    if (m_modId < 0) {
        dprintf("%s module not found\n", kModName_ezNetCtl);
        m_rpcRunning = 0;
    } else {
        while (1) {
            if (0 > sceSifBindRpc(&m_rpcCd, kRPCId_ezNetCtl, 0)) {
                dprintf("sceSifBindRpc failed, retrying\n");
            }
            if (m_rpcCd.serve != 0) break;
            DelayThread(10 * 1000);
        }
        m_rpcRunning = 1;
    }

    /*
     *  allow re-initialization while maintaining consistent state
     */
#ifndef INET_VERSION
    // attempt to acquire the active I/F, in case of re-init
    num = kMaxIfnum;
    sceEENetGetIfnames(ifnames, &num);
    for (ifn=ifnames; ifn<ifnames+num; ifn++) {
        len = sizeof(flags);
        sceEENetGetIfinfo(ifn->ifn_name, sceEENET_IFINFO_IFFLAGS, &flags, &len);
        len = sizeof(iftype);
        sceEENetGetIfinfo(ifn->ifn_name, sceEENET_IFINFO_IFTYPE, &iftype, &len);
        if (flags & IFF_RUNNING) {
            if (iftype == sceEENET_IFTYPE_PPPOE) {
                strncpy(m_status.ifname2, ifn->ifn_name, sizeof(m_status.ifname2));
            } else {
                strncpy(m_status.ifname,  ifn->ifn_name, sizeof(m_status.ifname));
            }
        }
    }

    // initialize event notification components
    if (m_ievSema <= 0) {
        sparam.maxCount = 32;
        m_ievSema = err = CreateSema(&sparam);
        if (err < 0) {
            dprintf("CreateSema failed (%d)\n", err);
        } else {
            err = sceEENetCtlRegisterEventHandler(ievHandler);
        }
    }
#endif  // !INET_VERSION

    // normalize err
    return (err < 0) ? err : 0;
}

/*
 *  shutdown RPC service and unbind client library.
 *
 *  NOTE: eznetctl_eenet can run with limited functionality without RPC.
 *  specify flags=ezNetCtlKeepEventHandler to unload the service module 
 *  only, while preserving the event handler registration.
 *
 *  specify flags=ezNetCtlKeepModule to keep the service module running,
 *  if you prefer to have this handled by your own IOP module manager.
 *  using this option with eznetctl_inet results in a nop.
 *  using this option AND ezNetCtlKeepEventHandler with eznetctl_eenet
 *  also results in a nop.
 *
 *  r = 0 after unload succeeds.
 *  r < 0 if IOP service module fails to stop or unload. 
 */
int
ezNetCtlExit(int flags)
{
    int err = 0;

#ifndef INET_VERSION
    if ( !(flags & ezNetCtlKeepEventHandler) && 
         (m_ievSema > 0) )
    {
        sceEENetCtlUnregisterEventHandler(ievHandler);
        DeleteSema(m_ievSema);
        m_ievSema = 0;
    }
#endif

    if ( !(flags & ezNetCtlKeepModule) &&
         m_rpcRunning &&
         0 <= sceSifStopModule(m_modId, 0, NULL, &err) &&
         0 <= (err = sceSifUnloadModule(m_modId)) )
    {
        err = m_rpcRunning = 0;
    }

    return err;
}


/*  --------------------------------------------------------
 *  PUBLIC library interfaces
 *  --------------------------------------------------------
 */

/*
 *  given a pointer to a previously loaded sceNetcnfifData record,
 *  send the record to the IOP and try to bind the configuration 
 *  to one of the existing network interfaces.
 *
 *  r > 0   binding successful, returns IEV from WaitEventTO().
 *  r < 0   binding failed / timed out from WaitEventTO().
 */
int
ezNetCtlSetProperties(sceNetcnfifData_t *addr)
{
    int err;
    rpcRequest_t *request = &m_rpcBuf;

    ASSERT_RPC( sizeof(int) );
    ASSERT_DMA( addr, 64 );
    SyncDCache( addr, (void *)((u_int)addr+sizeof(*addr)-1) );

#ifndef INET_VERSION
    memset(&m_status, 0, sizeof(m_status));
#endif  // !INET_VERSION

    // consume stale I/F events
    ezNetCtlWaitEventTO(0);

    /*
     *  instead of "pushing" the data from the high-speed EE and then
     *  busy-wait for the DMA to finish, tell the RPC server the address
     *  and have the IOP "pull" the data across via sceSifGetOtherData().
     */
    request->args[0] = (u_int)addr;

    // call eCtlSetProperties RPC
    sceSifCallRpc(&m_rpcCd, eCtlSetProperties,
                  0,
                  request, sizeof(int),
                  request, sizeof(int),
                  NULL, NULL);
    err = request->args[0];

    // check for IEV_Attach confirmation
    if (0 == err) {
        err = ezNetCtlWaitEventTO(kWaitAttachTimeout);
    }

    dprintf("returned %d\n", err);
    return err;
}


/*
 *  given the netdb path and a designated Combination, load in the
 *  Combination and try to bind the configuration to one of the 
 *  existing network interfaces.
 *
 *  r > 0   binding successful, returns IEV from WaitEventTO().
 *  r < 0   binding failed / timed out from WaitEventTO().
 */
int 
ezNetCtlSetCombination(const char *netdb, const char *combinationName)
{
    int err;
    rpcRequest_t *request = &m_rpcBuf;

    ASSERT_RPC(sizeof(rpcRequest_t));

#ifndef INET_VERSION
    memset(&m_status, 0, sizeof(m_status));
#endif  // !INET_VERSION

    // consume stale I/F events
    ezNetCtlWaitEventTO(0);

    // pack the RPC request structure
    strncpy(request->buf1, netdb, sizeof(request->buf1)-1);
    strncpy(request->buf2, combinationName, sizeof(request->buf2)-1);

    // call RPC
    sceSifCallRpc(&m_rpcCd, eCtlSetCombination,
                  0,
                  request, sizeof(rpcRequest_t),
                  request, sizeof(int),
                  NULL, NULL);
    err = request->args[0];

    // check for IEV_Attach confirmation
    if (0 == err) {
        err = ezNetCtlWaitEventTO(kWaitAttachTimeout);
    }

    dprintf("returned %d\n", err);
    return err;
}


/*
 *  preload the modem configuration file.
 *
 *  r = 0   preload successful.
 *  r < 0   preload failed.
 */
#if (SUPPORT_MODEM)
int 
ezNetCtlPreloadModemCnf(const char *path)
{
    rpcRequest_t *request = &m_rpcBuf;

    ASSERT_RPC(sizeof(rpcRequest_t));

    // pack the RPC request structure
    strncpy(request->buf1, path, sizeof(request->buf1)-1);

    // call RPC
    sceSifCallRpc(&m_rpcCd, eCtlPreloadModemCnf,
                  0,
                  request, sizeof(rpcRequest_t),
                  request, sizeof(int),
                  NULL, NULL);

    dprintf("returned %d\n", request->args[0]);
    return request->args[0];
}
#endif  // SUPPORT_MODEM


/*
 *  unload previously preloaded modem configuration data.
 *
 *  r = 0   preload successful.
 *  r < 0   preload failed.
 */
#if (SUPPORT_MODEM)
int 
ezNetCtlUnloadModemCnf()
{
    rpcRequest_t *request = &m_rpcBuf;

    ASSERT_RPC(sizeof(int));
    sceSifCallRpc(&m_rpcCd, eCtlUnloadModemCnf,
                  0,
                  NULL, 0,
                  request, sizeof(int),
                  NULL, NULL);

    dprintf("returned %d\n", request->args[0]);
    return request->args[0];
}
#endif  // SUPPORT_MODEM


/*  --------------------------------------------------------
 *  INET / EENET specific implementations
 *  --------------------------------------------------------
 */

/*
 *  up the active network interface.
 *
 *  r = 0   when successful.
 *  r < 0   when error occurs.
 */
int 
ezNetCtlUpInterface()
#ifdef INET_VERSION
{
    rpcRequest_t *request = &m_rpcBuf;

    ASSERT_RPC(sizeof(int));
    sceSifCallRpc(&m_rpcCd, eCtlUpInterface,
                  0,
                  NULL, 0,
                  request, sizeof(int),
                  NULL, NULL);
    return request->args[0];
}
#else   // !INET_VERSION
{
    int err = sceEENETCTL_E_Invalid;
    if (m_status.ifname[0]) {
        err = sceEENetCtlUpInterface(m_status.ifname);
    }
    return err;
}
#endif  // !INET_VERSION


/*
 *  down the active network interface.
 *
 *  r = 0   when successful.
 *  r < 0   when error occurs.
 */
int 
ezNetCtlDownInterface()
#ifdef INET_VERSION
{
    rpcRequest_t *request = &m_rpcBuf;

    ASSERT_RPC(sizeof(int));
    sceSifCallRpc(&m_rpcCd, eCtlDownInterface,
                  0,
                  NULL, 0,
                  request, sizeof(int),
                  NULL, NULL);
    return request->args[0];
}
#else   // !INET_VERSION
{
    int err = sceEENETCTL_E_Invalid;
    if (m_status.ifname[0]) {
        err = sceEENetCtlDownInterface(m_status.ifname);
    }
    return err;
}
#endif  // !INET_VERSION


/*
 *  sample the bound network interfaces for the current state.
 *
 *  r = 0   when update successful.
 *  r < 0   when sce{EENet,Inet}CtlGetState() returns error.
 */
int
ezNetCtlGetStatus(ezNetCtlStatus_t *s, char *msgBuf, unsigned int msgBufSize)
#ifdef INET_VERSION
{
    /*
     *  eznetctl.c overloads rpcRequest_t structure:
     *  rpcRequest_t.buf1[] = ifstat string buffer
     *  rpcRequest_t.buf2[] = ezNetCtlStatus_t
     *
     *  there really should be an assert for the overloaded size.
     */
    rpcRequest_t *request = &m_rpcBuf;

    ASSERT_RPC(sizeof(rpcRequest_t));
    sceSifCallRpc(&m_rpcCd, eCtlGetStatus,
                  0,
                  0, 0,
                  request, sizeof(rpcRequest_t),
                  0, 0);

    // retrieve overloaded ezNetCtlStatus_t from eznetctl.c
    memcpy(s, request->buf2, sizeof(*s));
    // recreate ezNetCtlStatus_t->error and ifstat fields
    ezNetCtlRecreateIfstat(request->buf1, s, &m_ifstat);
    // localize status message
    ezNetCtlLocalizeMessage(msgBuf, msgBufSize, s, &m_ifstat);
    return request->args[0];
}
#else   // !INET_VERSION
{
    int err = 0;
    int len;
    char *ifname;

    // no active network I/F
    if (m_status.iftype == sceEENET_IFTYPE_UNKNOWN) {
        goto done;
    }

    // update logical I/F state
    ifname = m_status.ifname2[0] ? m_status.ifname2 : m_status.ifname;
# if (EENETCTL_STATE_WORKAROUND)
    /*
     *  eenetctl state workaround (part 1 of 3)
     *
     *  there are two windows of getting the spurious S_STOPPED:
     *  1. after UpInterface() and before S_STARTING (race condition),
     *  2. during busy redial between IEV_Error+IEV_Stop+IEV_Retry.
     *
     *  the workaround requires witholding excessive state updates.
     *  while the I/F is running, only update the state from within
     *  ievHandler() after an appropriate IEV event. poll the state 
     *  here only when no IEV events are pending:
     *  - the I/F is not running,
     *  - the current state is terminal S_DETACHED or S_STOPPED.
     */
    {
        u_short flags;

        len = sizeof(flags);
        if ( !(err = sceEENetGetIfinfo(ifname, sceEENET_IFINFO_IFFLAGS, &flags, &len)) ) {
            if ( !(flags & IFF_RUNNING) || 
                 m_status.state == sceEENETCTL_S_DETACHED ||
                 m_status.state == sceEENETCTL_S_STOPPED )
            {
                err = sceEENetCtlGetState(ifname, AF_INET, &m_status.state);
            }
        }
    }
# else
    err = sceEENetCtlGetState(ifname, AF_INET, &m_status.state);
# endif
    if (err) {
        m_status.state = sceEENETCTL_S_DETACHED;
        goto done;
    }

    // update Ethernet I/F link status (at most 1 Hz)
    ifname = m_status.ifname;
    if (m_status.iftype == sceEENET_IFTYPE_ETHER || m_status.iftype == sceEENET_IFTYPE_PPPOE) {
        u_short media;
        static u_long lastTime = 0;
        u_long thisTime = GetTimerSystemTime();

        if ((thisTime-lastTime) > 147456000UL) {
            lastTime = thisTime;
            len = sizeof(media);
            if (0 == sceEENetGetIfinfo(ifname, sceEENET_IFINFO_MEDIA, &media, &len)) {
                if (media & sceEENET_MEDIA_AVALID) {
                    m_status.link = (media & sceEENET_MEDIA_ACTIVE) >> 1;
                }
            }
        }
    }

    // gather detailed ifstat connection information
    memset(&m_ifstat, 0, sizeof(m_ifstat));
    switch (m_status.iftype) {
    case sceEENET_IFTYPE_ETHER:
        sceEENetCtlGetIfStat(ifname, AF_INET, &m_ifstat.dhcp,  sizeof(m_ifstat.dhcp));
        break;
    case sceEENET_IFTYPE_PPP:
        sceEENetCtlGetIfStat(ifname, AF_INET, &m_ifstat.ppp,   sizeof(m_ifstat.ppp));
        if (m_status.error && m_ifstat.ppp.ppp_phase == sceEENETCTL_P_PppEstablish) {
# if (EENETCTL_STATE_WORKAROUND)
            /*
             *  eenetctl state workaround (part 2 of 3)
             *
             *  eenetctl does not generate an IEV event when dialing 
             *  resumes after the redial delay, thus S_RETRYING will not
             *  change back to S_STARTING as it does under inetctl.
             *  this workaround here emulates the inetctl behavior.
             */
            m_status.state = sceEENETCTL_S_STARTING;
# endif
            m_status.error = 0;
        }
        break;
    case sceEENET_IFTYPE_PPPOE:
        sceEENetCtlGetIfStat(ifname, AF_INET, &m_ifstat.pppoe, sizeof(m_ifstat.pppoe));
        break;
    }

done:
    ezNetCtlLocalizeMessage(msgBuf, msgBufSize, &m_status, &m_ifstat);
    memcpy(s, &m_status, sizeof(*s));
    return err;
}
#endif  // !INET_VERSION


int
ezNetCtlWaitEventTO(int ms)
#ifdef INET_VERSION
{
    rpcRequest_t *request = &m_rpcBuf;

    ASSERT_RPC(sizeof(int));
    request->args[0] = ms;

    sceSifCallRpc(&m_rpcCd, eCtlWaitEvent,
                  0,
                  request, sizeof(int),
                  request, sizeof(int),
                  0, 0);

    if (request->args[0] != -1)
        dprintf ("returned %d\n", request->args[0]);
    return request->args[0];
}
#else   // !INET_VERSION
{
    int alarmId = -1;
    int event   = -1;

    // poll first to see an IEV event occured since last.
    // the EE kernel does not enforce SemaParam.maxCount,
    // consume semaphore count here to prevent overflow.
    while (PollSema(m_ievSema) == m_ievSema) {
        event = m_ievLast;
    }

    // event not yet available, and caller is willing 
    // to wait (ms > 0 or ms == -1) for an event.
    if (ms && event < 0) {
        if (ms > 0) {
            int tid = GetThreadId();
            u_long cs = TimerUSec2BusClock(ms / 1000, (ms % 1000) * 1000);
            alarmId = SetTimerAlarm(cs, alarmHandler, (void *)tid);
            if (alarmId < 0) {
                dprintf("SetTimerAlarm failed (%d)\n", alarmId);
                goto exit;
            }
        }

        if (WaitSema(m_ievSema) == m_ievSema) {
            event = m_ievLast;
            // cancel outstanding alarm
            if (alarmId >= 0) {
                ReleaseTimerAlarm(alarmId);
            }
        }
    }

exit:
    if (event != -1)
        dprintf("returned %d\n", event);
    return event;
}
#endif  // !INET_VERSION


/*  --------------------------------------------------------
 *  PRIVATE library functions
 *  --------------------------------------------------------
 */

/*
 *  EENET ONLY: alarm handler
 */
#ifndef INET_VERSION
static u_long
alarmHandler(int alarmId, u_long ulSchedule, u_long ulActual, void *arg, void *addr)
{
    iReleaseWaitThread((int)arg);
    return 0;
}
#endif  // !INET_VERSION


/*
 *  EENET ONLY: eenetctl I/F event handler
 *
 *  eznetctl implementation notes

eenetctl vs inetctl differences, with respect to the PPPoE virtual I/F:
inetctl:    SetConfiguration() creates the virtual I/F when called with
            a PPPoE configuration. SetConfiguration() also deletes any
            existing virtual I/F if called with a non-PPPoE config.
eenetctl:   UpInterface() creates the virtual I/F, 
            DownInterface() deletes it.

I/F events are also delivered differently. the simplified eznetctl 
state indicator is kept to be consistent for both stacks.

eenetctl:       SetConfiguration()      UpInterface()
-----------------------------------------------------------------------
smap0 event:    Conf+Attach             Up              UpCompleted
smap0 state:    Stopped                 Starting >>>>>>>>>>>>>>>>>>>>>>

pppoe0 event:   (none)                  NoConf+Attach   Start
pppoe0 state:   (none)                  Stopped         Started >>>>>>>


inetctl:        SetConfiguration()      UpInterface()
-----------------------------------------------------------------------
eth0 event:     Conf+Attach             Up              (none)
eth0 state:     Stopped                 Starting >>>>>>>>>>>>>>>>>>>>>>

ppp0 event:     Conf+Attach             Up              Start
ppp0 state:     Stopped                 Starting        Started >>>>>>>


eznetctl:       SetConfiguration()      UpInterface()
-----------------------------------------------------------------------
state:          Stopped                 Starting        Started >>>>>>>

 */
#ifndef INET_VERSION
static void 
ievHandler(const char *ifname, int af, int event)
{
    int error, subcode;
# if (EENETCTL_STATE_WORKAROUND)
    int isStatefulEvent = 0;
# endif
    int isTerminalEvent = 0;

# if 0
    dprintf("<< %s event %d >>\n", ifname, event);
# endif
    switch (event) {
    case sceEENETCTL_IEV_Conf:
        // the physical I/F that binds to the configuration reports 
        // IEV_Conf+IEV_Attach, wait for IEV_Attach before doSignalSema
        strncpy(m_status.ifname, ifname, sizeof(m_status.ifname));
        if (!strncmp(ifname, "ppp", 3)) {
            m_status.iftype = sceEENET_IFTYPE_PPP;
        } else {
            m_status.iftype = sceEENET_IFTYPE_ETHER;
        }
        // reset the error field
        m_status.error = 0;
        m_status.link = -1;
        break;

    case sceEENETCTL_IEV_Attach:
        // announce the ifname only for the I/F that reported IEV_Conf
        if (!strcmp(ifname, m_status.ifname)) {
            isTerminalEvent = 1;
        }
        // the PPPoE virtual I/F reports IEV_NoConf+IEV_Attach, while
        // in S_STOPPED. wait for IEV_Start before announcing ifname2
        if (!strncmp(ifname, "pppoe", 5)) {
            m_status.iftype = sceEENET_IFTYPE_PPPOE;
        }
        break;

    case sceEENETCTL_IEV_Start:
        // announce ifname2 once the virtual I/F reaches S_STARTED
        if (!strncmp(ifname, "pppoe", 5)) {
            strncpy(m_status.ifname2, ifname, sizeof(m_status.ifname2));
        }
# if (EENETCTL_STATE_WORKAROUND)
        isStatefulEvent = 1;
# endif
        break;

    case sceEENETCTL_IEV_Stop:
# if (EENETCTL_STATE_WORKAROUND)
        isStatefulEvent = 1;
# endif
        break;

    case sceEENETCTL_IEV_Error:
        sceEENetCtlGetErrorCode(ifname, AF_INET, &error, &subcode);
        m_status.error = (error == sceEENETCTL_E_PppLinkEstab) ? subcode : error;
# if (EENETCTL_STATE_WORKAROUND)
        isStatefulEvent = 1;
# endif
        break;

    case sceEENETCTL_IEV_UpCompleted:
    case sceEENETCTL_IEV_DownCompleted:
# if (EENETCTL_STATE_WORKAROUND)
        isStatefulEvent = 1;
# endif
        isTerminalEvent = 1;
        break;

    case sceEENETCTL_IEV_Detach:
    case sceEENETCTL_IEV_DetachCompleted:
        // virtual I/F detachments are not event-worthy
        if (!strcmp(ifname, m_status.ifname2)) {
            memset(m_status.ifname2, 0, sizeof(m_status.ifname2));
        }
        // physical I/F detachments are event-worthy
        if (!strcmp(ifname, m_status.ifname)) {
# if (EENETCTL_STATE_WORKAROUND)
            isStatefulEvent = 1;
# endif
            isTerminalEvent = 1;
        }
        break;
    }

# if (EENETCTL_STATE_WORKAROUND)
    if (isStatefulEvent) {
        /*
         *  eenetctl state workaround (part 3 of 3)
         *
         *  update the state after appropriate IEV events, except
         *  during busy redial between IEV_Error+IEV_Stop+IEV_Retry,
         *  when the state is overrode as S_RETRYING.
         */
        if (m_status.error == sceEENETCTL_EE_ModemBusy && event != sceEENETCTL_IEV_DownCompleted) {
            m_status.state = sceEENETCTL_S_RETRYING;
        } else {
            ifname = m_status.ifname2[0] ? m_status.ifname2 : m_status.ifname;
            if (0 != sceEENetCtlGetState(ifname, AF_INET, &m_status.state)) {
                m_status.state = sceEENETCTL_S_DETACHED;
            }
        }
    }
# endif

    if (isTerminalEvent) {
        m_ievLast = event;
        SignalSema(m_ievSema);
    }
}
#endif  // !INET_VERSION
