/*
 * Copyright (c) 2007-2008 NVIDIA, Corporation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#ifdef HAVE_XEXTPROTO_71
#include <X11/extensions/dpmsconst.h>
#else
#define DPMS_SERVER
#include <X11/extensions/dpms.h>
#endif

#include <X11/Xatom.h>

#include "g80_type.h"
#include "g80_display.h"
#include "g80_output.h"

static void
G80SorSetPClk(xf86OutputPtr output, int pclk)
{
    G80Ptr pNv = G80PTR(output->scrn);
    G80OutputPrivPtr pPriv = output->driver_private;
    const int orOff = 0x800 * pPriv->or;
    const int limit = 165000;

    pNv->reg[(0x00614300+orOff)/4] = 0x70000 | (pclk > limit ? 0x101 : 0);
}

static void
G80SorDPMSSet(xf86OutputPtr output, int mode)
{
    G80Ptr pNv = G80PTR(output->scrn);
    G80OutputPrivPtr pPriv = output->driver_private;
    const int off = 0x800 * pPriv->or;
    CARD32 tmp;

    while(pNv->reg[(0x0061C004+off)/4] & 0x80000000);

    tmp = pNv->reg[(0x0061C004+off)/4];
    tmp |= 0x80000000;

    if(mode == DPMSModeOn)
        tmp |= 1;
    else
        tmp &= ~1;

    pNv->reg[(0x0061C004+off)/4] = tmp;
    while((pNv->reg[(0x61C030+off)/4] & 0x10000000));
}

static int
G80TMDSModeValid(xf86OutputPtr output, DisplayModePtr mode)
{
    G80Ptr pNv = G80PTR(output->scrn);

    // Disable dual-link modes unless enabled in the config file.
    if (mode->Clock > 165000 && !pNv->AllowDualLink)
        return MODE_CLOCK_HIGH;

    return G80OutputModeValid(output, mode);
}

static int
G80LVDSModeValid(xf86OutputPtr output, DisplayModePtr mode)
{
    G80OutputPrivPtr pPriv = output->driver_private;
    DisplayModePtr native = pPriv->nativeMode;

    // Ignore modes larger than the native res.
    if (mode->HDisplay > native->HDisplay || mode->VDisplay > native->VDisplay)
        return MODE_PANEL;

    return G80OutputModeValid(output, mode);
}

static void
G80SorModeSet(xf86OutputPtr output, DisplayModePtr mode,
              DisplayModePtr adjusted_mode)
{
    ScrnInfoPtr pScrn = output->scrn;
    G80OutputPrivPtr pPriv = output->driver_private;
    const int sorOff = 0x40 * pPriv->or;
    CARD32 type;

    if(!adjusted_mode) {
        /* Disconnect the SOR */
        C(0x00000600 + sorOff, 0);
        return;
    }

    if(pPriv->panelType == LVDS)
        type = 0;
    else if(adjusted_mode->Clock > 165000)
        type = 0x500;
    else
        type = 0x100;

    // This wouldn't be necessary, but the server is stupid and calls
    // G80SorDPMSSet after the output is disconnected, even though the hardware
    // turns it off automatically.
    G80SorDPMSSet(output, DPMSModeOn);

    C(0x00000600 + sorOff,
        (G80CrtcGetHead(output->crtc) == HEAD0 ? 1 : 2) |
        type |
        ((adjusted_mode->Flags & V_NHSYNC) ? 0x1000 : 0) |
        ((adjusted_mode->Flags & V_NVSYNC) ? 0x2000 : 0));

    G80CrtcSetScale(output->crtc, adjusted_mode, pPriv->scale);
}

static xf86OutputStatus
G80SorDetect(xf86OutputPtr output)
{
    G80OutputPrivPtr pPriv = output->driver_private;

    /* Assume physical status isn't going to change before the BlockHandler */
    if(pPriv->cached_status != XF86OutputStatusUnknown)
        return pPriv->cached_status;

    G80OutputPartnersDetect(pPriv->partner, output, pPriv->i2c);
    return pPriv->cached_status;
}

static xf86OutputStatus
G80SorLVDSDetect(xf86OutputPtr output)
{
    G80OutputPrivPtr pPriv = output->driver_private;

    if(pPriv->i2c) {
        /* If LVDS has an I2C port, use the normal probe routine to get the
         * EDID, if possible. */
        G80SorDetect(output);
    }

    /* Ignore G80SorDetect and assume LVDS is always connected */
    return XF86OutputStatusConnected;
}

static void
G80SorDestroy(xf86OutputPtr output)
{
    G80OutputPrivPtr pPriv = output->driver_private;

    G80OutputDestroy(output);

    xf86DeleteMode(&pPriv->nativeMode, pPriv->nativeMode);

    free(output->driver_private);
    output->driver_private = NULL;
}

static void G80SorSetModeBackend(DisplayModePtr dst, const DisplayModePtr src)
{
    // Stash the backend mode timings from src into dst
    dst->Clock           = src->Clock;
    dst->Flags           = src->Flags;
    dst->CrtcHDisplay    = src->CrtcHDisplay;
    dst->CrtcHBlankStart = src->CrtcHBlankStart;
    dst->CrtcHSyncStart  = src->CrtcHSyncStart;
    dst->CrtcHSyncEnd    = src->CrtcHSyncEnd;
    dst->CrtcHBlankEnd   = src->CrtcHBlankEnd;
    dst->CrtcHTotal      = src->CrtcHTotal;
    dst->CrtcHSkew       = src->CrtcHSkew;
    dst->CrtcVDisplay    = src->CrtcVDisplay;
    dst->CrtcVBlankStart = src->CrtcVBlankStart;
    dst->CrtcVSyncStart  = src->CrtcVSyncStart;
    dst->CrtcVSyncEnd    = src->CrtcVSyncEnd;
    dst->CrtcVBlankEnd   = src->CrtcVBlankEnd;
    dst->CrtcVTotal      = src->CrtcVTotal;
    dst->CrtcHAdjusted   = src->CrtcHAdjusted;
    dst->CrtcVAdjusted   = src->CrtcVAdjusted;
}

static Bool
G80SorModeFixup(xf86OutputPtr output, DisplayModePtr mode,
                DisplayModePtr adjusted_mode)
{
    G80OutputPrivPtr pPriv = output->driver_private;
    DisplayModePtr native = pPriv->nativeMode;

    if(native && pPriv->scale != G80_SCALE_OFF) {
        G80SorSetModeBackend(adjusted_mode, native);
        // This mode is already "fixed"
        G80CrtcSkipModeFixup(output->crtc);
    }

    return TRUE;
}

static Bool
G80SorTMDSModeFixup(xf86OutputPtr output, DisplayModePtr mode,
                    DisplayModePtr adjusted_mode)
{
    int scrnIndex = output->scrn->scrnIndex;
    G80OutputPrivPtr pPriv = output->driver_private;
    DisplayModePtr modes = output->probed_modes;

    xf86DeleteMode(&pPriv->nativeMode, pPriv->nativeMode);

    if(modes) {
        // Find the preferred mode and use that as the "native" mode.
        // If no preferred mode is available, use the first one.
        DisplayModePtr mode;

        // Find the preferred mode.
        for(mode = modes; mode; mode = mode->next) {
            if(mode->type & M_T_PREFERRED) {
                xf86DrvMsgVerb(scrnIndex, X_INFO, 5,
                               "%s: preferred mode is %s\n",
                               output->name, mode->name);
                break;
            }
        }

        // XXX: May not want to allow scaling if no preferred mode is found.
        if(!mode) {
            mode = modes;
            xf86DrvMsgVerb(scrnIndex, X_INFO, 5,
                    "%s: no preferred mode found, using %s\n",
                    output->name, mode->name);
        }

        pPriv->nativeMode = xf86DuplicateMode(mode);
        G80CrtcDoModeFixup(pPriv->nativeMode, mode);
    }

    return G80SorModeFixup(output, mode, adjusted_mode);
}

static DisplayModePtr
G80SorGetLVDSModes(xf86OutputPtr output)
{
    G80OutputPrivPtr pPriv = output->driver_private;

    /* If an EDID was read during detection, use the modes from that. */
    DisplayModePtr modes = G80OutputGetDDCModes(output);
    if(modes)
        return modes;

    /* Otherwise, feed in the mode we read during initialization. */
    return xf86DuplicateMode(pPriv->nativeMode);
}

#define MAKE_ATOM(a) MakeAtom((a), sizeof(a) - 1, TRUE);

struct property {
    Atom atom;
    INT32 range[2];
};

static struct {
    struct property dither;
    struct property scale;
} properties;

static void
G80SorCreateResources(xf86OutputPtr output)
{
    ScrnInfoPtr pScrn = output->scrn;
    G80Ptr pNv = G80PTR(pScrn);
    int data, err;
    const char *s;

    /******** dithering ********/
    properties.dither.atom = MAKE_ATOM("dither");
    properties.dither.range[0] = 0;
    properties.dither.range[1] = 1;
    err = RRConfigureOutputProperty(output->randr_output,
                                    properties.dither.atom, FALSE, TRUE, FALSE,
                                    2, properties.dither.range);
    if(err)
        xf86DrvMsg(pScrn->scrnIndex, X_ERROR,
                   "Failed to configure dithering property for %s: error %d\n",
                   output->name, err);

    // Set the default value
    data = pNv->Dither;
    err = RRChangeOutputProperty(output->randr_output, properties.dither.atom,
                                 XA_INTEGER, 32, PropModeReplace, 1, &data,
                                 FALSE, FALSE);
    if(err)
        xf86DrvMsg(pScrn->scrnIndex, X_ERROR,
                   "Failed to set dithering property for %s: error %d\n",
                   output->name, err);

    /******** scaling ********/
    properties.scale.atom = MAKE_ATOM("scale");
    err = RRConfigureOutputProperty(output->randr_output,
                                    properties.scale.atom, FALSE, FALSE,
                                    FALSE, 0, NULL);
    if(err)
        xf86DrvMsg(pScrn->scrnIndex, X_ERROR,
                   "Failed to configure scaling property for %s: error %d\n",
                   output->name, err);

    // Set the default value
    s = "aspect";
    err = RRChangeOutputProperty(output->randr_output, properties.scale.atom,
                                 XA_STRING, 8, PropModeReplace, strlen(s),
                                 (pointer)s, FALSE, FALSE);
    if(err)
        xf86DrvMsg(pScrn->scrnIndex, X_ERROR,
                   "Failed to set scaling property for %s: error %d\n",
                   output->name, err);
}

static Bool
G80SorSetProperty(xf86OutputPtr output, Atom prop, RRPropertyValuePtr val)
{
    G80OutputPrivPtr pPriv = output->driver_private;

    if(prop == properties.dither.atom) {
        INT32 i;

        if(val->type != XA_INTEGER || val->format != 32 || val->size != 1)
            return FALSE;

        i = *(INT32*)val->data;
        if(i < properties.dither.range[0] || i > properties.dither.range[1])
            return FALSE;

        G80CrtcSetDither(output->crtc, i, TRUE);
    } else if(prop == properties.scale.atom) {
        const char *s;
        enum G80ScaleMode oldScale, scale;
        int i;
        const struct {
            const char *name;
            enum G80ScaleMode scale;
        } modes[] = {
            { "off",    G80_SCALE_OFF },
            { "aspect", G80_SCALE_ASPECT },
            { "fill",   G80_SCALE_FILL },
            { "center", G80_SCALE_CENTER },
            { NULL,     0 },
        };

        if(val->type != XA_STRING || val->format != 8)
            return FALSE;
        s = (char*)val->data;

        for(i = 0; modes[i].name; i++) {
            const char *name = modes[i].name;
            const int len = strlen(name);

            if(val->size == len && !strncmp(name, s, len)) {
                scale = modes[i].scale;
                break;
            }
        }
        if(!modes[i].name)
            return FALSE;
        if(scale == G80_SCALE_OFF && pPriv->panelType == LVDS)
            // LVDS requires scaling
            return FALSE;

        oldScale = pPriv->scale;
        pPriv->scale = scale;
        if(output->crtc) {
            xf86CrtcPtr crtc = output->crtc;

            if(!xf86CrtcSetMode(crtc, &crtc->desiredMode, crtc->desiredRotation,
                                crtc->desiredX, crtc->desiredY)) {
                xf86DrvMsg(crtc->scrn->scrnIndex, X_ERROR,
                           "Failed to set scaling to %s for output %s\n",
                           modes[i].name, output->name);

                // Restore old scale and try again.
                pPriv->scale = oldScale;
                if(!xf86CrtcSetMode(crtc, &crtc->desiredMode,
                                    crtc->desiredRotation, crtc->desiredX,
                                    crtc->desiredY)) {
                    xf86DrvMsg(crtc->scrn->scrnIndex, X_ERROR,
                               "Failed to restore old scaling for output %s\n",
                               output->name);
                }

                return FALSE;
            }
        }
    }

    return TRUE;
}

static const xf86OutputFuncsRec G80SorTMDSOutputFuncs = {
    .dpms = G80SorDPMSSet,
    .save = NULL,
    .restore = NULL,
    .mode_valid = G80TMDSModeValid,
    .mode_fixup = G80SorTMDSModeFixup,
    .prepare = G80OutputPrepare,
    .commit = G80OutputCommit,
    .mode_set = G80SorModeSet,
    .detect = G80SorDetect,
    .get_modes = G80OutputGetDDCModes,
    .create_resources = G80SorCreateResources,
    .set_property = G80SorSetProperty,
    .destroy = G80SorDestroy,
};

static const xf86OutputFuncsRec G80SorLVDSOutputFuncs = {
    .dpms = G80SorDPMSSet,
    .save = NULL,
    .restore = NULL,
    .mode_valid = G80LVDSModeValid,
    .mode_fixup = G80SorModeFixup,
    .prepare = G80OutputPrepare,
    .commit = G80OutputCommit,
    .mode_set = G80SorModeSet,
    .detect = G80SorLVDSDetect,
    .get_modes = G80SorGetLVDSModes,
    .create_resources = G80SorCreateResources,
    .set_property = G80SorSetProperty,
    .destroy = G80SorDestroy,
};

static DisplayModePtr
ReadLVDSNativeMode(G80Ptr pNv, const int off)
{
    DisplayModePtr mode = xnfcalloc(1, sizeof(DisplayModeRec));
    const CARD32 size = pNv->reg[(0x00610B4C+off)/4];
    const int width = size & 0x3fff;
    const int height = (size >> 16) & 0x3fff;

    mode->HDisplay = mode->CrtcHDisplay = width;
    mode->VDisplay = mode->CrtcVDisplay = height;
    mode->Clock           = pNv->reg[(0x610AD4+off)/4] & 0x3fffff;
    mode->CrtcHBlankStart = pNv->reg[(0x610AFC+off)/4];
    mode->CrtcHSyncEnd    = pNv->reg[(0x610B04+off)/4];
    mode->CrtcHBlankEnd   = pNv->reg[(0x610AE8+off)/4];
    mode->CrtcHTotal      = pNv->reg[(0x610AF4+off)/4];

    mode->next = mode->prev = NULL;
    mode->status = MODE_OK;
    mode->type = M_T_DRIVER | M_T_PREFERRED;

    xf86SetModeDefaultName(mode);

    return mode;
}

static DisplayModePtr
GetLVDSNativeMode(G80Ptr pNv)
{
    CARD32 val = pNv->reg[0x00610050/4];

    if((val & 3) == 2)
        return ReadLVDSNativeMode(pNv, 0);
    else if((val & 0x300) == 0x200)
        return ReadLVDSNativeMode(pNv, 0x540);

    return NULL;
}

xf86OutputPtr
G80CreateSor(ScrnInfoPtr pScrn, ORNum or, PanelType panelType)
{
    G80Ptr pNv = G80PTR(pScrn);
    G80OutputPrivPtr pPriv = xnfcalloc(sizeof(*pPriv), 1);
    const int off = 0x800 * or;
    xf86OutputPtr output;
    char orName[5];
    const xf86OutputFuncsRec *funcs;

    if(!pPriv)
        return NULL;

    if(panelType == LVDS) {
        strcpy(orName, "LVDS");
        funcs = &G80SorLVDSOutputFuncs;

        pPriv->nativeMode = GetLVDSNativeMode(pNv);

        if(!pPriv->nativeMode) {
            xf86DrvMsg(pScrn->scrnIndex, X_WARNING,
                       "Failed to find LVDS native mode\n");
            free(pPriv);
            return NULL;
        }

        xf86DrvMsg(pScrn->scrnIndex, X_INFO, "%s native size %dx%d\n",
                   orName, pPriv->nativeMode->HDisplay,
                   pPriv->nativeMode->VDisplay);
    } else {
        snprintf(orName, 5, "DVI%d", or);
        pNv->reg[(0x61C00C+off)/4] = 0x03010700;
        pNv->reg[(0x61C010+off)/4] = 0x0000152f;
        pNv->reg[(0x61C014+off)/4] = 0x00000000;
        pNv->reg[(0x61C018+off)/4] = 0x00245af8;
        funcs = &G80SorTMDSOutputFuncs;
    }

    output = xf86OutputCreate(pScrn, funcs, orName);

    pPriv->type = SOR;
    pPriv->or = or;
    pPriv->panelType = panelType;
    pPriv->cached_status = XF86OutputStatusUnknown;
    if(panelType == TMDS)
        pPriv->set_pclk = G80SorSetPClk;
    output->driver_private = pPriv;
    output->interlaceAllowed = TRUE;
    output->doubleScanAllowed = TRUE;

    return output;
}