/* $NetBSD: ims.c,v 1.5 2023/05/10 00:10:02 riastradh Exp $ */
/* $OpenBSD ims.c,v 1.1 2016/01/12 01:11:15 jcs Exp $ */

/*
* HID-over-i2c mouse/trackpad driver
*
* Copyright (c) 2015, 2016 joshua stein <[email protected]>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: ims.c,v 1.5 2023/05/10 00:10:02 riastradh Exp $");

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/device.h>
#include <sys/ioctl.h>

#include <dev/i2c/i2cvar.h>
#include <dev/i2c/ihidev.h>

#include <dev/hid/hid.h>
#include <dev/hid/hidms.h>

struct ims_softc {
       struct ihidev   sc_hdev;
       struct hidms    sc_ms;
       bool            sc_enabled;
};

static void     ims_intr(struct ihidev *addr, void *ibuf, u_int len);

static int      ims_enable(void *);
static void     ims_disable(void *);
static int      ims_ioctl(void *, u_long, void *, int, struct lwp *);

const struct wsmouse_accessops ims_accessops = {
       ims_enable,
       ims_ioctl,
       ims_disable,
};

static int      ims_match(device_t, cfdata_t, void *);
static void     ims_attach(device_t, device_t, void *);
static int      ims_detach(device_t, int);
static void     ims_childdet(device_t, device_t);

CFATTACH_DECL2_NEW(ims, sizeof(struct ims_softc), ims_match, ims_attach,
   ims_detach, NULL, NULL, ims_childdet);

static int
ims_match(device_t parent, cfdata_t match, void *aux)
{
       struct ihidev_attach_arg *iha = (struct ihidev_attach_arg *)aux;
       int size;
       void *desc;

       ihidev_get_report_desc(iha->parent, &desc, &size);

       if (hid_is_collection(desc, size, iha->reportid,
           HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_POINTER)))
               return (IMATCH_IFACECLASS);

       if (hid_is_collection(desc, size, iha->reportid,
           HID_USAGE2(HUP_GENERIC_DESKTOP, HUG_MOUSE)))
               return (IMATCH_IFACECLASS);

       if (hid_is_collection(desc, size, iha->reportid,
           HID_USAGE2(HUP_DIGITIZERS, HUD_PEN)))
               return (IMATCH_IFACECLASS);

       if (hid_is_collection(desc, size, iha->reportid,
           HID_USAGE2(HUP_DIGITIZERS, HUD_TOUCH_SCREEN)))
               return (IMATCH_IFACECLASS);

       return (IMATCH_NONE);
}

static void
ims_attach(device_t parent, device_t self, void *aux)
{
       struct ims_softc *sc = device_private(self);
       struct hidms *ms = &sc->sc_ms;
       struct ihidev_attach_arg *iha = (struct ihidev_attach_arg *)aux;
       int size, repid;
       void *desc;
       struct hid_data * d __debugused;
       struct hid_item item __debugused;

       sc->sc_hdev.sc_idev = self;
       sc->sc_hdev.sc_intr = ims_intr;
       sc->sc_hdev.sc_parent = iha->parent;
       sc->sc_hdev.sc_report_id = iha->reportid;

       if (!pmf_device_register(self, NULL, NULL))
               aprint_error_dev(self, "couldn't establish power handler\n");

       ihidev_get_report_desc(iha->parent, &desc, &size);
       repid = iha->reportid;
       sc->sc_hdev.sc_isize = hid_report_size(desc, size, hid_input, repid);
       sc->sc_hdev.sc_osize = hid_report_size(desc, size, hid_output, repid);
       sc->sc_hdev.sc_fsize = hid_report_size(desc, size, hid_feature, repid);

       if (!hidms_setup(self, ms, iha->reportid, desc, size) != 0)
               return;

#if defined(DEBUG)
       /* calibrate the touchscreen */
       memset(&sc->sc_ms.sc_calibcoords, 0, sizeof(sc->sc_ms.sc_calibcoords));
       d = hid_start_parse(desc, size, hid_input);
       if (d != NULL) {
               while (hid_get_item(d, &item)) {
                       if (item.kind != hid_input
                           || HID_GET_USAGE_PAGE(item.usage) != HUP_GENERIC_DESKTOP
                           || item.report_ID != sc->sc_hdev.sc_report_id)
                               continue;
                       if (HID_GET_USAGE(item.usage) == HUG_X) {
                               aprint_normal("X range: %d - %d\n", item.logical_minimum, item.logical_maximum);
                       }
                       if (HID_GET_USAGE(item.usage) == HUG_Y) {
                               aprint_normal("Y range: %d - %d\n", item.logical_minimum, item.logical_maximum);
                       }
               }
               hid_end_parse(d);
       }
#endif
       tpcalib_init(&sc->sc_ms.sc_tpcalib);
       tpcalib_ioctl(&sc->sc_ms.sc_tpcalib, WSMOUSEIO_SCALIBCOORDS,
           (void *)&sc->sc_ms.sc_calibcoords, 0, 0);

       hidms_attach(self, ms, &ims_accessops);
}

static int
ims_detach(device_t self, int flags)
{
       int error;

       /* No need to do reference counting of ums, wsmouse has all the goo. */
       error = config_detach_children(self, flags);
       if (error)
               return error;

       pmf_device_deregister(self);
       return 0;
}

void
ims_childdet(device_t self, device_t child)
{
       struct ims_softc *sc = device_private(self);

       KASSERT(KERNEL_LOCKED_P());

       KASSERT(sc->sc_ms.hidms_wsmousedev == child);
       sc->sc_ms.hidms_wsmousedev = NULL;
}


static void
ims_intr(struct ihidev *addr, void *buf, u_int len)
{
       struct ims_softc *sc = (struct ims_softc *)addr;
       struct hidms *ms = &sc->sc_ms;

       if (sc->sc_enabled)
               hidms_intr(ms, buf, len);
}

static int
ims_enable(void *v)
{
       struct ims_softc *sc = v;
       int error;

       KASSERT(KERNEL_LOCKED_P());

       if (sc->sc_enabled)
               return EBUSY;

       sc->sc_enabled = 1;
       sc->sc_ms.hidms_buttons = 0;

       error = ihidev_open(&sc->sc_hdev);
       if (error)
               sc->sc_enabled = 0;
       return error;
}

static void
ims_disable(void *v)
{
       struct ims_softc *sc = v;

       KASSERT(KERNEL_LOCKED_P());

#ifdef DIAGNOSTIC
       if (!sc->sc_enabled) {
               printf("ums_disable: not enabled\n");
               return;
       }
#endif

       if (sc->sc_enabled) {
               sc->sc_enabled = 0;
               ihidev_close(&sc->sc_hdev);
       }

}

static int
ims_ioctl(void *v, u_long cmd, void *data, int flag, struct lwp *l)
{
       struct ims_softc *sc = v;

       switch (cmd) {
       case WSMOUSEIO_GTYPE:
               if (sc->sc_ms.flags & HIDMS_ABS) {
                       *(u_int *)data = WSMOUSE_TYPE_TPANEL;
               } else {
                       /* XXX: should we set something else? */
                       *(u_int *)data = WSMOUSE_TYPE_USB;
               }
               return 0;
       case WSMOUSEIO_SCALIBCOORDS:
       case WSMOUSEIO_GCALIBCOORDS:
               return tpcalib_ioctl(&sc->sc_ms.sc_tpcalib, cmd, data, flag, l);
       }
       return EPASSTHROUGH;
}