{
    This file is part of Chentrah,
    Copyright (C) 2004-2010 Anton Rzheshevski (chebmaster@mail.ru).

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see http://www.gnu.org/licenses/

 **********************************************************************}
{$longstrings on}
{$codepage utf-8}
unit cl_xinput_tablet;  //unlike cl_wintab, this unit is supplementary,
  //a vital part of event processing is placed inside the window manager class
  //which incorporated the class defined here as its field.

interface
uses SysUtils, ctypes, x, xlib, xutil;

var
  XinputTabletError: WideString;
  XinputTabletLoaded: boolean = false;

type
  TPenEvent = record x, y, pressure, reserved1, reserved2, reserved3: single end;

  {$packrecords c}
  PXAxisInfo = ^TXAxisInfo;
  TXAxisInfo = record
    resolution,
    min_value,
    max_value: cint;
  end;

(*
typedef struct {
        unsigned char input_class;
        unsigned char event_type_base;
} XInputClassInfo;
*)
  PXInputClassInfo = ^TXInputClassInfo;
  TXInputClassInfo = record
    input_class, event_type_base: cuchar;
  end;

(*
typedef struct {
        XID              device_id;
        int              num_classes;
        XInputClassInfo *classes;
} XDevice;
*)
  PXDevice = ^TXDevice;
  TXDevice = record
    device_id: TXID;
    num_classes: cint;
    _classes: PXInputClassInfo;
  end;


  { TXinputTabletManager }

  TXinputTabletManager = class
  protected
    Initialized: boolean;
    PressureRange: integer;
    display: pointer;
    device: PXDevice;
  public
    ErrorMessage: WideString;
    DeviceName: WideString;
    Events: array of TPenEvent;
    AxisInfo: array of TXAxisInfo;
    ValuatorEvent: integer;
    constructor Create(_display, _window: pointer);
    destructor Destroy;
  end;


(*typedef struct
    {
    int           type;        /* of event */
    unsigned long serial;      /* # of last request processed by server */
    Bool          send_event;  /* true if from a SendEvent request */
    Display       *display;    /* Display the event was read from */
    Window        window;      /* "event" window reported relative to */
    XID           deviceid;
    Window        root;        /* root window that the event occured on */
    Window        subwindow;   /* child window */
    Time          time;        /* milliseconds */
    int           x, y;        /* x, y coordinates in event window */
    int           x_root;      /* coordinates relative to root */
    int           y_root;      /* coordinates relative to root */
    unsigned int  state;       /* key or button mask */
    char          is_hint;     /* detail */
    Bool          same_screen; /* same screen flag */
    unsigned int  device_state; /* device key or button mask */
    unsigned char axes_count;
    unsigned char first_axis;
    int           axis_data[6];
    } XDeviceMotionEvent;
*)
TXDeviceMotionEvent = record
    _type: cint;
    serial: culong;
    send_event: cbool;
    display: pointer;
    window: pointer;
    deviceid: TXID;
    root: pointer;
    subwindow: pointer;
    time: TTime;
    x, y: cint;
    x_root: cint;
    y_root: cint;
    state: cuint;
    is_hint: cchar;
    same_screen: cbool;
    device_state: cuint;
    axes_count: cuchar;
    first_axis: cuchar;
    axis_data: array[0..5] of cint;
end;

implementation
  uses classes, cge;

const
  libXi='Xi';

type
(*typedef struct _XDeviceInfo
{
    XID        id;
    Atom        type;
    char        *name;
    int        num_classes;
    int        use;
    XAnyClassPtr    inputclassinfo;
} XDeviceInfo; *)


  PXDeviceInfo = ^TXDeviceInfo;
  TXDeviceInfo = record
    id: TXID;
    _type: TAtom;
    name: PChar;
    num_classes,
    use: cint;
    inputclassinfo: pointer;
  end;

  PXAnyClass = ^TXAnyClass;
  TXAnyClass = record
    _class: TXID;
    length: cint;
  end;

(*typedef struct _XValuatorInfo {
        XID            class;
        int            length;
        unsigned char  num_axes;
        unsigned char  mode;
        unsigned long  motion_buffer;
        XAxisInfoPtr   axes;
} XValuatorInfo;
typedef struct _XAxisInfo {
        int  resolution;
        int  min_value;
        int  max_value;
} XAxisInfo;
 *)


  PXValuatorInfo = ^TXValuatorInfo;
  TXValuatorInfo = record
    _class: TXID;
    length: cint;
    num_axes,
    mode: cuchar;
    motion_buffer: culong;
    axes: PXAxisInfo;
  end;

const
  ValuatorClass = 2;
  ModeAbsolute = 1;


//XDeviceInfo *XListInputDevices(Display *display, int  *ndevices_return);
//int XFreeDeviceList(XDeviceInfo *list);
  function XListInputDevices(display: pointer; ndevices_return: Pcint): PXDeviceInfo; cdecl; external libXi;
  function XFreeDeviceList(list: PXDeviceInfo): cint; cdecl; external libXi;
//XDevice *XOpenDevice(display, device_id) Display *display; XID device_id;
  function XOpenDevice(display: pointer; device_id: TXID): PXDevice; cdecl; external libXi;
  procedure XCloseDevice(display: pointer; device: PXDevice);  cdecl; external libXi;
{  int XSelectExtensionEvent(display, w, event_list, event_count)
      Display     *display;
      Window       w;
      XEventClass *event_list;
      int          event_count;        }
  function XSelectExtensionEvent(display, window, event_list: pointer; event_count: cint): cint;  cdecl; external libXi;


constructor TXinputTabletManager.Create(_display, _window: pointer);
var
  list, CurrentDevice: PXDeviceInfo;

  num_devices: cint;
  i, j, k: integer;
  DeviceName: WideString = '';
  pss: WideString = '';
  valuator: PXValuatorInfo;
  axis: PXAxisInfo;
  dclass: PXInputClassInfo;
  xeclass: cuint;
  DevNameList, DevDetailsList: TStringList;
  ExtendedDevicesFound: integer = 0;
  ChosenDeviceName: string = 'stylus';
begin
  ValuatorEvent:= -1;
  display:= _display;
  DeviceName:= RuEn('не найден','not found');
  list:= XListInputDevices(display, @num_devices);

  //It's all one HUGE hack. Behold the divination at work:
  CurrentDevice:= list;
  DevNameList:= TStringList.Create;
  DevDetailsList:= TStringList.Create;
  //first, fill the names list
  for i:=0 to num_devices - 1 do begin
    if CurrentDevice^.use = 3 then begin
      inc(ExtendedDevicesFound);
      DevNameList.Add(CurrentDevice^.name);
      DevDEtailsList.Add(PervertedFormat('(type=%0, id=%1)',[CurrentDevice^._type, CurrentDevice^.id]));
    end;
    inc(CurrentDevice);
  end;
  //second, report it
  if MotherState.VerboseLog then begin
    AddLog('Listed %0 extended input devices:',[ExtendedDevicesFound]);
    for i:=0 to ExtendedDevicesFound - 1 do begin
      AddLog('  "%0" %1', [DevNameList[i], DevDetailsList[i]]);
    end;
  end;
  //third, the true mumbo-jumbo with names:
  for i:=0 to ExtendedDevicesFound - 1 do
    for j:=0 to ExtendedDevicesFound - 1 do begin
      if (i <> j) and (length(DevNameList[i]) < length(DevNameList[j]))
      and (DevNameList[i] = copy(DevNameList[j], 1, length(DevNameList[i])))
      and ((pos('ERASER', UpperCase(copy(DevNameList[j], length(DevNameList[i]) + 1, length(DevNameList[j]) - length(DevNameList[i])))) > 0)
        or (pos('CURSOR', UpperCase(copy(DevNameList[j], length(DevNameList[i]) + 1, length(DevNameList[j]) - length(DevNameList[i])))) > 0))
      then begin
        //in short, if there's a device whose name consists of the currently viewed device name
        // with 'eraser' or 'cursor' added at the end, then this device is a
        // pen tablet. Damn Ubuntu Wacom driver, why couldn't they just keep
        // calling it 'stylus'? :(
        ChosenDeviceName:= DevNameList[i];
        break;
      end;
{      if (i <> j) and (length(DevNameList[i]) < length(DevNameList[j])) then AddLog('"%0" "%1" "%2"',[
        DevNameList[i],
        copy(DevNameList[j], 1, length(DevNameList[i])),
        copy(DevNameList[j], length(DevNameList[i]), length(DevNameList[j]) - length(DevNameList[i]))
        ])  }
    end;
  DevNameList.Free;
  DevDetailsList.Free;

  ErrorMessage:= PervertedFormat(RuEn(
    'ни одно из %0 расширенных устройств ввода не распознано как планшет',
    'none of the %0 extended inpud devices is detected as a tablet'),[ExtendedDevicesFound]);
  CurrentDevice:= list;
  for i:=0 to num_devices - 1 do begin
    if  CurrentDevice^.use = 3 then begin
      if CurrentDevice^.name = ChosenDeviceName then begin
        if MotherState.VerboseLog then AddLog('Gypsy says device "%0" is the tablet we''re searching for.',[ChosenDeviceName]);
        if ChosenDeviceName = 'stylus'
          then DeviceName:= RuEn('Стандартное устройство ввода XInput','Generic XInput device')
          else DeviceName:= ChosenDeviceName;
        ErrorMessage:= RuEn('нельзя использовать т.к. не найдены параметры осей пера (сбойный драйвер/настройки X сервера?)',
                            'cannot use it as there is no axes info (a faulty driver | X settings?)');
         //browsing the classes to find valuator.
        valuator:= CurrentDevice^.inputclassinfo;
        for j:=1 to CurrentDevice^.num_classes do begin
          if MotherState.VerboseLog then AddLog('  class=%0 length=%1',[valuator^._class, valuator^.length]);
          if valuator^._class = ValuatorClass then begin //it's the class we need
            if MotherState.VerboseLog then AddLog('    num_axes=%0 mode=%1 motion_buffer=%2',[valuator^.num_axes, valuator^.mode, valuator^.motion_buffer]);
            ErrorMessage:= '';
            if valuator^.mode <> ModeAbsolute then ErrorMessage+= RuEn(
              'нельзя использовать, т.к. перо работает в относительном режиме. Переключите в абсолютный (в настройках драйвера)  ',
              'cannot use it as the pen mode is set to relative. Switch it to absolute (see the driver settings)  ');
            //browsing the axes
            axis:= valuator^.axes;
            SetLength(AxisInfo, valuator^.num_axes);
            for k:=0 to valuator^.num_axes - 1 do begin
              AxisInfo[k]:= axis^;
              if MotherState.VerboseLog then AddLog('    axis#%0: min=%1 max=%2 resolution=%3',[k,AxisInfo[k].min_value,AxisInfo[k].max_value,AxisInfo[k].resolution]);
              inc(axis);
            end;
            if (Length(AxisInfo) < 3) or (AxisInfo[2].min_value <> 0) or (AxisInfo[2].max_value < 32) then ErrorMessage+= RuEn(
              'нельзя использовать, т.к. отсутствует чувствительность к нажатию',
              'cannot use it as it has no pressure sensitivity')
            else begin
              PressureRange:= AxisInfo[2].max_value - AxisInfo[2].min_value + 1;
              pss:= PervertedFormat(RuEn(
              '%0 градаций чувствительности к нажатию',
              'pressure sensitivity range %0'), [PressureRange]);
              MotherState.PenTabletPressureResolution:= PressureRange;
            end;
            if MotherState.VerboseLog then AddLog('Opening the stylus device...');
            device:= XOpenDevice(display, CurrentDevice^.id);
            if not Assigned(device) then ErrorMessage+= RuEn(
              'Ошибка при открытии устройства',
              'Failed to open the device')
            else begin
              if MotherState.VerboseLog then AddLog('device_id=%0 num_classes=%1',[device^.device_id, device^.num_classes]);
              //browsing the classes
              dclass:= device^._classes;
              for k:=1 to device^.num_classes do begin
                if MotherState.VerboseLog then AddLog('  #%0 input_class=%1 event_type_base=%2',[k, dclass^.input_class, dclass^.event_type_base]);
                if dclass^.input_class = ValuatorClass then ValuatorEvent:= dclass^.event_type_base;
                inc(dclass);
              end;
              if MotherState.VerboseLog then AddLog('  event_base=%0',[ValuatorEvent]);
              if ValuatorEvent < 0 then ErrorMessage+= RuEn(
                'не могу найти базовое значение для расширенного события ValuatorClass ',
                'unable to find the event base for the ValuatorClass ')
              else begin
  {              I reverse ingeneer it here assuming XEventClass to be a C unsigned int.

                 I couldn't find  XEventClass description IN THE WHOLE FUCKING INTERNET! >:(
                 ..nor in the XInput.h and other headers.

                 And let me tell you one more thing. I HATE macros.

  }
  //XInput.h:
  //#define _deviceMotionNotify	0
  //
  //#define FindTypeAndClass(d,type,_class,classid,offset) \
  //    { int _i; XInputClassInfo *_ip; \
  //    type = 0; _class = 0; \
  //    for (_i=0, _ip= ((XDevice *) d)->classes; \
  //	 _i< ((XDevice *) d)->num_classes; \
  //	 _i++, _ip++) \
  //	if (_ip->input_class == classid) \
  //	    {type =  _ip->event_type_base + offset; \
  //	     _class =  ((XDevice *) d)->device_id << 8 | type;}}
  //
  //#define DeviceMotionNotify(d,type,_class) \
  //    FindTypeAndClass(d, type, _class, ValuatorClass, _deviceMotionNotify)

                 xeclass:= (device^.device_id shl 8) + ValuatorEvent;
                 XSelectExtensionEvent(_display, _window, @xeclass, 1);
                 Initialized:= true;
              end;
            end;
          end;
          pointer(valuator)+= cardinal(valuator^.length);
        end;
      end;
    end;
    inc(CurrentDevice);
  end;
  XFreeDeviceList(list);

  if ErrorMessage <> '' then begin
    pss:= ErrorMessage;
    Initialized:= false;
  end;
  AddLog(RuEn('Графический планшет: %0 (%1)','Pen tablet: %0 (%1)'), [DeviceName, pss]);
  if not Initialized then MotherState.PenTabletAbsenceReason:= ErrorMessage
                     else MotherState.PenTabletName:= DeviceName;
  MotherState.PenTabletPresent:= Initialized;
end;

destructor TXinputTabletManager.Destroy;
begin
  if Initialized then XCloseDevice(display, device);
end;


end.

