/*
 * Copyright © 2007 Intel 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 (including the next
 * paragraph) 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.
 *
 * Authors:
 *    Eric Anholt <eric@anholt.net>
 *
 */

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

#include "xf86.h"
#include "i830.h"
#include "xf86Modes.h"
#include "i830_display.h"
#include "X11/Xatom.h"

struct i830_hdmi_priv {
    uint32_t output_reg;

    uint32_t save_SDVO;

    Bool has_hdmi_sink;
    /* Default 0 for full RGB range 0-255, 1 is for RGB range 16-235 */
    uint32_t broadcast_rgb;
};

static Atom broadcast_atom;

static int
i830_hdmi_mode_valid(xf86OutputPtr output, DisplayModePtr mode)
{
    if (mode->Clock > 165000)
	return MODE_CLOCK_HIGH;

    if (mode->Clock < 20000)
	return MODE_CLOCK_LOW;

    return MODE_OK;
}

static Bool
i830_hdmi_mode_fixup(xf86OutputPtr output, DisplayModePtr mode,
		     DisplayModePtr adjusted_mode)
{
    /* The HDMI output doesn't need the pixel multiplication that SDVO does,
     * so no fixup.
     */
    return TRUE;
}

static void
i830_hdmi_mode_set(xf86OutputPtr output, DisplayModePtr mode,
		   DisplayModePtr adjusted_mode)
{
    ScrnInfoPtr pScrn = output->scrn;
    I830OutputPrivatePtr intel_output = output->driver_private;
    struct i830_hdmi_priv *dev_priv = intel_output->dev_priv;
    I830Ptr pI830 = I830PTR(pScrn);
    xf86CrtcPtr crtc = output->crtc;
    I830CrtcPrivatePtr intel_crtc = crtc->driver_private;
    uint32_t sdvox;

    sdvox = SDVO_ENCODING_HDMI |
	SDVO_BORDER_ENABLE |
	SDVO_VSYNC_ACTIVE_HIGH |
	SDVO_HSYNC_ACTIVE_HIGH;

    if (dev_priv->has_hdmi_sink)
	    sdvox |= SDVO_AUDIO_ENABLE;

    if (intel_crtc->pipe == 1)
	sdvox |= SDVO_PIPE_B_SELECT;

    OUTREG(dev_priv->output_reg, sdvox);
    POSTING_READ(dev_priv->output_reg);
}

static void
i830_hdmi_dpms(xf86OutputPtr output, int mode)
{
    ScrnInfoPtr pScrn = output->scrn;
    I830OutputPrivatePtr intel_output = output->driver_private;
    struct i830_hdmi_priv *dev_priv = intel_output->dev_priv;
    I830Ptr pI830 = I830PTR(pScrn);
    uint32_t  temp;

    if (mode == DPMSModeOff) {
	temp = INREG(dev_priv->output_reg);
	OUTREG(dev_priv->output_reg, temp & ~SDVO_ENABLE);
    } else {
	temp = INREG(dev_priv->output_reg);
	OUTREG(dev_priv->output_reg, temp | SDVO_ENABLE);
    }
}

static void
i830_hdmi_save(xf86OutputPtr output)
{
    ScrnInfoPtr pScrn = output->scrn;
    I830OutputPrivatePtr intel_output = output->driver_private;
    struct i830_hdmi_priv *dev_priv = intel_output->dev_priv;
    I830Ptr pI830 = I830PTR(pScrn);

    dev_priv->save_SDVO = INREG(dev_priv->output_reg);
}

static void
i830_hdmi_restore(xf86OutputPtr output)
{
    ScrnInfoPtr pScrn = output->scrn;
    I830OutputPrivatePtr intel_output = output->driver_private;
    struct i830_hdmi_priv *dev_priv = intel_output->dev_priv;
    I830Ptr pI830 = I830PTR(pScrn);

    OUTREG(dev_priv->output_reg, dev_priv->save_SDVO);
}

/**
 * Uses CRT_HOTPLUG_EN and CRT_HOTPLUG_STAT to detect HDMI connection.
 *
 * \return TRUE if HDMI port is connected.
 * \return FALSE if HDMI port is disconnected.
 */
static xf86OutputStatus
i830_hdmi_detect(xf86OutputPtr output)
{
    ScrnInfoPtr	pScrn = output->scrn;
    I830OutputPrivatePtr intel_output = output->driver_private;
    struct i830_hdmi_priv *dev_priv = intel_output->dev_priv;
    I830Ptr pI830 = I830PTR(pScrn);
    uint32_t temp, bit;
    xf86OutputStatus status;
    xf86MonPtr edid_mon;

    dev_priv->has_hdmi_sink = FALSE;

    /* For G4X desktop chip, PEG_BAND_GAP_DATA 3:0 must first be written 0xd.
     * Failure to do so will result in spurious interrupts being
     * generated on the port when a cable is not attached.
     */
    if (IS_G4X(pI830) && !IS_GM45(pI830)) {
	temp = INREG(PEG_BAND_GAP_DATA);
	OUTREG(PEG_BAND_GAP_DATA, (temp & ~0xf) | 0xd);
    }

    temp = INREG(PORT_HOTPLUG_EN);

    switch (dev_priv->output_reg) {
    case SDVOB:
	temp |= HDMIB_HOTPLUG_INT_EN;
	break;
    case SDVOC:
	temp |= HDMIC_HOTPLUG_INT_EN;
	break;
    default:
	return XF86OutputStatusUnknown;
    }

    OUTREG(PORT_HOTPLUG_EN, temp);

    POSTING_READ(PORT_HOTPLUG_EN);

    i830WaitForVblank(pScrn);
    switch (dev_priv->output_reg) {
    case SDVOB:
	bit = HDMIB_HOTPLUG_INT_STATUS;
	break;
    case SDVOC:
	bit = HDMIC_HOTPLUG_INT_STATUS;
	break;
    default:
	return XF86OutputStatusUnknown;
    }

    if ((INREG(PORT_HOTPLUG_STAT) & bit) != 0)
	status = XF86OutputStatusConnected;
    else
	return XF86OutputStatusDisconnected;

    edid_mon = xf86OutputGetEDID (output, intel_output->pDDCBus);
    if (!edid_mon || !DIGITAL(edid_mon->features.input_type))
	status = XF86OutputStatusDisconnected;

    if (xf86LoaderCheckSymbol("xf86MonitorIsHDMI") &&
	    xf86MonitorIsHDMI(edid_mon))
	dev_priv->has_hdmi_sink = TRUE;

    if (pI830->debug_modes)
	xf86DrvMsg(pScrn->scrnIndex, X_INFO,
			"%s monitor detected on HDMI-%d\n",
			dev_priv->has_hdmi_sink ? "HDMI" : "DVI",
			(dev_priv->output_reg == SDVOB) ? 1 : 2);

    xfree(edid_mon);
    return status;
}

static void
i830_hdmi_destroy (xf86OutputPtr output)
{
    I830OutputPrivatePtr intel_output = output->driver_private;

    if (intel_output != NULL) {
	xf86DestroyI2CBusRec(intel_output->pDDCBus, FALSE, FALSE);
	xfree(intel_output);
    }
}

static void
i830_hdmi_create_resources(xf86OutputPtr output)
{
    ScrnInfoPtr                 pScrn = output->scrn;
    I830Ptr                     pI830 = I830PTR(pScrn);
    I830OutputPrivatePtr        intel_output = output->driver_private;
    struct i830_hdmi_priv       *dev_priv = intel_output->dev_priv;
    INT32			broadcast_range[2];
    int                         err;

    /* only R G B are 8bit color mode */
    if (pScrn->depth != 24 ||
        /* only 965G and G4X platform */
        !(IS_I965G(pI830) || IS_G4X(pI830)))
        return;

    broadcast_atom =
        MakeAtom("BROADCAST_RGB", sizeof("BROADCAST_RGB") - 1, TRUE);

    broadcast_range[0] = 0;
    broadcast_range[1] = 1;
    err = RRConfigureOutputProperty(output->randr_output,
                                    broadcast_atom,
                                    FALSE, TRUE, FALSE, 2, broadcast_range);
    if (err != 0) {
        xf86DrvMsg(pScrn->scrnIndex, X_ERROR,
                   "RRConfigureOutputProperty error, %d\n", err);
        return;
    }
    /* Set the current value of the broadcast property as full range */
    dev_priv->broadcast_rgb = 0;
    err = RRChangeOutputProperty(output->randr_output,
                                 broadcast_atom,
                                 XA_INTEGER, 32, PropModeReplace,
                                 1, &dev_priv->broadcast_rgb,
                                 FALSE, TRUE);
    if (err != 0) {
        xf86DrvMsg(pScrn->scrnIndex, X_ERROR,
                   "RRChangeOutputProperty error, %d\n", err);
        return;
    }
}

static Bool
i830_hdmi_set_property(xf86OutputPtr output, Atom property,
                       RRPropertyValuePtr value)
{
    ScrnInfoPtr             pScrn = output->scrn;
    I830Ptr                 pI830 = I830PTR(pScrn);
    I830OutputPrivatePtr    intel_output = output->driver_private;
    struct i830_hdmi_priv   *dev_priv = intel_output->dev_priv;
    uint32_t temp;

    if (property == broadcast_atom) {
        uint32_t val;

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

        val = *(INT32 *)value->data;
        if (val < 0 || val > 1)
        {
            return FALSE;
        }
        if (val == dev_priv->broadcast_rgb)
            return TRUE;

        temp = INREG(dev_priv->output_reg);

        if (val == 1)
            temp |= SDVO_COLOR_NOT_FULL_RANGE;
        else if (val == 0)
            temp &= ~SDVO_COLOR_NOT_FULL_RANGE;

        OUTREG(dev_priv->output_reg, temp);
        dev_priv->broadcast_rgb = val;
    }
    return TRUE;
}

static const xf86OutputFuncsRec i830_hdmi_output_funcs = {
    .create_resources = i830_hdmi_create_resources,
    .dpms = i830_hdmi_dpms,
    .save = i830_hdmi_save,
    .restore = i830_hdmi_restore,
    .mode_valid = i830_hdmi_mode_valid,
    .mode_fixup = i830_hdmi_mode_fixup,
    .prepare = i830_output_prepare,
    .mode_set = i830_hdmi_mode_set,
    .commit = i830_output_commit,
    .detect = i830_hdmi_detect,
    .get_modes = i830_ddc_get_modes,
    .set_property = i830_hdmi_set_property,
    .destroy = i830_hdmi_destroy
};

void
i830_hdmi_init(ScrnInfoPtr pScrn, int output_reg)
{
    xf86OutputPtr output;
    I830OutputPrivatePtr intel_output;
    struct i830_hdmi_priv *dev_priv;

    output = xf86OutputCreate(pScrn, &i830_hdmi_output_funcs,
			      (output_reg == SDVOB) ? "HDMI-1" : "HDMI-2");
    if (!output)
	return;
    intel_output = xnfcalloc(sizeof (I830OutputPrivateRec) +
			     sizeof (struct i830_hdmi_priv), 1);
    if (intel_output == NULL) {
	xf86OutputDestroy(output);
	return;
    }
    output->driver_private = intel_output;
    output->interlaceAllowed = FALSE;
    output->doubleScanAllowed = FALSE;

    dev_priv = (struct i830_hdmi_priv *)(intel_output + 1);
    dev_priv->output_reg = output_reg;
    dev_priv->has_hdmi_sink = FALSE;

    intel_output->dev_priv = dev_priv;
    intel_output->type = I830_OUTPUT_HDMI;
    intel_output->pipe_mask = ((1 << 0) | (1 << 1));
    intel_output->clone_mask = (1 << I830_OUTPUT_HDMI);

    /* Set up the DDC bus. */
    if (output_reg == SDVOB)
	I830I2CInit(pScrn, &intel_output->pDDCBus, GPIOE, "HDMIDDC_B");
    else
	I830I2CInit(pScrn, &intel_output->pDDCBus, GPIOD, "HDMIDDC_C");

    xf86DrvMsg(pScrn->scrnIndex, X_INFO,
	       "HDMI output %d detected\n",
	       (output_reg == SDVOB) ? 1 : 2);
}
