/*
 * Copyright 2024 Rémi Bernon for CodeWeavers
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include <stdarg.h>
#include <stdlib.h>

#include "ntstatus.h"
#include "windef.h"
#include "winbase.h"
#include "winternl.h"
#include "winioctl.h"
#include "ntuser.h"

#include "ddk/wdm.h"
#include "ddk/hidport.h"
#include "ddk/hidpddi.h"
#include "ddk/hidtypes.h"

#include "wine/hid.h"
#include "wine/debug.h"
#include "wine/list.h"

WINE_DEFAULT_DEBUG_CHANNEL(hid);

struct contact
{
    struct list entry;
    ULONG id;
    POINT pos;
};

struct device
{
    LONG removed;
    DEVICE_OBJECT *bus_device;
    PHIDP_PREPARSED_DATA preparsed;

    FILE_OBJECT dummy_file;
    IO_STATUS_BLOCK io;
    ULONG report_len;
    char *report_buf;

    ULONG contact_count;
    struct list contacts;

    ULONG caps_count;
    ULONG contact_max;
    HIDP_VALUE_CAPS *id_caps;
    HIDP_VALUE_CAPS *x_caps;
    HIDP_VALUE_CAPS *y_caps;
};

static inline struct device *impl_from_DEVICE_OBJECT( DEVICE_OBJECT *device )
{
    return (struct device *)device->DeviceExtension;
}

static inline LONG sign_extend( ULONG value, const HIDP_VALUE_CAPS *caps )
{
    UINT sign = 1 << (caps->BitSize - 1);
    if (sign <= 1 || caps->LogicalMin >= 0) return value;
    return value - ((value & sign) << 1);
}

static inline LONG scale_value( ULONG value, const HIDP_VALUE_CAPS *caps, LONG min, LONG max )
{
    LONG tmp = sign_extend( value, caps );
    if (caps->LogicalMin > caps->LogicalMax) return 0;
    if (caps->LogicalMin > tmp || caps->LogicalMax < tmp) return 0;
    return min + MulDiv( tmp - caps->LogicalMin, max - min, caps->LogicalMax - caps->LogicalMin );
}

static NTSTATUS WINAPI read_completion( DEVICE_OBJECT *device, IRP *irp, void *context );

static NTSTATUS start_device_read( DEVICE_OBJECT *device )
{
    struct device *impl = impl_from_DEVICE_OBJECT( device );
    IO_STACK_LOCATION *stack;
    NTSTATUS status;
    IRP *irp;

    TRACE( "device %p\n", device );

    irp = IoBuildAsynchronousFsdRequest( IRP_MJ_READ, device, impl->report_buf,
                                         impl->report_len, NULL, &impl->io );
    if (!irp) return STATUS_NO_MEMORY;
    irp->Tail.Overlay.OriginalFileObject = &impl->dummy_file;
    stack = IoGetNextIrpStackLocation( irp );
    stack->FileObject = &impl->dummy_file;

    TRACE( "created irp %p\n", irp );

    IoSetCompletionRoutine( irp, read_completion, device, TRUE, TRUE, TRUE );
    if ((status = IoCallDriver( impl->bus_device, irp )) && status != STATUS_PENDING) return status;
    return STATUS_SUCCESS;
}

static void add_contact( struct device *impl, struct list *old_contacts, ULONG id, LONG x, LONG y )
{
    UINT flags = POINTER_MESSAGE_FLAG_INRANGE | POINTER_MESSAGE_FLAG_INCONTACT | POINTER_MESSAGE_FLAG_CONFIDENCE;
    INPUT input = {.type = INPUT_HARDWARE};
    struct contact *contact;

    LIST_FOR_EACH_ENTRY( contact, old_contacts, struct contact, entry )
        if (contact->id == id) break;

    if (&contact->entry != old_contacts)
    {
        input.hi.uMsg = WM_POINTERUPDATE;
        list_remove( &contact->entry );

        contact->pos.x = x;
        contact->pos.y = y;
        TRACE( "updating contact %#lx, pos %s\n", contact->id, wine_dbgstr_point( &contact->pos ) );
    }
    else if ((contact = calloc( 1, sizeof(*contact) )))
    {
        input.hi.uMsg = WM_POINTERDOWN;
        flags |= POINTER_MESSAGE_FLAG_NEW;

        contact->id = id;
        contact->pos.x = x;
        contact->pos.y = y;
        TRACE( "new contact %#lx, pos %s\n", contact->id, wine_dbgstr_point( &contact->pos ) );
    }
    else
    {
        ERR( "failed to allocate new contact\n" );
        return;
    }

    input.hi.wParamL = contact->id;
    input.hi.wParamH = flags;
    NtUserSendHardwareInput( 0, 0, &input, MAKELPARAM(contact->pos.x, contact->pos.y) );

    list_add_tail( &impl->contacts, &contact->entry );
}

static void release_contacts( struct list *contacts )
{
    struct contact *contact, *next;

    LIST_FOR_EACH_ENTRY_SAFE( contact, next, contacts, struct contact, entry )
    {
        INPUT input = {.type = INPUT_HARDWARE};
        ULONG flags = POINTER_MESSAGE_FLAG_CONFIDENCE;

        TRACE( "releasing contact %#lx, pos %s\n", contact->id, wine_dbgstr_point( &contact->pos ) );

        input.hi.uMsg = WM_POINTERUP;
        input.hi.wParamL = contact->id;
        input.hi.wParamH = flags;
        NtUserSendHardwareInput( 0, 0, &input, MAKELPARAM(contact->pos.x, contact->pos.y) );

        list_remove( &contact->entry );
        free( contact );
    }
}

static void process_hid_report( struct device *impl, char *report_buf, UINT report_len )
{
    struct list old_contacts = LIST_INIT( old_contacts );
    ULONG i, value, contact_count, usage_count, id;
    LONG x = 0, y = 0;
    NTSTATUS status;
    USHORT usage;

    TRACE( "impl %p, report_buf %p, report_len %u\n", impl, report_buf, report_len );

    status = HidP_GetUsageValue( HidP_Input, HID_USAGE_PAGE_DIGITIZER, 0, HID_USAGE_DIGITIZER_CONTACT_COUNT,
                                 &contact_count, impl->preparsed, report_buf, report_len );
    if (status != HIDP_STATUS_SUCCESS) return;
    if (contact_count > impl->contact_max)
    {
        WARN( "got %lu contacts, capping to %lu.\n", contact_count, impl->contact_max );
        contact_count = impl->contact_max;
    }

    list_move_tail( &old_contacts, &impl->contacts );

    for (i = 0; i < impl->caps_count; i++)
    {
        USHORT collection = impl->id_caps[i].LinkCollection;

        usage_count = 1;
        usage = HID_USAGE_DIGITIZER_TIP_SWITCH;
        status = HidP_GetUsages( HidP_Input, HID_USAGE_PAGE_DIGITIZER, collection, &usage, &usage_count,
                                 impl->preparsed, report_buf, report_len );
        if (status != HIDP_STATUS_SUCCESS || !usage_count) continue;

        status = HidP_GetUsageValue( HidP_Input, HID_USAGE_PAGE_DIGITIZER, collection, HID_USAGE_DIGITIZER_CONTACT_ID,
                                     &id, impl->preparsed, report_buf, report_len );
        if (status != HIDP_STATUS_SUCCESS) continue;

        status = HidP_GetUsageValue( HidP_Input, HID_USAGE_PAGE_GENERIC, collection, HID_USAGE_GENERIC_X,
                                     &value, impl->preparsed, report_buf, report_len );
        if (status != HIDP_STATUS_SUCCESS) continue;
        else x = scale_value( value, impl->x_caps + i, 0, 65535 );

        status = HidP_GetUsageValue( HidP_Input, HID_USAGE_PAGE_GENERIC, collection, HID_USAGE_GENERIC_Y,
                                     &value, impl->preparsed, report_buf, report_len );
        if (status != HIDP_STATUS_SUCCESS) continue;
        else y = scale_value( value, impl->y_caps + i, 0, 65535 );

        add_contact( impl, &old_contacts, id, x, y );
    }

    release_contacts( &old_contacts );
}

static NTSTATUS WINAPI read_completion( DEVICE_OBJECT *device, IRP *irp, void *context )
{
    struct device *impl = impl_from_DEVICE_OBJECT( context );
    NTSTATUS status;

    TRACE( "device %p, irp %p, context %p\n", device, irp, context );

    if (irp->IoStatus.Status)
        WARN( "device read failed with status %#lx, stopping\n", irp->IoStatus.Status );
    else
    {
        process_hid_report( impl, impl->report_buf, impl->report_len );

        if (!InterlockedOr( &impl->removed, FALSE ) && (status = start_device_read( context )))
            ERR( "Failed to start next read, status %#lx\n", status );
    }

    if (irp->PendingReturned) IoMarkIrpPending( irp );
    return STATUS_SUCCESS;
}

static NTSTATUS WINAPI driver_ioctl( DEVICE_OBJECT *device, IRP *irp )
{
    IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation( irp );
    ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
    struct device *impl = impl_from_DEVICE_OBJECT( device );

    if (InterlockedOr( &impl->removed, FALSE ))
    {
        irp->IoStatus.Status = STATUS_DELETE_PENDING;
        irp->IoStatus.Information = 0;
        IoCompleteRequest( irp, IO_NO_INCREMENT );
        return STATUS_DELETE_PENDING;
    }

    TRACE( "device %p, irp %p, code %#lx, bus_device %p.\n", device, irp, code, impl->bus_device );

    IoSkipCurrentIrpStackLocation( irp );
    return IoCallDriver( impl->bus_device, irp );
}

static NTSTATUS call_hid_device( DEVICE_OBJECT *device, DWORD major, DWORD code, void *in_buf,
                                 DWORD in_len, void *out_buf, DWORD out_len )
{
    struct device *impl = impl_from_DEVICE_OBJECT( device );
    IO_STACK_LOCATION *stack;
    IO_STATUS_BLOCK io;
    NTSTATUS status;
    KEVENT event;
    IRP *irp;

    KeInitializeEvent( &event, NotificationEvent, FALSE );
    if (major == IRP_MJ_DEVICE_CONTROL)
        irp = IoBuildDeviceIoControlRequest( code, device, in_buf, in_len, out_buf, out_len,
                                             FALSE, &event, &io );
    else
        irp = IoBuildSynchronousFsdRequest( code, device, out_buf, out_len, NULL, &event, &io );
    if (!irp) return STATUS_NO_MEMORY;

    irp->Tail.Overlay.OriginalFileObject = &impl->dummy_file;
    if (code == IRP_MJ_CREATE) irp->Flags |= IRP_CREATE_OPERATION;
    if (code == IRP_MJ_CLOSE) irp->Flags |= IRP_CLOSE_OPERATION;
    stack = IoGetNextIrpStackLocation( irp );
    stack->FileObject = &impl->dummy_file;

    status = IoCallDriver( impl->bus_device, irp );
    if (status == STATUS_PENDING) KeWaitForSingleObject( &event, Executive, KernelMode, FALSE, NULL );
    return io.Status;
}

static NTSTATUS initialize_device( DEVICE_OBJECT *device )
{
    struct device *impl = impl_from_DEVICE_OBJECT( device );
    HID_COLLECTION_INFORMATION info;
    USHORT usage, count, report_len;
    HIDP_VALUE_CAPS value_caps;
    char *report_buf;
    NTSTATUS status;
    HIDP_CAPS caps;

    list_init( &impl->contacts );

    if ((status = call_hid_device( device, IRP_MJ_CREATE, 0, NULL, 0, NULL, 0 ))) return status;
    if ((status = call_hid_device( device, IRP_MJ_DEVICE_CONTROL, IOCTL_HID_GET_COLLECTION_INFORMATION,
                                   &info, 0, &info, sizeof(info) )))
        return status;
    if (!(impl->preparsed = malloc( info.DescriptorSize ))) return STATUS_NO_MEMORY;
    if ((status = call_hid_device( device, IRP_MJ_DEVICE_CONTROL, IOCTL_HID_GET_COLLECTION_DESCRIPTOR,
                                   NULL, 0, impl->preparsed, info.DescriptorSize )))
        return status;

    status = HidP_GetCaps( impl->preparsed, &caps );
    if (status != HIDP_STATUS_SUCCESS) return status;

    count = 1;
    usage = HID_USAGE_DIGITIZER_CONTACT_COUNT_MAX;
    status = HidP_GetSpecificValueCaps( HidP_Feature, HID_USAGE_PAGE_DIGITIZER, 0, usage,
                                        &value_caps, &count, impl->preparsed );
    if (status == HIDP_STATUS_SUCCESS)
    {
        report_len = caps.FeatureReportByteLength;
        if (!(report_buf = malloc( report_len ))) return STATUS_NO_MEMORY;
        report_buf[0] = value_caps.ReportID;

        status = call_hid_device( device, IRP_MJ_DEVICE_CONTROL, IOCTL_HID_GET_FEATURE, NULL, 0, report_buf, report_len );
        if (!status) status = HidP_GetUsageValue( HidP_Feature, HID_USAGE_PAGE_DIGITIZER, 0, usage,
                                                  &impl->contact_max, impl->preparsed, report_buf, report_len );
        free( report_buf );
        if (status && status != HIDP_STATUS_SUCCESS) return status;

        count = 1;
        usage = HID_USAGE_DIGITIZER_CONTACT_COUNT;
        status = HidP_GetSpecificValueCaps( HidP_Input, HID_USAGE_PAGE_DIGITIZER, 0, usage,
                                            &value_caps, &count, impl->preparsed );
        if (status != HIDP_STATUS_SUCCESS) goto failed;

        count = caps.NumberLinkCollectionNodes;
        usage = HID_USAGE_DIGITIZER_CONTACT_ID;
        if (!(impl->id_caps = malloc( sizeof(*impl->id_caps) * caps.NumberLinkCollectionNodes ))) return STATUS_NO_MEMORY;
        status = HidP_GetSpecificValueCaps( HidP_Input, HID_USAGE_PAGE_DIGITIZER, 0, usage,
                                            impl->id_caps, &count, impl->preparsed );
        if (status != HIDP_STATUS_SUCCESS) goto failed;
        impl->caps_count = count;

        count = impl->caps_count;
        usage = HID_USAGE_GENERIC_X;
        if (!(impl->x_caps = malloc( sizeof(*impl->x_caps) * impl->caps_count ))) return STATUS_NO_MEMORY;
        status = HidP_GetSpecificValueCaps( HidP_Input, HID_USAGE_PAGE_GENERIC, 0, usage,
                                            impl->x_caps, &count, impl->preparsed );
        if (status != HIDP_STATUS_SUCCESS) goto failed;

        count = impl->caps_count;
        usage = HID_USAGE_GENERIC_Y;
        if (!(impl->y_caps = malloc( sizeof(*impl->y_caps) * impl->caps_count ))) return STATUS_NO_MEMORY;
        status = HidP_GetSpecificValueCaps( HidP_Input, HID_USAGE_PAGE_GENERIC, 0, usage,
                                            impl->y_caps, &count, impl->preparsed );
        if (status != HIDP_STATUS_SUCCESS) goto failed;

        impl->report_len = caps.InputReportByteLength;
        if (!(impl->report_buf = malloc( impl->report_len ))) return STATUS_NO_MEMORY;
        impl->report_buf[0] = value_caps.ReportID;

        return STATUS_SUCCESS;
    }

failed:
    ERR( "%#x usage not found, unsupported device\n", usage );
    return STATUS_NOT_SUPPORTED;
}

static NTSTATUS WINAPI set_event_completion( DEVICE_OBJECT *device, IRP *irp, void *context )
{
    if (irp->PendingReturned) KeSetEvent( (KEVENT *)context, IO_NO_INCREMENT, FALSE );
    return STATUS_MORE_PROCESSING_REQUIRED;
}

static NTSTATUS WINAPI driver_pnp( DEVICE_OBJECT *device, IRP *irp )
{
    IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation( irp );
    struct device *impl = impl_from_DEVICE_OBJECT( device );
    UCHAR code = stack->MinorFunction;
    NTSTATUS status;
    KEVENT event;

    TRACE( "device %p, irp %p, code %#x, bus_device %p.\n", device, irp, code, impl->bus_device );

    switch (stack->MinorFunction)
    {
    case IRP_MN_START_DEVICE:
        KeInitializeEvent( &event, NotificationEvent, FALSE );
        IoCopyCurrentIrpStackLocationToNext( irp );
        IoSetCompletionRoutine( irp, set_event_completion, &event, TRUE, TRUE, TRUE );

        status = IoCallDriver( impl->bus_device, irp );
        if (status == STATUS_PENDING)
        {
            KeWaitForSingleObject( &event, Executive, KernelMode, FALSE, NULL );
            status = irp->IoStatus.Status;
        }
        if (!status) status = initialize_device( device );
        if (!status) status = start_device_read( device );

        if (status) irp->IoStatus.Status = status;
        IoCompleteRequest( irp, IO_NO_INCREMENT );
        return status;

    case IRP_MN_SURPRISE_REMOVAL:
        status = STATUS_SUCCESS;
        if (InterlockedExchange( &impl->removed, TRUE )) break;
        call_hid_device( device, IRP_MJ_CLOSE, 0, NULL, 0, NULL, 0 );
        break;

    case IRP_MN_REMOVE_DEVICE:
        IoSkipCurrentIrpStackLocation( irp );
        status = IoCallDriver( impl->bus_device, irp );
        IoDetachDevice( impl->bus_device );
        release_contacts( &impl->contacts );
        free( impl->id_caps );
        free( impl->x_caps );
        free( impl->y_caps );
        free( impl->report_buf );
        free( impl->preparsed );
        IoDeleteDevice( device );
        return status;

    default:
        IoSkipCurrentIrpStackLocation( irp );
        return IoCallDriver( impl->bus_device, irp );
    }

    return STATUS_SUCCESS;
}

static NTSTATUS WINAPI add_device( DRIVER_OBJECT *driver, DEVICE_OBJECT *bus_device )
{
    struct device *impl;
    DEVICE_OBJECT *device;
    NTSTATUS status;

    TRACE( "driver %p, bus_device %p.\n", driver, bus_device );

    if ((status = IoCreateDevice( driver, sizeof(struct device), NULL, FILE_DEVICE_BUS_EXTENDER,
                                  0, FALSE, &device )))
    {
        ERR( "failed to create bus FDO, status %#lx.\n", status );
        return status;
    }

    impl = device->DeviceExtension;
    impl->bus_device = bus_device;

    IoAttachDeviceToDeviceStack( device, bus_device );
    device->Flags &= ~DO_DEVICE_INITIALIZING;
    return STATUS_SUCCESS;
}

static void WINAPI driver_unload( DRIVER_OBJECT *driver )
{
    TRACE( "driver %p\n", driver );
}

NTSTATUS WINAPI DriverEntry( DRIVER_OBJECT *driver, UNICODE_STRING *path )
{
    TRACE( "driver %p, path %s.\n", driver, debugstr_w(path->Buffer) );

    driver->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] = driver_ioctl;
    driver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = driver_ioctl;
    driver->MajorFunction[IRP_MJ_PNP] = driver_pnp;
    driver->DriverExtension->AddDevice = add_device;
    driver->DriverUnload = driver_unload;

    return STATUS_SUCCESS;
}
