//
// Definition of macOS Cocoa Pen/Tablet event driver.
//
// Copyright 2025-2026 by Bill Spitzak and others.
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file.  If this
// file is missing or damaged, see the license at:
//
//     https://www.fltk.org/COPYING.php
//
// Please see the following page on how to report bugs and issues:
//
//     https://www.fltk.org/bugs.php
//

#include "src/drivers/Base/Fl_Base_Pen_Events.H"

#include <FL/platform.H>
#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include "../../Fl_Screen_Driver.H"

#import <Cocoa/Cocoa.h>


extern Fl_Window *fl_xmousewin;

#if MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12

typedef short NSEventSubtype;
#define NSPointingDeviceTypePen NSPenPointingDevice
#define NSEventTypeMouseEntered NSMouseEntered
#define NSEventTypeMouseExited NSMouseExited
#define NSEventTypeTabletProximity NSTabletProximity
#define NSEventTypeTabletPoint NSTabletPoint
#define NSEventSubtypeTabletProximity NSTabletProximityEventSubtype
#define NSEventSubtypeTabletPoint NSTabletPointEventSubtype
#define NSEventSubtypeMouseEvent NSMouseEventSubtype
#define NSEventTypeLeftMouseDown NSLeftMouseDown
#define NSEventTypeLeftMouseUp NSLeftMouseUp
#define NSEventTypeLeftMouseDragged NSLeftMouseDragged
#define NSEventTypeMouseMoved NSMouseMoved
#define NSEventTypeRightMouseDown NSRightMouseDown
#define NSEventTypeRightMouseUp NSRightMouseUp
#define NSEventTypeRightMouseDragged NSRightMouseDragged
#define NSEventTypeOtherMouseUp NSOtherMouseUp
#define NSEventTypeOtherMouseDown NSOtherMouseDown
#define NSEventTypeOtherMouseDragged NSOtherMouseDragged
#define NSPointingDeviceTypeEraser NSEraserPointingDevice

#endif // MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12

static NSPointingDeviceType device_type_ { NSPointingDeviceTypePen };

// The trait list keeps track of traits for every pen ID that appears while
// handling events.
// AppKit does not tell us what traits are available per pen or tablet, so
// we use the first 5 motion events to discover event values that are not
// the default value, and enter that knowledge into the traits database.
typedef std::map<int, Fl::Pen::Trait> TraitList;
static TraitList trait_list_;
static int trait_countdown_ { 5 };
static int current_pen_id_ { -1 };
static Fl::Pen::Trait current_pen_trait_ { Fl::Pen::Trait::DRIVER_AVAILABLE };
static Fl::Pen::Trait driver_traits_ {
  Fl::Pen::Trait::DRIVER_AVAILABLE | Fl::Pen::Trait::PEN_ID |
  Fl::Pen::Trait::ERASER | Fl::Pen::Trait::PRESSURE |
  Fl::Pen::Trait::BARREL_PRESSURE | Fl::Pen::Trait::TILT_X |
  Fl::Pen::Trait::TILT_Y | Fl::Pen::Trait::TWIST
  // Notably missing: PROXIMITY
};

// Temporary storage of event data for the driver;
static Fl::Pen::EventData ev;


namespace Fl {

namespace Private {

// Global mouse position at mouse down event
extern int e_x_down;
extern int e_y_down;

}; // namespace Private

namespace Pen {

class Cocoa_Driver : public Driver {
public:
  Cocoa_Driver() = default;
  //virtual void subscribe(Fl_Widget* widget) override;
  //virtual void unsubscribe(Fl_Widget* widget) override;
  //virtual void release() override;
  virtual Trait traits() override { return driver_traits_; }
  virtual Trait pen_traits(int pen_id) override {
    auto it = trait_list_.find(pen_id);
    if (pen_id == 0)
      return current_pen_trait_;
    if (it == trait_list_.end()) {
      return Trait::DRIVER_AVAILABLE;
    } else {
      return it->second;
    }
  }
};

Cocoa_Driver cocoa_driver;
Driver& driver = cocoa_driver;

} // namespace Pen

} // namespace Fl


using namespace Fl::Pen;


/*
 Copy the event state.
 */
static void copy_state() {
  Fl::Pen::State tr = (Fl::Pen::State)((uint32_t)Fl::Pen::e.state ^ (uint32_t)ev.state);
  Fl::Pen::e = ev;
  Fl::Pen::e.trigger = tr;
  Fl::e_x = (int)ev.x;
  Fl::e_y = (int)ev.y;
  Fl::e_x_root = (int)ev.rx;
  Fl::e_y_root = (int)ev.ry;
}

/*
 Offset coordinates for subwindows and subsubwindows.
 */
static void offset_subwindow_event(Fl_Widget *w, double &x, double &y) {
  Fl_Widget *p = w, *q;
  while (p) {
    q = p->parent();
    if (p->as_window() && q) {
      x -= p->x();
      y -= p->y();
    }
    p = q;
  };
}

/*
 Check if coordinates are within the widget box.
 Coordinates are in top_window space. We iterate up the hierarchy to ensure
 that we handle subwindows correctly.
 */
static bool event_inside(Fl_Widget *w, double x, double y) {
  offset_subwindow_event(w, x, y);
  if (w->as_window()) {
    return ((x >= 0) && (y >= 0) && (x < w->w()) && (y < w->h()));
  } else {
    return ((x >= w->x()) && (y >= w->y()) && (x < w->x() + w->w()) && (y < w->y() + w->h()));
  }
}

/*
 Find the widget under the pen event.
 Search the subscriber list for widgets that are inside the same top window,
 are visible, and are within the give coordinates. Subwindow aware.
 */
static Fl_Widget *find_below_pen(Fl_Window *win, double x, double y) {
  for (auto &sub: subscriber_list_) {
    Fl_Widget *candidate = sub.second->widget();
    if (candidate && (candidate->top_window() == win)) {
      if (candidate->visible() && event_inside(candidate, x, y)) {
        return candidate;
      }
    }
  }
  return nullptr;
}

/*
 Send the current event and event data to a widget.
 Note: we will get the wrong coordinates if the widget is not a child of
 the current event window (LEAVE events between windows).
 */
static int pen_send(Fl_Widget *w, int event, State trigger, bool &copied) {
  // Copy most event data only once
  if (!copied) {
    copy_state();
    copied = true;
  }
  // Copy the top_window coordinates again as they may change when w changes
  e.x = ev.x;
  e.y = ev.y;
  offset_subwindow_event(w, e.x, e.y);
  Fl::e_x = e.x;
  Fl::e_y = e.y;
  // Send the event.
  e.trigger = trigger;
  return w->handle(event);
}

/*
 Send an event to all subscribers.
 */
static int pen_send_all(int event, State trigger) {
  bool copied = false;
  // use local value because handler may still change ev values
  for (auto &it: subscriber_list_) {
    auto w = it.second->widget();
    if (w)
      pen_send(w, event, trigger, copied);
  }
  return 1;
}

/*
 Convert the NSEvent button number to Fl::Pen::State,
 */
static State button_to_trigger(NSInteger button, bool down) {
  switch (button) {
    case 0:
      if ( (ev.state & (State::ERASER_DOWN | State::ERASER_HOVERS)) != State::NONE ) {
        return down ? State::ERASER_DOWN : State::ERASER_HOVERS;
      } else {
        return down ? State::TIP_DOWN : State::TIP_HOVERS;
      }
    case 1: return State::BUTTON0;
    case 2: return State::BUTTON1;
    case 3: return State::BUTTON2;
    case 4: return State::BUTTON3;
    default: return State::NONE;
  }
}

/*
 Handle events coming from Cocoa.
 `capabilityMask` is useless, because it is vendor defined
 If a modal window is open, AppKit will send window specific events only there.
 */
bool fl_cocoa_tablet_handler(NSEvent *event, Fl_Window *eventWindow) {
  // Quick access to the main type.
  auto type = [event type];

  // There seems nothing useful here. Ignore for now.
  if ((type == NSEventTypeMouseEntered) || (type == NSEventTypeMouseExited)) {
    return false;
  }

  // Sort out tablet-only events and mouse plus tablet events.
  bool is_mouse = ((type != NSEventTypeTabletPoint) && (type != NSEventTypeTabletProximity));

  // Set the subtype if one is available. Only NSEventSubtypeTabletPoint and
  // NSEventSubtypeTabletProximity matter in this context
  NSEventSubtype subtype = is_mouse ? [event subtype] : NSEventSubtypeMouseEvent;

  // Is this a change in proximity event?
  bool is_proximity = ((type == NSEventTypeTabletProximity) || (subtype == NSEventSubtypeTabletProximity));

  // Is this a pen pointer event?
  bool is_point = ((type == NSEventTypeTabletPoint) || (subtype == NSEventSubtypeTabletPoint));

  // Check if any of the pen down, move, drag, or up events was triggered.
  bool is_down = ((type == NSEventTypeLeftMouseDown) || (type == NSEventTypeRightMouseDown) || (type == NSEventTypeOtherMouseDown));
  bool is_up = ((type == NSEventTypeLeftMouseUp) || (type == NSEventTypeRightMouseUp) || (type == NSEventTypeOtherMouseUp));
  bool is_drag = ((type == NSEventTypeLeftMouseDragged) || (type == NSEventTypeRightMouseDragged) || (type == NSEventTypeOtherMouseDragged));
  bool is_motion = is_drag || (type == NSEventTypeMouseMoved);

  // Find out if we can get the pen position
  bool has_position = (eventWindow != nullptr) && (is_up || is_down || is_motion || is_proximity || is_point);

  // Event has extended pen data set:
  if (has_position) {
    // Get the position data.
    auto pt = [event locationInWindow];
    double s = Fl::screen_driver()->scale(0);
    ev.x = pt.x/s;
    ev.y = eventWindow->h() - pt.y/s;
    ev.rx = ev.x + eventWindow->x();
    ev.ry = ev.y + eventWindow->y();
    if (!is_proximity) {
      // Get the pressure data.
      ev.pressure = [event pressure];
      ev.barrel_pressure = [event tangentialPressure];
      // Get the tilt
      auto tilt = [event tilt];
      ev.tilt_x = -tilt.x;
      ev.tilt_y =  tilt.y;
      // Other stuff
      ev.twist = [event rotation]; // TODO: untested
      // ev.proximity = [event proximity]; // not supported in AppKit
    }
    if (device_type_ == NSPointingDeviceTypeEraser) {
      if ([event buttonMask] & 1)
        ev.state = State::ERASER_DOWN;
      else
        ev.state = State::ERASER_HOVERS;
    } else {
      if ([event buttonMask] & 1)
        ev.state = State::TIP_DOWN;
      else
        ev.state = State::TIP_HOVERS;
    }
    if ([event buttonMask] & 0x0002) ev.state |= State::BUTTON0;
    if ([event buttonMask] & 0x0004) ev.state |= State::BUTTON1;
    if ([event buttonMask] & 0x0008) ev.state |= State::BUTTON2;
    if ([event buttonMask] & 0x0010) ev.state |= State::BUTTON3;
    // printf("0x%08x\n", [event buttonMask]);
  }
  if (is_proximity) {
    ev.pen_id = (int)[event vendorID];
    device_type_ = [event pointingDeviceType];
  }
  if (type == NSEventTypeTabletProximity) {
    if ([event isEnteringProximity]) {
      // Check if this is the first time we see this pen, or if the pen changed
      if (current_pen_id_ != ev.pen_id) {
        current_pen_id_ = ev.pen_id;
        auto it = trait_list_.find(current_pen_id_);
        if (it == trait_list_.end()) { // not found, create a new entry
          trait_list_[current_pen_id_] = Trait::DRIVER_AVAILABLE;
          trait_countdown_ = 5;
          pen_send_all(Fl::Pen::DETECTED, State::NONE);
          // printf("IN RANGE, NEW PEN\n");
        } else {
          pen_send_all(Fl::Pen::CHANGED, State::NONE);
          // printf("IN RANGE, CHANGED PEN\n");
        }
        trait_list_[0] = trait_list_[current_pen_id_];  // set current pen traits
      } else {
        pen_send_all(Fl::Pen::IN_RANGE, State::NONE);
        // printf("IN RANGE\n");
      }
    } else {
      pen_send_all(Fl::Pen::OUT_OF_RANGE, State::NONE);
      // printf("OUT OF RANGE\n");
    }
  }

  Fl_Widget *receiver = nullptr;
  bool pushed = false;
  bool event_data_copied = false;

  if (has_position) {
    if (trait_countdown_) {
      trait_countdown_--;
      if (ev.tilt_x != 0.0) current_pen_trait_ |= Trait::TILT_X;
      if (ev.tilt_y != 0.0) current_pen_trait_ |= Trait::TILT_Y;
      if (ev.pressure != 1.0) current_pen_trait_ |= Trait::PRESSURE;
      if (ev.barrel_pressure != 0.0) current_pen_trait_ |= Trait::BARREL_PRESSURE;
      if (ev.pen_id != 0) current_pen_trait_ |= Trait::PEN_ID;
      if (ev.twist != 0.0) current_pen_trait_ |= Trait::TWIST;
      //if (ev.proximity != 0) current_pen_trait_ |= Trait::PROXIMITY;
      trait_list_[current_pen_id_] = current_pen_trait_;
    }
    fl_xmousewin = eventWindow;
    if (pushed_ && pushed_->widget() && (Fl::pushed() == pushed_->widget())) {
      receiver = pushed_->widget();
      if (Fl::grab() && (Fl::grab() != receiver->top_window()))
        return 0;
      if (Fl::modal() && (Fl::modal() != receiver->top_window()))
        return 0;
      pushed = true;
    } else {
      if (Fl::grab() && (Fl::grab() != eventWindow))
        return 0;
      if (Fl::modal() && (Fl::modal() != eventWindow))
        return 0;
      auto bpen = below_pen_ ? below_pen_->widget() : nullptr;
      auto bmouse = Fl::belowmouse();
      auto bpen_old = bmouse && (bmouse == bpen) ? bpen : nullptr;
      auto bpen_now = find_below_pen(eventWindow, ev.x, ev.y);

      if (bpen_now != bpen_old) {
        if (bpen_old) {
          pen_send(bpen_old, Fl::Pen::LEAVE, State::NONE, event_data_copied);
        }
        below_pen_ = nullptr;
        if (bpen_now) {
          State state = (device_type_ == NSPointingDeviceTypeEraser) ? State::ERASER_HOVERS : State::TIP_HOVERS;
          if (pen_send(bpen_now, Fl::Pen::ENTER, state, event_data_copied)) {
            below_pen_ = subscriber_list_[bpen_now];
            Fl::belowmouse(bpen_now);
          }
        }
      }

      receiver = below_pen_ ? below_pen_->widget() : nullptr;
      if (!receiver)
        return 0;
    }
  } else {
    // Proximity events were handled earlier.
  }

  if (!receiver)
    return 0;

  if (is_down) {
    if (!pushed) {
      pushed_ = subscriber_list_[receiver];
      Fl::pushed(receiver);
    }
    State trigger = button_to_trigger([event buttonNumber], true);
    if ([event buttonNumber] == 0) {
      Fl::e_is_click = 1;
      Fl::Private::e_x_down = (int)ev.x;
      Fl::Private::e_y_down = (int)ev.y;
      if ([event clickCount] > 1)
        Fl::e_clicks++;
      else
        Fl::e_clicks = 0;
      pen_send(receiver, Fl::Pen::TOUCH, trigger, event_data_copied);
    } else {
      pen_send(receiver, Fl::Pen::BUTTON_PUSH, trigger, event_data_copied);
    }
  } else if (is_up) {
    if ( (ev.state & State::ANY_DOWN) == State::NONE ) {
      Fl::pushed(nullptr);
      pushed_ = nullptr;
    }
    State trigger = button_to_trigger([event buttonNumber], true);
    if ([event buttonNumber] == 0)
      pen_send(receiver, Fl::Pen::LIFT, trigger, event_data_copied);
    else
      pen_send(receiver, Fl::Pen::BUTTON_RELEASE, trigger, event_data_copied);
  } else if (is_motion) {
    if (  Fl::e_is_click &&
         ( (fabs((int)ev.x - Fl::Private::e_x_down) > 5) ||
           (fabs((int)ev.y - Fl::Private::e_y_down) > 5) ) )
      Fl::e_is_click = 0;
    if (pushed) {
      pen_send(receiver, Fl::Pen::DRAW, State::NONE, event_data_copied);
    } else {
      pen_send(receiver, Fl::Pen::HOVER, State::NONE, event_data_copied);
    }
  }
  // Always return 1 because at this point, we capture pen events and don't
  // want mouse events anymore!
  return 1;
}
