/*
 * Copyright (c) 2015, NVIDIA CORPORATION. All rights reserved.
 *
 * 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.
 */

#include "nvidia-drm-conftest.h" /* NV_DRM_ATOMIC_MODESET_AVAILABLE */

#if defined(NV_DRM_ATOMIC_MODESET_AVAILABLE)

#include "nvidia-drm-priv.h"
#include "nvidia-drm-encoder.h"
#include "nvidia-drm-utils.h"
#include "nvidia-drm-connector.h"
#include "nvidia-drm-crtc.h"

#include <drm/drm_crtc_helper.h>

#include <drm/drm_atomic.h>
#include <drm/drm_atomic_helper.h>
#include <linux/version.h>

static void nv_drm_encoder_destroy(struct drm_encoder *encoder)
{
    struct nv_drm_encoder *nv_encoder = to_nv_encoder(encoder);

    drm_encoder_cleanup(encoder);

    nv_drm_free(nv_encoder);
}

static const struct drm_encoder_funcs nv_encoder_funcs = {
    .destroy = nv_drm_encoder_destroy,
};

static bool nv_drm_encoder_mode_fixup(struct drm_encoder *encoder,
                                      const struct drm_display_mode *mode,
                                      struct drm_display_mode *adjusted_mode)
{
    return true;
}

static void nv_drm_encoder_prepare(struct drm_encoder *encoder)
{

}

static void nv_drm_encoder_commit(struct drm_encoder *encoder)
{

}

static void nv_drm_encoder_mode_set(struct drm_encoder *encoder,
                                    struct drm_display_mode *mode,
                                    struct drm_display_mode *adjusted_mode)
{

}

static const struct drm_encoder_helper_funcs nv_encoder_helper_funcs = {
    .mode_fixup = nv_drm_encoder_mode_fixup,
    .prepare    = nv_drm_encoder_prepare,
    .commit     = nv_drm_encoder_commit,
    .mode_set   = nv_drm_encoder_mode_set,
};

static uint32_t get_crtc_mask(struct drm_device *dev, uint32_t headMask)
{
    struct drm_crtc *crtc = NULL;
    uint32_t crtc_mask = 0x0;

    list_for_each_entry(crtc, &dev->mode_config.crtc_list, head) {
        struct nv_drm_crtc *nv_crtc = to_nv_crtc(crtc);

        if (headMask & BIT(nv_crtc->head)) {
            crtc_mask |= drm_crtc_mask(crtc);
        }
    }

    return crtc_mask;
}

/*
 * Helper function to create new encoder for given NvKmsKapiDisplay
 * with given signal format.
 */
static struct drm_encoder*
nv_drm_encoder_new(struct drm_device *dev,
                   NvKmsKapiDisplay hDisplay,
                   NvKmsConnectorSignalFormat format,
                   unsigned int crtc_mask)
{
    struct nv_drm_device *nv_dev = to_nv_device(dev);

    struct nv_drm_encoder *nv_encoder = NULL;

    int ret = 0;

    /* Allocate an NVIDIA encoder object */

    nv_encoder = nv_drm_calloc(1, sizeof(*nv_encoder));

    if (nv_encoder == NULL) {
        NV_DRM_DEV_LOG_ERR(
            nv_dev,
            "Failed to allocate memory for NVIDIA-DRM encoder object");
        return ERR_PTR(-ENOMEM);
    }

    nv_encoder->hDisplay = hDisplay;

    /* Initialize the base encoder object and add it to the drm subsystem */

    ret = drm_encoder_init(dev,
                           &nv_encoder->base, &nv_encoder_funcs,
                           nvkms_connector_signal_to_drm_encoder_signal(format)
#if defined(NV_DRM_ENCODER_INIT_HAS_NAME_ARG)
                           , NULL
#endif
                           );

    if (ret != 0) {
        nv_drm_free(nv_encoder);

        NV_DRM_DEV_LOG_ERR(
            nv_dev,
            "Failed to initialize encoder created from NvKmsKapiDisplay 0x%08x",
            hDisplay);
        return ERR_PTR(ret);
    }

    nv_encoder->base.possible_crtcs = crtc_mask;

    drm_encoder_helper_add(&nv_encoder->base, &nv_encoder_helper_funcs);

    return &nv_encoder->base;
}

/*
 * Add encoder for given NvKmsKapiDisplay
 */
struct drm_encoder*
nv_drm_add_encoder(struct drm_device *dev, NvKmsKapiDisplay hDisplay)
{
    struct nv_drm_device *nv_dev = to_nv_device(dev);

    struct NvKmsKapiStaticDisplayInfo *displayInfo = NULL;
    struct NvKmsKapiConnectorInfo *connectorInfo = NULL;

    struct drm_encoder *encoder = NULL;
    struct nv_drm_encoder *nv_encoder = NULL;

    struct drm_connector *connector = NULL;

    int ret = 0;

    /* Query NvKmsKapiStaticDisplayInfo and NvKmsKapiConnectorInfo */

    if ((displayInfo = nv_drm_calloc(1, sizeof(*displayInfo))) == NULL) {
        ret = -ENOMEM;
        goto done;
    }

    if (!nvKms->getStaticDisplayInfo(nv_dev->pDevice, hDisplay, displayInfo)) {
        ret = -EINVAL;
        goto done;
    }

    connectorInfo = nvkms_get_connector_info(nv_dev->pDevice,
                                             displayInfo->connectorHandle);

    if (IS_ERR(connectorInfo)) {
        ret = PTR_ERR(connectorInfo);
        goto done;
    }

    /* Create and add drm encoder */

    encoder = nv_drm_encoder_new(dev,
                                 displayInfo->handle,
                                 connectorInfo->signalFormat,
                                 get_crtc_mask(dev, connectorInfo->headMask));

    if (IS_ERR(encoder)) {
        ret = PTR_ERR(encoder);
        goto done;
    }

    /* Get connector from repective physical index */

    connector =
        nv_drm_get_connector(dev,
                                 connectorInfo->physicalIndex,
                                 connectorInfo->type,
                                 displayInfo->internal, displayInfo->dpAddress);

    if (IS_ERR(connector)) {
        ret = PTR_ERR(connector);
        goto failed_connector_encoder_attach;
    }

    /* Attach encoder and connector */

#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 19, 0)
    ret = drm_mode_connector_attach_encoder(connector, encoder);
#else
    ret = drm_connector_attach_encoder(connector, encoder);
#endif

    if (ret != 0) {
        NV_DRM_DEV_LOG_ERR(
            nv_dev,
            "Failed to attach encoder created from NvKmsKapiDisplay 0x%08x "
            "to connector",
            hDisplay);
        goto failed_connector_encoder_attach;
    }

    nv_encoder = to_nv_encoder(encoder);

    mutex_lock(&dev->mode_config.mutex);

    nv_encoder->nv_connector = to_nv_connector(connector);

    nv_drm_connector_mark_connection_status_dirty(nv_encoder->nv_connector);

    mutex_unlock(&dev->mode_config.mutex);

    goto done;

failed_connector_encoder_attach:

    drm_encoder_cleanup(encoder);

    nv_drm_free(encoder);

done:

    nv_drm_free(displayInfo);

    nv_drm_free(connectorInfo);

    return ret != 0 ? ERR_PTR(ret) : encoder;
}

static inline struct nv_drm_encoder*
get_nv_encoder_from_nvkms_display(struct drm_device *dev,
                                  NvKmsKapiDisplay hDisplay)
{
    struct drm_encoder *encoder;

    list_for_each_entry(encoder, &dev->mode_config.encoder_list, head) {
        struct nv_drm_encoder *nv_encoder = to_nv_encoder(encoder);

        if (nv_encoder->hDisplay == hDisplay) {
            return nv_encoder;
        }
    }

    return NULL;
}

void nv_drm_handle_display_change(struct nv_drm_device *nv_dev,
                                  NvKmsKapiDisplay hDisplay)
{
    struct drm_device *dev = nv_dev->dev;
    struct nv_drm_encoder *nv_encoder = NULL;

    mutex_lock(&dev->mode_config.mutex);

    nv_encoder = get_nv_encoder_from_nvkms_display(dev, hDisplay);

    mutex_unlock(&dev->mode_config.mutex);

    if (nv_encoder == NULL) {
        return;
    }

    nv_drm_connector_mark_connection_status_dirty(nv_encoder->nv_connector);

    drm_kms_helper_hotplug_event(dev);
}

void nv_drm_handle_dynamic_display_connected(struct nv_drm_device *nv_dev,
                                             NvKmsKapiDisplay hDisplay)
{
    struct drm_device *dev = nv_dev->dev;

    struct drm_encoder *encoder = NULL;
    struct nv_drm_encoder *nv_encoder = NULL;

    /*
     * Look for an existing encoder with the same hDisplay and
     * use it if available.
     */

    nv_encoder = get_nv_encoder_from_nvkms_display(dev, hDisplay);

    if (nv_encoder != NULL) {
        NV_DRM_DEV_LOG_ERR(
            nv_dev,
            "Encoder with NvKmsKapiDisplay 0x%08x already exists.",
            hDisplay);
        return;
    }

    encoder = nv_drm_add_encoder(dev, hDisplay);

    if (IS_ERR(encoder)) {
        NV_DRM_DEV_LOG_ERR(
            nv_dev,
            "Failed to add encoder for NvKmsKapiDisplay 0x%08x",
            hDisplay);
        return;
    }

    /*
     * On some kernels, DRM has the notion of a "primary group" that
     * tracks the global mode setting state for the device.
     *
     * On kernels where DRM has a primary group, we need to reinitialize
     * after adding encoders and connectors.
     */
#if defined(NV_DRM_REINIT_PRIMARY_MODE_GROUP_PRESENT)
    drm_reinit_primary_mode_group(dev);
#endif

    drm_kms_helper_hotplug_event(dev);
}
#endif
