/* $Id: DevSMC.cpp 35353 2010-12-27 17:25:52Z vboxsync $ */
/** @file
 * DevSMC - SMC device emulation.
 */

/*
 * Copyright (C) 2006-2010 Oracle Corporation
 *
 * This file is part of VirtualBox Open Source Edition (OSE), as
 * available from http://www.virtualbox.org. This file is free software;
 * you can redistribute it and/or modify it under the terms of the GNU
 * General Public License (GPL) as published by the Free Software
 * Foundation, in version 2 as it comes in the "COPYING" file of the
 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
 * --------------------------------------------------------------------
 *
 * This code is based on:
 *
 *  Apple SMC controller
 *
 *  Copyright (c) 2007 Alexander Graf
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * *****************************************************************
 *
 * In all Intel-based Apple hardware there is an SMC chip to control the
 * backlight, fans and several other generic device parameters. It also
 * contains the magic keys used to dongle Mac OS X to the device.
 *
 * This driver was mostly created by looking at the Linux AppleSMC driver
 * implementation and does not support IRQ.
 *
 */

/*******************************************************************************
*   Header Files                                                               *
*******************************************************************************/
#define LOG_GROUP LOG_GROUP_DEV_SMC
#include <VBox/vmm/pdmdev.h>
#include <VBox/log.h>
#include <VBox/vmm/stam.h>
#include <iprt/assert.h>
#include <iprt/string.h>
#ifdef IN_RING0
# include <iprt/asm-amd64-x86.h>
# include <iprt/once.h>
# include <iprt/thread.h>
#endif

#include "VBoxDD2.h"


/*******************************************************************************
*   Defined Constants And Macros                                               *
*******************************************************************************/
/* data port used by Apple SMC */
#define APPLESMC_DATA_PORT      0x300
/* command/status port used by Apple SMC */
#define APPLESMC_CMD_PORT       0x304
#define APPLESMC_NR_PORTS       32 /* 0x300-0x31f */
#define APPLESMC_MAX_DATA_LENGTH 32

#define APPLESMC_READ_CMD       0x10
#define APPLESMC_WRITE_CMD      0x11
#define APPLESMC_GET_KEY_BY_INDEX_CMD   0x12
#define APPLESMC_GET_KEY_TYPE_CMD       0x13

/** The version of the saved state. */
#define SMC_SAVED_STATE_VERSION 1

/** The ring-0 operation number that attempts to get OSK0 and OSK1 from the real
 *  SMC. */
#define SMC_CALLR0_READ_OSK     1

/*******************************************************************************
*   Structures and Typedefs                                                    *
*******************************************************************************/
typedef struct AppleSMCData
{
    uint8_t       len;
    const char    *key;
    const char    *data;
} AppleSMCData;


typedef struct
{
    PPDMDEVINSR3   pDevIns;

    uint8_t cmd;
    uint8_t status;
    uint8_t key[4];
    uint8_t read_pos;
    uint8_t data_len;
    uint8_t data_pos;
    uint8_t data[255];

    /** The OSK0 value. This is currently only used in the constructor. */
    uint8_t abOsk0[32];
    /** The OSK1 value. This is currently only used in the constructor */
    uint8_t abOsk1[32];
} SMCState;

/*******************************************************************************
*   Global Variables                                                           *
*******************************************************************************/
#ifdef IN_RING3
static char osk[64];

/* See http://www.mactel-linux.org/wiki/AppleSMC */
static struct AppleSMCData data[] =
{
    {6, "REV ", "\0x01\0x13\0x0f\0x00\0x00\0x03"},
    {32,"OSK0", osk },
    {32,"OSK1", osk+32 },
    {1, "NATJ",  "\0" },
    {1, "MSSP",  "\0" },
    {1, "MSSD",  "\0x3" },
    {1, "NTOK",  "\0"},
    {0, NULL,    NULL }
};
#endif /* IN_RING3 */
#ifdef IN_RING0
/** Do once for the SMC ring-0 static data (g_abOsk0, g_abOsk1, g_fHaveOsk).  */
static RTONCE   g_SmcR0Once = RTONCE_INITIALIZER;
/** Indicates whether we've successfully queried the OSK* keys. */
static bool     g_fHaveOsk = false;
/** The OSK0 value.   */
static uint8_t  g_abOsk0[32];
/** The OSK1 value.  */
static uint8_t  g_abOsk1[32];
#endif /* IN_RING0 */

#ifndef VBOX_DEVICE_STRUCT_TESTCASE

#ifdef IN_RING0

/**
 * Waits for the specified status on the host SMC.
 *
 * @returns success indicator.
 * @param   bStatus             The desired status.
 * @param   pszWhat             What we're currently doing. For the log.
 */
static bool devR0SmcWaitHostStatus(uint8_t bStatus, const char *pszWhat)
{
    uint8_t bCurStatus;
    for (uint32_t cMsSleep = 1; cMsSleep <= 64; cMsSleep <<= 1)
    {
        RTThreadSleep(cMsSleep);
        bCurStatus = ASMInU8(APPLESMC_CMD_PORT);
        if ((bCurStatus & 0xf) == bStatus)
            return true;
    }

    LogRel(("devR0Smc: %s: bCurStatus=%#x, wanted %#x.\n", pszWhat, bCurStatus, bStatus));
    return false;
}

/**
 * Reads a key by name from the host SMC.
 *
 * @returns success indicator.
 * @param   pszName             The key name, must be exactly 4 chars long.
 * @param   pbBuf               The output buffer.
 * @param   cbBuf               The buffer size. Max 32 bytes.
 */
static bool devR0SmcQueryHostKey(const char *pszName, uint8_t *pbBuf, size_t cbBuf)
{
    Assert(strlen(pszName) == 4);
    Assert(cbBuf <= 32);
    Assert(cbBuf > 0);

    /*
     * Issue the READ command.
     */
    uint32_t cMsSleep = 1;
    for (;;)
    {
        ASMOutU8(APPLESMC_CMD_PORT, APPLESMC_READ_CMD);
        RTThreadSleep(cMsSleep);
        uint8_t bCurStatus = ASMInU8(APPLESMC_CMD_PORT);
        if ((bCurStatus & 0xf) == 0xc)
            break;
        cMsSleep <<= 1;
        if (cMsSleep > 64)
        {
            LogRel(("devR0Smc: %s: bCurStatus=%#x, wanted %#x.\n", "cmd", bCurStatus, 0xc));
            return false;
        }
    }

    /*
     * Send it the key.
     */
    for (unsigned off = 0; off < 4; off++)
    {
        ASMOutU8(APPLESMC_DATA_PORT, pszName[off]);
        if (!devR0SmcWaitHostStatus(4, "key"))
            return false;
    }

    /*
     * The desired amount of output.
     */
    ASMOutU8(APPLESMC_DATA_PORT, (uint8_t)cbBuf);

    /*
     * Read the output.
     */
    for (size_t off = 0; off < cbBuf; off++)
    {
        if (!devR0SmcWaitHostStatus(5, off ? "data" : "len"))
            return false;
        pbBuf[off] = ASMInU8(APPLESMC_DATA_PORT);
    }

    return true;
}

/**
 * RTOnce callback that initializes g_fHaveOsk, g_abOsk0 and g_abOsk1.
 *
 * @returns VINF_SUCCESS.
 * @param   pvUser1Ignored  Ignored.
 * @param   pvUser2Ignored  Ignored.
 */
static DECLCALLBACK(int) devR0SmcInitOnce(void *pvUser1Ignored, void *pvUser2Ignored)
{
    g_fHaveOsk = devR0SmcQueryHostKey("OSK0", &g_abOsk0[0], sizeof(g_abOsk0))
              && devR0SmcQueryHostKey("OSK1", &g_abOsk1[0], sizeof(g_abOsk1));

    NOREF(pvUser1Ignored); NOREF(pvUser2Ignored);
    return VINF_SUCCESS;
}

/**
 * @interface_method_impl{FNPDMDEVREQHANDLERR0}
 */
PDMBOTHCBDECL(int) devR0SmcReqHandler(PPDMDEVINS pDevIns, uint32_t uOperation, uint64_t u64Arg)
{
    SMCState *pThis = PDMINS_2_DATA(pDevIns, SMCState *);
    int       rc    = VERR_INVALID_FUNCTION;

    if (uOperation == SMC_CALLR0_READ_OSK)
    {
        rc = RTOnce(&g_SmcR0Once, devR0SmcInitOnce, NULL, NULL);
        if (   RT_SUCCESS(rc)
            && g_fHaveOsk)
        {
            AssertCompile(sizeof(g_abOsk0) == sizeof(pThis->abOsk0));
            AssertCompile(sizeof(g_abOsk1) == sizeof(pThis->abOsk1));
            memcpy(pThis->abOsk0, g_abOsk0, sizeof(pThis->abOsk0));
            memcpy(pThis->abOsk1, g_abOsk1, sizeof(pThis->abOsk1));
        }
    }
    return rc;
}

#endif /* IN_RING0 */
#ifdef IN_RING3

/**
 * Saves a state of the SMC device.
 *
 * @returns VBox status code.
 * @param   pDevIns     The device instance.
 * @param   pSSMHandle  The handle to save the state to.
 */
static DECLCALLBACK(int) smcSaveExec(PPDMDEVINS pDevIns, PSSMHANDLE pSSMHandle)
{
    SMCState *pThis = PDMINS_2_DATA(pDevIns, SMCState *);

    /** @todo: implement serialization */
    return VINF_SUCCESS;
}


/**
 * Loads a SMC device state.
 *
 * @returns VBox status code.
 * @param   pDevIns     The device instance.
 * @param   pSSMHandle  The handle to the saved state.
 * @param   uVersion    The data unit version number.
 * @param   uPass       The data pass.
 */
static DECLCALLBACK(int) smcLoadExec(PPDMDEVINS pDevIns, PSSMHANDLE pSSMHandle, uint32_t uVersion, uint32_t uPass)
{
    SMCState *pThis = PDMINS_2_DATA(pDevIns, SMCState *);

    if (uVersion != SMC_SAVED_STATE_VERSION)
        return VERR_SSM_UNSUPPORTED_DATA_UNIT_VERSION;
    Assert(uPass == SSM_PASS_FINAL); NOREF(uPass);

    /** @todo: implement serialization */
    return VINF_SUCCESS;
}

/**
 * Reset notification.
 *
 * @returns VBox status.
 * @param   pDevIns     The device instance data.
 */
static DECLCALLBACK(void) smcReset(PPDMDEVINS pDevIns)
{
    SMCState *pThis = PDMINS_2_DATA(pDevIns, SMCState *);
    LogFlow(("smcReset: \n"));
}


/**
 * Info handler, device version.
 *
 * @param   pDevIns     Device instance which registered the info.
 * @param   pHlp        Callback functions for doing output.
 * @param   pszArgs     Argument string. Optional and specific to the handler.
 */
static DECLCALLBACK(void) smcInfo(PPDMDEVINS pDevIns, PCDBGFINFOHLP pHlp, const char *pszArgs)
{
    SMCState   *pThis = PDMINS_2_DATA(pDevIns, SMCState *);
}


static void applesmc_fill_data(SMCState *s)
{
    struct AppleSMCData *d;
    for (d=data; d->len; d++)
    {
        uint32_t key_data = *((uint32_t*)d->key);
        uint32_t key_current = *((uint32_t*)s->key);
        if (key_data == key_current)
        {
            Log(("APPLESMC: Key matched (%s Len=%d Data=%s)\n", d->key, d->len, d->data));
            memcpy(s->data, d->data, d->len);
            return;
        }
    }
}

/**
 * Port I/O Handler for IN operations.
 *
 * @returns VBox status code.
 *
 * @param   pDevIns     The device instance.
 * @param   pvUser      User argument - ignored.
 * @param   uPort       Port number used for the IN operation.
 * @param   pu32        Where to store the result.
 * @param   cb          Number of bytes read.
 */
PDMBOTHCBDECL(int) smcIOPortRead(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT Port, uint32_t *pu32, unsigned cb)
{
    SMCState * s = PDMINS_2_DATA(pDevIns, SMCState *);
    uint8_t retval = 0;

    NOREF(pvUser);
    Log(("SMC port read: %x (%d)\n", Port, cb));

    /** @todo: status code? */
    if (cb != 1)
        return VERR_IOM_IOPORT_UNUSED;

    switch (Port)
    {
        case APPLESMC_CMD_PORT:
        {
            retval = s->status;
            break;
        }
        case APPLESMC_DATA_PORT:
        {
            switch (s->cmd) {
                case APPLESMC_READ_CMD:
                    if(s->data_pos < s->data_len)
                    {
                        retval = s->data[s->data_pos];
                        Log(("APPLESMC: READ_DATA[%d] = %#hhx\n", s->data_pos, retval));
                        s->data_pos++;
                        if(s->data_pos == s->data_len)
                        {
                            s->status = 0x00;
                            Log(("APPLESMC: EOF\n"));
                        }
                        else
                            s->status = 0x05;
                    }
            }
            break;
        }
    }

    *pu32 = retval;

    return VINF_SUCCESS;
}


/**
 * Port I/O Handler for OUT operations.
 *
 * @returns VBox status code.
 *
 * @param   pDevIns     The device instance.
 * @param   pvUser      User argument - ignored.
 * @param   uPort       Port number used for the IN operation.
 * @param   u32         The value to output.
 * @param   cb          The value size in bytes.
 */
PDMBOTHCBDECL(int) smcIOPortWrite(PPDMDEVINS pDevIns, void *pvUser, RTIOPORT Port, uint32_t u32, unsigned cb)
{
    SMCState* s = PDMINS_2_DATA(pDevIns, SMCState *);

    NOREF(pvUser);

    Log(("SMC port write: %x (%d) %x\n", Port, cb, u32));
    /** @todo: status code? */
    if (cb != 1)
        return VINF_SUCCESS;

    switch (Port)
    {
        case APPLESMC_CMD_PORT:
        {
            switch (u32)
            {
                case APPLESMC_READ_CMD:
                    s->status = 0x0c;
                    break;
            }
            s->cmd = u32;
            s->read_pos = 0;
            s->data_pos = 0;
            break;
        }
        case APPLESMC_DATA_PORT:
        {
            switch(s->cmd)
            {
                case APPLESMC_READ_CMD:
                    if (s->read_pos < 4)
                    {
                        s->key[s->read_pos] = u32;
                        s->status = 0x04;
                    }
                    else
                    if (s->read_pos == 4)
                    {
                        s->data_len = u32;
                        s->status = 0x05;
                        s->data_pos = 0;
                        Log(("APPLESMC: Key = %c%c%c%c Len = %d\n", s->key[0], s->key[1], s->key[2], s->key[3], u32));
                        applesmc_fill_data(s);
                    }
                    s->read_pos++;
                    break;
            }
        }
    }
    return VINF_SUCCESS;
}


/**
 * @interface_method_impl{PDMDEVREG,pfnConstruct}
 */
static DECLCALLBACK(int) smcConstruct(PPDMDEVINS pDevIns, int iInstance, PCFGMNODE pCfg)
{
    SMCState   *pThis = PDMINS_2_DATA(pDevIns, SMCState *);
    Assert(iInstance == 0);

    /*
     * Store state.
     */
    pThis->pDevIns = pDevIns;

    /*
     * Validate and read the configuration.
     */
    PDMDEV_VALIDATE_CONFIG_RETURN(pDevIns, "DeviceKey|GetKeyFromRealSMC", "");

    /*
     * Read the DeviceKey config value.
     */
    char *pszDeviceKey;
    int rc = CFGMR3QueryStringAllocDef(pCfg, "DeviceKey", &pszDeviceKey, "");
    if (RT_FAILURE(rc))
        return PDMDevHlpVMSetError(pDevIns, rc, RT_SRC_POS,
                                   N_("Configuration error: Querying \"DeviceKey\" as a string failed"));

    size_t cchDeviceKey = strlen(pszDeviceKey);
    if (cchDeviceKey > 0)
        memcpy(&pThis->abOsk0[0], pszDeviceKey, RT_MIN(cchDeviceKey, sizeof(pThis->abOsk0)));
    if (cchDeviceKey > sizeof(pThis->abOsk0))
        memcpy(&pThis->abOsk1[0], &pszDeviceKey[sizeof(pThis->abOsk0)],
               RT_MIN(cchDeviceKey - sizeof(pThis->abOsk0), sizeof(pThis->abOsk1)));

    MMR3HeapFree(pszDeviceKey);

    /*
     * Query the key from the real hardware if asked to do so.
     */
    bool fGetKeyFromRealSMC;
    rc = CFGMR3QueryBoolDef(pCfg, "GetKeyFromRealSMC", &fGetKeyFromRealSMC, false);
    if (RT_FAILURE(rc))
        return PDMDevHlpVMSetError(pDevIns, rc, RT_SRC_POS,
                                   N_("Configuration error: Querying \"GetKeyFromRealSMC\" as a boolean failed"));
    if (fGetKeyFromRealSMC)
    {
        rc = PDMDevHlpCallR0(pDevIns, SMC_CALLR0_READ_OSK, 0 /*u64Arg*/);
        if (RT_FAILURE(rc))
            return PDMDevHlpVMSetError(pDevIns, rc, RT_SRC_POS,
                                       N_("Failed to query SMC value from the host"));
    }

    /*
     * For practical/historical reasons, the OSK[0|1] data is stored in a
     * global buffer in ring-3.
     */
    AssertCompile(sizeof(osk) == sizeof(pThis->abOsk0) + sizeof(pThis->abOsk1));
    AssertCompile(sizeof(char) == sizeof(uint8_t));
    memcpy(osk, pThis->abOsk0, sizeof(pThis->abOsk0));
    memcpy(&osk[sizeof(pThis->abOsk0)], pThis->abOsk1, sizeof(pThis->abOsk1));

    /*
     * Register the IO ports.
     */
    rc = PDMDevHlpIOPortRegister(pDevIns, APPLESMC_DATA_PORT, 1, NULL,
                                 smcIOPortWrite, smcIOPortRead,
                                 NULL, NULL, "SMC Data");
    if (RT_FAILURE(rc))
        return rc;
    rc = PDMDevHlpIOPortRegister(pDevIns, APPLESMC_CMD_PORT, 1, NULL,
                                 smcIOPortWrite, smcIOPortRead,
                                 NULL, NULL, "SMC Commands");
    if (RT_FAILURE(rc))
        return rc;

    /* Register saved state management */
    rc = PDMDevHlpSSMRegister(pDevIns, SMC_SAVED_STATE_VERSION, sizeof(*pThis), smcSaveExec, smcLoadExec);
    if (RT_FAILURE(rc))
        return rc;

    /*
     * Initialize the device state.
     */
    smcReset(pDevIns);

    /**
     * @todo: Register statistics.
     */
    PDMDevHlpDBGFInfoRegister(pDevIns, "smc", "Display SMC status. (no arguments)", smcInfo);

    return VINF_SUCCESS;
}

/**
 * The device registration structure.
 */
const PDMDEVREG g_DeviceSMC =
{
    /* u32Version */
    PDM_DEVREG_VERSION,
    /* szName */
    "smc",
    /* szRCMod */
    "VBoxDD2GC.gc",
    /* szR0Mod */
    "VBoxDD2R0.r0",
    /* pszDescription */
    "System Management Controller (SMC) Device",
    /* fFlags */
    PDM_DEVREG_FLAGS_HOST_BITS_DEFAULT | PDM_DEVREG_FLAGS_GUEST_BITS_32_64 | PDM_DEVREG_FLAGS_PAE36| PDM_DEVREG_FLAGS_R0,
    /* fClass */
    PDM_DEVREG_CLASS_MISC,
    /* cMaxInstances */
    1,
    /* cbInstance */
    sizeof(SMCState),
    /* pfnConstruct */
    smcConstruct,
    /* pfnDestruct */
    NULL,
    /* pfnRelocate */
    NULL,
    /* pfnIOCtl */
    NULL,
    /* pfnPowerOn */
    NULL,
    /* pfnReset */
    smcReset,
    /* pfnSuspend */
    NULL,
    /* pfnResume */
    NULL,
    /* pfnAttach */
    NULL,
    /* pfnDetach */
    NULL,
    /* pfnQueryInterface. */
    NULL,
    /* pfnInitComplete */
    NULL,
    /* pfnPowerOff */
    NULL,
    /* pfnSoftReset */
    NULL,
    /* u32VersionEnd */
    PDM_DEVREG_VERSION
};

#endif /* IN_RING3 */

#endif /* VBOX_DEVICE_STRUCT_TESTCASE */
