// Copyright (C) 2014 John Layt <jlayt@kde.org>
// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qwindowsprintdevice_p.h"

#include <QtCore/qdebug.h>

#ifndef DC_COLLATE
#  define DC_COLLATE 22
#endif

QT_BEGIN_NAMESPACE

using WindowsPrinterLookup = QList<QWindowsPrinterInfo>;
Q_GLOBAL_STATIC(WindowsPrinterLookup, windowsDeviceLookup);

extern qreal qt_pointMultiplier(QPageLayout::Unit unit);

static inline uint qwcsnlen(const wchar_t *str, uint maxlen)
{
    uint length = 0;
    if (str) {
        while (length < maxlen && *str++)
            length++;
    }
    return length;
}

static LPDEVMODE getDevmode(HANDLE hPrinter, const QString &printerId)
{
    LPWSTR printerIdUtf16 = const_cast<LPWSTR>(reinterpret_cast<LPCWSTR>(printerId.utf16()));
    // Allocate the required DEVMODE buffer
    LONG dmSize = DocumentProperties(nullptr, hPrinter, printerIdUtf16, nullptr, nullptr, 0);
    if (dmSize <= 0)
        return nullptr;
    LPDEVMODE pDevMode = reinterpret_cast<LPDEVMODE>(malloc(dmSize));
     // Get the default DevMode
    LONG result = DocumentProperties(nullptr, hPrinter, printerIdUtf16, pDevMode, nullptr, DM_OUT_BUFFER);
    if (result != IDOK) {
        free(pDevMode);
        pDevMode = nullptr;
    }
    return pDevMode;
}

QWindowsPrintDevice::QWindowsPrintDevice()
    : QPlatformPrintDevice(),
      m_hPrinter(0)
{
}

QWindowsPrintDevice::QWindowsPrintDevice(const QString &id)
    : QPlatformPrintDevice(id),
      m_hPrinter(0)
{
    // First do a fast lookup to see if printer exists, if it does then open it
    if (!id.isEmpty() && QWindowsPrintDevice::availablePrintDeviceIds().contains(id)) {
        if (OpenPrinter(const_cast<LPWSTR>(wcharId()), &m_hPrinter, nullptr)) {
            DWORD needed = 0;
            GetPrinter(m_hPrinter, 2, 0, 0, &needed);
            QScopedArrayPointer<BYTE> buffer(new BYTE[needed]);
            if (GetPrinter(m_hPrinter, 2, buffer.data(), needed, &needed)) {
                PPRINTER_INFO_2 info = reinterpret_cast<PPRINTER_INFO_2>(buffer.data());
                m_name = QString::fromWCharArray(info->pPrinterName);
                m_location = QString::fromWCharArray(info->pLocation);
                m_makeAndModel = QString::fromWCharArray(info->pDriverName); // TODO Check is not available elsewhere
                m_isRemote = info->Attributes & PRINTER_ATTRIBUTE_NETWORK;
            }
            QWindowsPrinterInfo m_info;
            m_info.m_id = m_id;
            m_info.m_name = m_name;
            m_info.m_location = m_location;
            m_info.m_makeAndModel = m_makeAndModel;
            m_info.m_isRemote = m_isRemote;
            m_infoIndex = windowsDeviceLookup()->indexOf(m_info);
            if (m_infoIndex != -1) {
                m_info = windowsDeviceLookup()->at(m_infoIndex);
                m_havePageSizes = m_info.m_havePageSizes;
                m_pageSizes = m_info.m_pageSizes;
                m_haveResolutions = m_info.m_haveResolutions;
                m_resolutions = m_info.m_resolutions;
                m_haveCopies = m_info.m_haveCopies;
                m_supportsMultipleCopies = m_info.m_supportsMultipleCopies;
                m_supportsCollateCopies = m_info.m_supportsCollateCopies;
                m_haveMinMaxPageSizes = m_info.m_haveMinMaxPageSizes;
                m_minimumPhysicalPageSize = m_info.m_minimumPhysicalPageSize;
                m_maximumPhysicalPageSize = m_info.m_maximumPhysicalPageSize;
                m_supportsCustomPageSizes = m_info.m_supportsCustomPageSizes;
                m_haveInputSlots = m_info.m_haveInputSlots;
                m_inputSlots = m_info.m_inputSlots;
                m_haveOutputBins = m_info.m_haveOutputBins;
                m_outputBins = m_info.m_outputBins;
                m_haveDuplexModes = m_info.m_haveDuplexModes;
                m_duplexModes = m_info.m_duplexModes;
                m_haveColorModes = m_info.m_haveColorModes;
                m_colorModes = m_info.m_colorModes;
                m_infoIndex = windowsDeviceLookup()->indexOf(m_info);
            } else {
                windowsDeviceLookup()->append(m_info);
                m_infoIndex = windowsDeviceLookup()->count() - 1;
            }
        }
    }
}

QWindowsPrintDevice::~QWindowsPrintDevice()
{
    ClosePrinter(m_hPrinter);
}

bool QWindowsPrintDevice::isValid() const
{
    return m_hPrinter;
}

bool QWindowsPrintDevice::isDefault() const
{
    return m_id == defaultPrintDeviceId();
}

QPrint::DeviceState QWindowsPrintDevice::state() const
{
    DWORD needed = 0;
    GetPrinter(m_hPrinter, 6, 0, 0, &needed);
    QScopedArrayPointer<BYTE> buffer(new BYTE[needed]);

    if (GetPrinter(m_hPrinter, 6, buffer.data(), needed, &needed)) {
        PPRINTER_INFO_6 info = reinterpret_cast<PPRINTER_INFO_6>(buffer.data());
        // TODO Check mapping
        if (info->dwStatus == 0
            || (info->dwStatus & PRINTER_STATUS_WAITING) == PRINTER_STATUS_WAITING
            || (info->dwStatus & PRINTER_STATUS_POWER_SAVE) == PRINTER_STATUS_POWER_SAVE) {
            return QPrint::Idle;
        } else if ((info->dwStatus & PRINTER_STATUS_PRINTING) == PRINTER_STATUS_PRINTING
                   || (info->dwStatus & PRINTER_STATUS_BUSY) == PRINTER_STATUS_BUSY
                   || (info->dwStatus & PRINTER_STATUS_INITIALIZING) == PRINTER_STATUS_INITIALIZING
                   || (info->dwStatus & PRINTER_STATUS_IO_ACTIVE) == PRINTER_STATUS_IO_ACTIVE
                   || (info->dwStatus & PRINTER_STATUS_PROCESSING) == PRINTER_STATUS_PROCESSING
                   || (info->dwStatus & PRINTER_STATUS_WARMING_UP) == PRINTER_STATUS_WARMING_UP) {
            return QPrint::Active;
        }
    }

    return QPrint::Error;
}

void QWindowsPrintDevice::loadPageSizes() const
{
    // Get the number of paper sizes and check all 3 attributes have same count
    const int paperCount = DeviceCapabilities(wcharId(), nullptr, DC_PAPERNAMES, nullptr, nullptr);
    if (paperCount > 0
        && DeviceCapabilities(wcharId(), nullptr, DC_PAPERSIZE, nullptr, nullptr) == paperCount
        && DeviceCapabilities(wcharId(), nullptr, DC_PAPERS, nullptr, nullptr) == paperCount) {

        QScopedArrayPointer<wchar_t> paperNames(new wchar_t[paperCount*64]);
        QScopedArrayPointer<POINT> winSizes(new POINT[paperCount]);
        QScopedArrayPointer<wchar_t> papers(new wchar_t[paperCount]);

        // Get the details and match the default paper size
        if (DeviceCapabilities(wcharId(), nullptr, DC_PAPERNAMES, paperNames.data(), nullptr) == paperCount
            && DeviceCapabilities(wcharId(), nullptr, DC_PAPERSIZE,
                                  reinterpret_cast<wchar_t *>(winSizes.data()), nullptr) == paperCount
            && DeviceCapabilities(wcharId(), nullptr, DC_PAPERS, papers.data(), nullptr) == paperCount) {

            // Returned size is in tenths of a millimeter
            const qreal multiplier = qt_pointMultiplier(QPageLayout::Millimeter);
            for (int i = 0; i < int(paperCount); ++i) {
                QSize size = QSize(qRound((winSizes[i].x / 10.0) * multiplier), qRound((winSizes[i].y / 10.0) * multiplier));
                wchar_t *paper = paperNames.data() + (i * 64);
                QString name = QString::fromWCharArray(paper, qwcsnlen(paper, 64));
                m_pageSizes.append(createPageSize(papers[i], size, name));
            }

        }
    }

    m_havePageSizes = true;
    QWindowsPrinterInfo *info = windowsDeviceLookup()->data();
    info[m_infoIndex].m_havePageSizes = true;
    info[m_infoIndex].m_pageSizes = m_pageSizes;
}

QPageSize QWindowsPrintDevice::defaultPageSize() const
{
    if (!m_havePageSizes)
        loadPageSizes();

    QPageSize pageSize;

    if (LPDEVMODE pDevMode = getDevmode(m_hPrinter, m_id)) {
        // Get the default paper size
        if (pDevMode->dmFields & DM_PAPERSIZE) {
            // Find the supported page size that matches, in theory default should be one of them
            for (const QPageSize &ps : m_pageSizes) {
                if (ps.windowsId() == pDevMode->dmPaperSize) {
                    pageSize = ps;
                    break;
                }
            }
        }
        // Clean-up
        free(pDevMode);
    }

    return pageSize;
}

QMarginsF QWindowsPrintDevice::printableMargins(const QPageSize &pageSize,
                                                QPageLayout::Orientation orientation,
                                                int resolution) const
{
    // TODO This is slow, need to cache values or find better way!
    // Modify the DevMode to get the DC printable margins in device pixels
    QMarginsF margins = QMarginsF(0, 0, 0, 0);
    DWORD needed = 0;
    GetPrinter(m_hPrinter, 2, 0, 0, &needed);
    QScopedArrayPointer<BYTE> buffer(new BYTE[needed]);
    if (GetPrinter(m_hPrinter, 2, buffer.data(), needed, &needed)) {
        PPRINTER_INFO_2 info = reinterpret_cast<PPRINTER_INFO_2>(buffer.data());
        LPDEVMODE devMode = info->pDevMode;
        bool separateDevMode = false;
        if (!devMode) {
            // GetPrinter() didn't include the DEVMODE. Get it a different way.
            devMode = getDevmode(m_hPrinter, m_id);
            if (!devMode)
                return margins;
            separateDevMode = true;
        }

        HDC pDC = CreateDC(nullptr, wcharId(), nullptr, devMode);
        if (pageSize.id() == QPageSize::Custom || pageSize.windowsId() <= 0 || pageSize.windowsId() > DMPAPER_LAST) {
            devMode->dmPaperSize =  0;
            devMode->dmPaperWidth = pageSize.size(QPageSize::Millimeter).width() * 10.0;
            devMode->dmPaperLength = pageSize.size(QPageSize::Millimeter).height() * 10.0;
        } else {
            devMode->dmPaperSize =  pageSize.windowsId();
        }
        devMode->dmPrintQuality = resolution;
        devMode->dmOrientation = orientation == QPageLayout::Portrait ? DMORIENT_PORTRAIT : DMORIENT_LANDSCAPE;
        ResetDC(pDC, devMode);
        const int dpiWidth = GetDeviceCaps(pDC, LOGPIXELSX);
        const int dpiHeight = GetDeviceCaps(pDC, LOGPIXELSY);
        const qreal wMult = 72.0 / dpiWidth;
        const qreal hMult = 72.0 / dpiHeight;
        const qreal physicalWidth = GetDeviceCaps(pDC, PHYSICALWIDTH) * wMult;
        const qreal physicalHeight = GetDeviceCaps(pDC, PHYSICALHEIGHT) * hMult;
        const qreal printableWidth = GetDeviceCaps(pDC, HORZRES) * wMult;
        const qreal printableHeight = GetDeviceCaps(pDC, VERTRES) * hMult;
        const qreal leftMargin = GetDeviceCaps(pDC, PHYSICALOFFSETX)* wMult;
        const qreal topMargin = GetDeviceCaps(pDC, PHYSICALOFFSETY) * hMult;
        const qreal rightMargin = physicalWidth - leftMargin - printableWidth;
        const qreal bottomMargin = physicalHeight - topMargin - printableHeight;
        margins = QMarginsF(leftMargin, topMargin, rightMargin, bottomMargin);
        if (separateDevMode)
            free(devMode);
        DeleteDC(pDC);
    }
    return margins;
}

void QWindowsPrintDevice::loadResolutions() const
{
    const int resCount = DeviceCapabilities(wcharId(), nullptr, DC_ENUMRESOLUTIONS, nullptr, nullptr);
    if (resCount > 0) {
        QScopedArrayPointer<LONG> resolutions(new LONG[resCount*2]);
        // Get the details and match the default paper size
        if (DeviceCapabilities(wcharId(), nullptr, DC_ENUMRESOLUTIONS,
                               reinterpret_cast<LPWSTR>(resolutions.data()), nullptr) == resCount) {
            for (int i = 0; i < int(resCount * 2); i += 2)
                m_resolutions.append(resolutions[i+1]);
        }
    }
    m_haveResolutions = true;
    QWindowsPrinterInfo *info = windowsDeviceLookup()->data();
    info[m_infoIndex].m_haveResolutions = true;
    info[m_infoIndex].m_resolutions = m_resolutions;
}

int QWindowsPrintDevice::defaultResolution() const
{
    int resolution = 72;  // TODO Set a sensible default?

    if (LPDEVMODE pDevMode = getDevmode(m_hPrinter, m_id)) {
        // Get the default resolution
        if (pDevMode->dmFields & DM_YRESOLUTION) {
            if (pDevMode->dmPrintQuality > 0)
                resolution = pDevMode->dmPrintQuality;
            else
                resolution = pDevMode->dmYResolution;
        }
        // Clean-up
        free(pDevMode);
    }
    return resolution;
}

void QWindowsPrintDevice::loadInputSlots() const
{
    const auto printerId = wcharId();
    const int binCount = DeviceCapabilities(printerId, nullptr, DC_BINS, nullptr, nullptr);
    if (binCount > 0
        && DeviceCapabilities(printerId, nullptr, DC_BINNAMES, nullptr, nullptr) == binCount) {

        QScopedArrayPointer<WORD> bins(new WORD[binCount]);
        QScopedArrayPointer<wchar_t> binNames(new wchar_t[binCount*24]);

        // Get the details and match the default paper size
        if (DeviceCapabilities(printerId, nullptr, DC_BINS,
                               reinterpret_cast<LPWSTR>(bins.data()), nullptr) == binCount
            && DeviceCapabilities(printerId, nullptr, DC_BINNAMES, binNames.data(),
                                  nullptr) == binCount) {

            for (int i = 0; i < int(binCount); ++i) {
                wchar_t *binName = binNames.data() + (i * 24);
                QString name = QString::fromWCharArray(binName, qwcsnlen(binName, 24));
                m_inputSlots.append(QPrintUtils::paperBinToInputSlot(bins[i], name));
            }

        }
    }

    m_haveInputSlots = true;
    QWindowsPrinterInfo *info = windowsDeviceLookup()->data();
    info[m_infoIndex].m_haveInputSlots = true;
    info[m_infoIndex].m_inputSlots = m_inputSlots;
}

QPrint::InputSlot QWindowsPrintDevice::defaultInputSlot() const
{
    QPrint::InputSlot inputSlot = QPlatformPrintDevice::defaultInputSlot();;

    if (LPDEVMODE pDevMode = getDevmode(m_hPrinter, m_id)) {
        // Get the default input slot
        if (pDevMode->dmFields & DM_DEFAULTSOURCE) {
            QPrint::InputSlot tempSlot =
                    QPrintUtils::paperBinToInputSlot(pDevMode->dmDefaultSource, QString());
            const auto inputSlots = supportedInputSlots();
            for (const QPrint::InputSlot &slot : inputSlots) {
                if (slot.key == tempSlot.key) {
                    inputSlot = slot;
                    break;
                }
            }
        }
        // Clean-up
        free(pDevMode);
    }
    return inputSlot;
}

void QWindowsPrintDevice::loadOutputBins() const
{
    m_outputBins.append(QPlatformPrintDevice::defaultOutputBin());
    m_haveOutputBins = true;
    QWindowsPrinterInfo *info = windowsDeviceLookup()->data();
    info[m_infoIndex].m_haveOutputBins = true;
    info[m_infoIndex].m_outputBins = m_outputBins;
}

void QWindowsPrintDevice::loadDuplexModes() const
{
    m_duplexModes.append(QPrint::DuplexNone);
    DWORD duplex = DeviceCapabilities(wcharId(), nullptr, DC_DUPLEX, nullptr, nullptr);
    if (int(duplex) == 1) {
        // TODO Assume if duplex flag supports both modes
        m_duplexModes.append(QPrint::DuplexAuto);
        m_duplexModes.append(QPrint::DuplexLongSide);
        m_duplexModes.append(QPrint::DuplexShortSide);
    }
    m_haveDuplexModes = true;
    QWindowsPrinterInfo *info = windowsDeviceLookup()->data();
    info[m_infoIndex].m_haveDuplexModes = true;
    info[m_infoIndex].m_duplexModes = m_duplexModes;
}

QPrint::DuplexMode QWindowsPrintDevice::defaultDuplexMode() const
{
    QPrint::DuplexMode duplexMode = QPrint::DuplexNone;

    if (LPDEVMODE pDevMode = getDevmode(m_hPrinter, m_id)) {
        // Get the default duplex mode
        if (pDevMode->dmFields & DM_DUPLEX) {
            if (pDevMode->dmDuplex == DMDUP_VERTICAL)
                duplexMode = QPrint::DuplexLongSide;
            else if (pDevMode->dmDuplex == DMDUP_HORIZONTAL)
                duplexMode = QPrint::DuplexShortSide;
        }
        // Clean-up
        free(pDevMode);
    }
    return duplexMode;
}

void QWindowsPrintDevice::loadColorModes() const
{
    m_colorModes.append(QPrint::GrayScale);
    DWORD color = DeviceCapabilities(wcharId(), nullptr, DC_COLORDEVICE, nullptr, nullptr);
    if (int(color) == 1)
        m_colorModes.append(QPrint::Color);
    m_haveColorModes = true;
    QWindowsPrinterInfo *info = windowsDeviceLookup()->data();
    info[m_infoIndex].m_haveColorModes = true;
    info[m_infoIndex].m_colorModes = m_colorModes;
}

QPrint::ColorMode QWindowsPrintDevice::defaultColorMode() const
{
    if (!m_haveColorModes)
        loadColorModes();
    if (!m_colorModes.contains(QPrint::Color))
        return QPrint::GrayScale;

    QPrint::ColorMode colorMode = QPrint::GrayScale;

    if (LPDEVMODE pDevMode = getDevmode(m_hPrinter, m_id)) {
        // Get the default color mode
        if (pDevMode->dmFields & DM_COLOR && pDevMode->dmColor == DMCOLOR_COLOR)
            colorMode = QPrint::Color;
        // Clean-up
        free(pDevMode);
    }
    return colorMode;
}

QStringList QWindowsPrintDevice::availablePrintDeviceIds()
{
    QStringList list;
    DWORD needed = 0;
    DWORD returned = 0;
    if ((!EnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, nullptr, 4, 0, 0, &needed, &returned)
         && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
        || !needed) {
        return list;
    }
    QScopedArrayPointer<BYTE> buffer(new BYTE[needed]);
    if (!EnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, nullptr, 4, buffer.data(), needed, &needed, &returned))
        return list;
    PPRINTER_INFO_4 infoList = reinterpret_cast<PPRINTER_INFO_4>(buffer.data());
    for (uint i = 0; i < returned; ++i)
        list.append(QString::fromWCharArray(infoList[i].pPrinterName));
    return list;
}

QString QWindowsPrintDevice::defaultPrintDeviceId()
{
    DWORD size = 0;
    if (GetDefaultPrinter(nullptr, &size) == ERROR_FILE_NOT_FOUND || size < 2)
       return QString();

    QScopedArrayPointer<wchar_t> name(new wchar_t[size]);
    GetDefaultPrinter(name.data(), &size);
    return QString::fromWCharArray(name.data());
}

void QWindowsPrintDevice::loadCopiesSupport() const
{
    auto printerId = wcharId();
    m_supportsMultipleCopies = (DeviceCapabilities(printerId, nullptr, DC_COPIES, nullptr, nullptr) > 1);
    m_supportsCollateCopies = DeviceCapabilities(printerId, nullptr, DC_COLLATE, nullptr, nullptr);
    m_haveCopies = true;
    QWindowsPrinterInfo *info = windowsDeviceLookup()->data();
    info[m_infoIndex].m_haveCopies = true;
    info[m_infoIndex].m_supportsMultipleCopies = m_supportsMultipleCopies;
    info[m_infoIndex].m_supportsCollateCopies = m_supportsCollateCopies;
}

bool QWindowsPrintDevice::supportsCollateCopies() const
{
    if (!m_haveCopies)
        loadCopiesSupport();
    return m_supportsCollateCopies;
}

bool QWindowsPrintDevice::supportsMultipleCopies() const
{
    if (!m_haveCopies)
        loadCopiesSupport();
    return m_supportsMultipleCopies;
}

bool QWindowsPrintDevice::supportsCustomPageSizes() const
{
    if (!m_haveMinMaxPageSizes)
        loadMinMaxPageSizes();
    return m_supportsCustomPageSizes;
}

QSize QWindowsPrintDevice::minimumPhysicalPageSize() const
{
    if (!m_haveMinMaxPageSizes)
        loadMinMaxPageSizes();
    return m_minimumPhysicalPageSize;
}

QSize QWindowsPrintDevice::maximumPhysicalPageSize() const
{
    if (!m_haveMinMaxPageSizes)
        loadMinMaxPageSizes();
    return m_maximumPhysicalPageSize;
}

void QWindowsPrintDevice::loadMinMaxPageSizes() const
{
    // Min/Max custom size is in tenths of a millimeter
    const qreal multiplier = qt_pointMultiplier(QPageLayout::Millimeter);
    auto printerId = wcharId();
    DWORD min = DeviceCapabilities(printerId, nullptr, DC_MINEXTENT, nullptr, nullptr);
    m_minimumPhysicalPageSize = QSize((LOWORD(min) / 10.0) * multiplier, (HIWORD(min) / 10.0) * multiplier);
    DWORD max = DeviceCapabilities(printerId, nullptr, DC_MAXEXTENT, nullptr, nullptr);
    m_maximumPhysicalPageSize = QSize((LOWORD(max) / 10.0) * multiplier, (HIWORD(max) / 10.0) * multiplier);
    m_supportsCustomPageSizes = (m_maximumPhysicalPageSize.width() > 0 && m_maximumPhysicalPageSize.height() > 0);
    m_haveMinMaxPageSizes = true;
    QWindowsPrinterInfo *info = windowsDeviceLookup()->data();
    info[m_infoIndex].m_haveCopies = true;
    info[m_infoIndex].m_supportsMultipleCopies = m_supportsMultipleCopies;
    info[m_infoIndex].m_supportsCollateCopies = m_supportsCollateCopies;
}

QT_END_NAMESPACE
