/*
 * SPDX-FileCopyrightText: Copyright (c) 2008-2019 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
 * SPDX-License-Identifier: MIT
 *
 * 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 "nvos.h"
#include "dp/nvdp-connector.h"
#include "nvkms-event.h"
#include "nvkms-rm.h"
#include "nvkms-types.h"
#include "nvkms-dpy.h"
#include "nvkms-rmapi.h"
#include "nvkms-utils.h"
#include "nvkms-private.h"
#include "nvkms-evo.h"
#include "nvkms-hdmi.h"

#include "ctrl/ctrla083.h"

/*
 * Handle a display device hotplug event.
 *
 * What "hotplug" means is unclear, but it could mean any of the following:
 *   - A display device is plugged in.
 *   - A display device is unlugged.
 *   - A display device was unplugged and then plugged back in.
 *   - A display device was plugged in and then unplugged.
 *   - An already connected display device is turned on.
 *   - An already connected display device is turned off.
 *   - A DisplayPort device needs its link status and RX Capabilities fields
 *     read and may need to be retrained ("long" hotplug event, > 2ms).
 *
 * DisplayPort "short" hotplug events, which are between 0.25ms and 2ms, are
 * handled separately by nvHandleDPIRQEventDeferredWork below.
 */

void
nvHandleHotplugEventDeferredWork(void *dataPtr, NvU32 dataU32)
{
    NVDispEvoPtr pDispEvo = dataPtr;
    NV0073_CTRL_SYSTEM_GET_HOTPLUG_UNPLUG_STATE_PARAMS hotplugParams = { 0 };
    NvU32 ret;
    NVDpyIdList hotplugged, unplugged, tmpUnplugged, changed;
    NVDpyIdList connectedDisplays;
    NVDpyEvoPtr pDpyEvo;
    NVDevEvoPtr pDevEvo = pDispEvo->pDevEvo;

    if (!pDevEvo->displaylessHw) {
        // Get the hotplug state.
        if ((ret = nvRmApiControl(
                    nvEvoGlobal.clientHandle,
                    pDevEvo->displayCommonHandle,
                    NV0073_CTRL_CMD_SYSTEM_GET_HOTPLUG_UNPLUG_STATE,
                    &hotplugParams,
                    sizeof(hotplugParams)))
                != NVOS_STATUS_SUCCESS) {

            nvEvoLogDisp(pDispEvo, EVO_LOG_WARN, "Failed to determine which "
                         "devices were hotplugged: 0x%x\n", ret);
            return;
        }

        /*
         * Work around an RM bug in hotplug notification when the GPU is in
         * GC6.  In this case, the RM will notify us of a hotplug event, but
         * NV0073_CTRL_CMD_SYSTEM_GET_HOTPLUG_UNPLUG_STATE returns both
         * hotPlugMask and hotUnplugMask as 0.
         * Bug 200528641 tracks finding a root cause.  Until that bug is
         * fixed, call NV0073_CTRL_CMD_SYSTEM_GET_CONNECT_STATE to get the
         * full list of connected dpys and construct hotplugged and
         * unplugged lists from that if we encounter this case.
         */
        if ((hotplugParams.hotPlugMask == 0) &&
            (hotplugParams.hotUnplugMask == 0)) {
            const NVDpyIdList updatedDisplayList = nvRmGetConnectedDpys(pDispEvo,
                                                      pDispEvo->connectorIds);
            hotplugged = nvDpyIdListMinusDpyIdList(updatedDisplayList,
                                                   pDispEvo->connectedDisplays);
            unplugged = nvDpyIdListMinusDpyIdList(pDispEvo->connectedDisplays,
                                                  updatedDisplayList);
        } else {
            hotplugged = nvNvU32ToDpyIdList(hotplugParams.hotPlugMask);
            unplugged = nvNvU32ToDpyIdList(hotplugParams.hotUnplugMask);
        }
    } else {
        NVA083_CTRL_VIRTUAL_DISPLAY_GET_MAX_RESOLUTION_PARAMS getMaxResolutionParams = { 0 };
        const NVDpyIdList updatedDisplayList = nvRmGetConnectedDpys(pDispEvo,
                                                  pDispEvo->connectorIds);

        /*
         * Update connected displays
         *
         * XXX We don't know which dpys actually changed, so treat all connected
         * dpys as hotplugged.
         */
        hotplugged = updatedDisplayList;
        unplugged  = nvDpyIdListMinusDpyIdList(pDispEvo->connectedDisplays,
                                               updatedDisplayList);

        // Update max resolution
        if ((ret = nvRmApiControl(nvEvoGlobal.clientHandle,
                    pDevEvo->displaylessHandle,
                    NVA083_CTRL_CMD_VIRTUAL_DISPLAY_GET_MAX_RESOLUTION,
                    &getMaxResolutionParams,
                    sizeof(getMaxResolutionParams)))
                != NVOS_STATUS_SUCCESS) {

           nvEvoLogDev(pDevEvo, EVO_LOG_ERROR,
                       "Failed to get the max resolution for displayless HW,"
                       "error %d", ret);
        } else {
            pDevEvo->caps.maxWidthInBytes = getMaxResolutionParams.maxHResolution * (32 >> 3);
            pDevEvo->caps.maxWidthInPixels = getMaxResolutionParams.maxHResolution;
            pDevEvo->caps.maxHeight = getMaxResolutionParams.maxVResolution;
        }
    }

    // The RM only reports the latest plug/unplug status of each dpy.
    nvAssert(nvDpyIdListIsEmpty(nvIntersectDpyIdListAndDpyIdList(hotplugged,
                                                                 unplugged)));
    nvAssert(nvDpyIdListIsASubSetofDpyIdList(hotplugged,
                                             pDispEvo->connectorIds));
    nvAssert(nvDpyIdListIsASubSetofDpyIdList(unplugged,
                                             pDispEvo->connectorIds));

    connectedDisplays = pDispEvo->connectedDisplays;

    // Ignore non-DP devices that were reported as unplugged while already
    // disconnected.
    tmpUnplugged = nvEmptyDpyIdList();
    FOR_ALL_EVO_DPYS(pDpyEvo, unplugged, pDispEvo) {
        NVConnectorEvoPtr pConnectorEvo = pDpyEvo->pConnectorEvo;

        if (nvConnectorUsesDPLib(pConnectorEvo) ||
            nvDpyIdIsInDpyIdList(pConnectorEvo->displayId, connectedDisplays)) {

            tmpUnplugged =
                nvAddDpyIdToDpyIdList(pConnectorEvo->displayId, tmpUnplugged);
        }
    }
    unplugged = tmpUnplugged;

    // Non-DP devices that were disconnected and connected again should generate an
    // unplug / plug pair.
    FOR_ALL_EVO_DPYS(pDpyEvo, hotplugged, pDispEvo) {
        NVConnectorEvoPtr pConnectorEvo = pDpyEvo->pConnectorEvo;

        if (!nvConnectorUsesDPLib(pConnectorEvo) &&
            nvDpyIdIsInDpyIdList(pConnectorEvo->displayId, connectedDisplays)) {

            unplugged = nvAddDpyIdToDpyIdList(pConnectorEvo->displayId, unplugged);
        }
    }

#if defined(DEBUG)
    if (!nvDpyIdListIsEmpty(hotplugged)) {
        char *str = nvGetDpyIdListStringEvo(pDispEvo, hotplugged);
        nvEvoLogDispDebug(pDispEvo, EVO_LOG_INFO,
                          "Received display hotplug event: %s",
                          nvSafeString(str, "unknown"));
        nvFree(str);
    }
    if (!nvDpyIdListIsEmpty(unplugged)) {
        char *str = nvGetDpyIdListStringEvo(pDispEvo, unplugged);
        nvEvoLogDispDebug(pDispEvo, EVO_LOG_INFO,
                          "Received display unplug event: %s",
                          nvSafeString(str, "unknown"));
        nvFree(str);
    }
#endif /* DEBUG */

    // First, the OR configuration of the connector should not change, but
    // re-query it to make sure.
    changed = nvAddDpyIdListToDpyIdList(hotplugged, unplugged);
    FOR_ALL_EVO_DPYS(pDpyEvo, changed, pDispEvo) {
        nvRmGetConnectorORInfo(pDpyEvo->pConnectorEvo, TRUE);
    }

    // Next, disconnect devices that are in the unplug mask.
    FOR_ALL_EVO_DPYS(pDpyEvo, unplugged, pDispEvo) {
        NVConnectorEvoPtr pConnectorEvo = pDpyEvo->pConnectorEvo;

        if (nvConnectorUsesDPLib(pConnectorEvo)) {
            nvDPNotifyLongPulse(pConnectorEvo, FALSE);
        } else {
            nvSendDpyEventEvo(pDpyEvo, NVKMS_EVENT_TYPE_DPY_CHANGED);
        }
    }

    // Finally, connect devices that are in the plug mask.
    FOR_ALL_EVO_DPYS(pDpyEvo, hotplugged, pDispEvo) {
        NVConnectorEvoPtr pConnectorEvo = pDpyEvo->pConnectorEvo;

        if (nvConnectorUsesDPLib(pConnectorEvo)) {
            nvDPNotifyLongPulse(pConnectorEvo, TRUE);
        } else {
            nvSendDpyEventEvo(pDpyEvo, NVKMS_EVENT_TYPE_DPY_CHANGED);
        }
        
        pDpyEvo->hotplugged = TRUE;
    }
}

void
nvHandleDPIRQEventDeferredWork(void *dataPtr, NvU32 dataU32)
{
    NVDispEvoPtr pDispEvo = dataPtr;
    NVDpyId id = nvNvU32ToDpyId(dataU32);
    NVConnectorEvoPtr pConnectorEvo = nvGetConnectorFromDisp(pDispEvo, id);

    // Notify event-related DP connector. For DP Serializer connector, HPD_IRQ
    // indicates loss of clock/sync, so re-train the link.
    if (nvConnectorUsesDPLib(pConnectorEvo)) {
        nvDPNotifyShortPulse(pConnectorEvo->pDpLibConnector);
    } else if (nvConnectorIsDPSerializer(pConnectorEvo)) {
        nvDPSerializerHandleDPIRQ(pDispEvo, pConnectorEvo);
    }
}

void
nvHandleHDMIFRLRetrainEventDeferredWork(void *dataPtr, NvU32 dataU32)
{
    NVDispEvoPtr pDispEvo = dataPtr;
    NVDpyId id = nvNvU32ToDpyId(dataU32);
    NVDpyEvoRec *pDpyEvo = nvGetDpyEvoFromDispEvo(pDispEvo, id);

    nvSendDpyEventEvo(pDpyEvo, NVKMS_EVENT_TYPE_DPY_CHANGED);
    pDpyEvo->hdmi.reassessFrlLinkCaps = TRUE;
}
