// XInput2 example for x11-rs
//
// This is a basic example showing how to use XInput2 to read
// keyboard, mouse and other input events in X11.
//
// See Pete Hutterer's "XI2 Recipes" blog series,
// starting at http://who-t.blogspot.co.uk/2009/05/xi2-recipes-part-1.html
// for a guide.

#![cfg_attr(not(feature = "xlib"), allow(dead_code))]
#![cfg_attr(not(feature = "xlib"), allow(unused_imports))]

extern crate x11;
extern crate libc;

use std::ffi::CString;
use std::ptr::{
  null,
  null_mut,
};
use std::mem::{transmute, zeroed};
use std::os::raw::*;
use std::slice::{from_raw_parts};
use x11::{xlib, xinput2};

/// Provides a basic framework for connecting to an X Display,
/// creating a window, displaying it and running the event loop
pub struct DemoWindow {
    pub display: *mut xlib::Display,
    pub window: xlib::Window,

    wm_protocols: xlib::Atom,
    wm_delete_window: xlib::Atom
}

impl DemoWindow {
    /// Create a new window with a given title and size
    pub fn new(title: &str, width: u32, height: u32) -> DemoWindow {
        unsafe {
            // Open display
            let display = xlib::XOpenDisplay(null());
            if display == null_mut() {
                panic!("can't open display");
            }

            // Load atoms
            let wm_delete_window_str = CString::new("WM_DELETE_WINDOW").unwrap();
            let wm_protocols_str = CString::new("WM_PROTOCOLS").unwrap();

            let wm_delete_window = xlib::XInternAtom(display, wm_delete_window_str.as_ptr(), xlib::False);
            let wm_protocols = xlib::XInternAtom(display, wm_protocols_str.as_ptr(), xlib::False);

            if wm_delete_window == 0 || wm_protocols == 0 {
                panic!("can't load atoms");
            }

            // Create window
            let screen_num = xlib::XDefaultScreen(display);
            let root = xlib::XRootWindow(display, screen_num);
            let white_pixel = xlib::XWhitePixel(display, screen_num);

            let mut attributes: xlib::XSetWindowAttributes = zeroed();
            attributes.background_pixel = white_pixel;

            let window = xlib::XCreateWindow(display, root, 0, 0, width as c_uint, height as c_uint, 0, 0,
                                             xlib::InputOutput as c_uint, null_mut(),
                                             xlib::CWBackPixel, &mut attributes);
            // Set window title
            let title_str = CString::new(title).unwrap();
            xlib::XStoreName(display, window, title_str.as_ptr() as *mut _);

            // Subscribe to delete (close) events
            let mut protocols = [wm_delete_window];

            if xlib::XSetWMProtocols(display, window, &mut protocols[0] as *mut xlib::Atom, 1) == xlib::False {
                panic!("can't set WM protocols");
            }

            DemoWindow{
                display: display,
                window: window,
                wm_protocols: wm_protocols,
                wm_delete_window: wm_delete_window
            }
        }
    }

    /// Display the window
    pub fn show(&mut self) {
        unsafe {
            xlib::XMapWindow(self.display, self.window);
        }
    }

    /// Process events for the window. Window close events are handled automatically,
    /// other events are passed on to |event_handler|
    pub fn run_event_loop<EventHandler>(&mut self, mut event_handler: EventHandler)
        where EventHandler: FnMut(&xlib::XEvent) {
            let mut event: xlib::XEvent = unsafe{zeroed()};
            loop {
                unsafe{xlib::XNextEvent(self.display, &mut event)};
                match event.get_type() {
                    xlib::ClientMessage => {
                        let xclient: xlib::XClientMessageEvent = From::from(event);

                        // WM_PROTOCOLS client message
                        if xclient.message_type == self.wm_protocols && xclient.format == 32 {
                            let protocol = xclient.data.get_long(0) as xlib::Atom;

                            // WM_DELETE_WINDOW (close event)
                            if protocol == self.wm_delete_window {
                                break;
                            }
                        }
                    },
                    _ => event_handler(&event)
                }
            }
        }
}

impl Drop for DemoWindow {
    /// Destroys the window and disconnects from the display
    fn drop(&mut self) {
        unsafe {
            xlib::XDestroyWindow(self.display, self.window);
            xlib::XCloseDisplay(self.display);
        }
    }
}

const TITLE: &'static str = "XInput Demo";
const DEFAULT_WIDTH: c_uint = 640;
const DEFAULT_HEIGHT: c_uint = 480;

#[derive(Debug)]
enum AxisType {
    HorizontalScroll,
    VerticalScroll,
    Other
}

#[derive(Debug)]
struct Axis {
    id: i32,
    device_id: i32,
    axis_number: i32,
    axis_type: AxisType
}

#[derive(Debug)]
struct AxisValue {
    device_id: i32,
    axis_number: i32,
    value: f64
}

struct InputState {
    cursor_pos: (f64, f64),
    axis_values: Vec<AxisValue>
}

fn read_input_axis_info(display: *mut xlib::Display) -> Vec<Axis> {
    let mut axis_list = Vec::new();
    let mut device_count = 0;

    // only get events from the master devices which are 'attached'
    // to the keyboard or cursor
    let devices = unsafe{xinput2::XIQueryDevice(display, xinput2::XIAllMasterDevices, &mut device_count)};
    for i in 0..device_count {
        let device = unsafe { *(devices.offset(i as isize)) };
        for k in 0..device.num_classes {
            let class = unsafe { *(device.classes.offset(k as isize)) };
            match unsafe { (*class)._type } {
                xinput2::XIScrollClass => {
                    let scroll_class: &xinput2::XIScrollClassInfo = unsafe{transmute(class)};
                    axis_list.push(Axis{
                        id: scroll_class.sourceid,
                        device_id: device.deviceid,
                        axis_number: scroll_class.number,
                        axis_type: match scroll_class.scroll_type {
                            xinput2::XIScrollTypeHorizontal => AxisType::HorizontalScroll,
                            xinput2::XIScrollTypeVertical => AxisType::VerticalScroll,
                            _ => { unreachable!() }
                        }
                    })
                },
                xinput2::XIValuatorClass => {
                    let valuator_class: &xinput2::XIValuatorClassInfo = unsafe{transmute(class)};
                    axis_list.push(Axis{
                        id: valuator_class.sourceid,
                        device_id: device.deviceid,
                        axis_number: valuator_class.number,
                        axis_type: AxisType::Other
                    })
                },
                _ => { /* TODO */ }
            }
        }
    }

    axis_list.sort_by(|a, b| {
        if a.device_id != b.device_id {
            a.device_id.cmp(&b.device_id)
        } else if a.id != b.id {
            a.id.cmp(&b.id)
        } else {
            a.axis_number.cmp(&b.axis_number)
        }
    });
    axis_list
}

/// Given an input motion event for an axis and the previous
/// state of the axises, return the horizontal/vertical
/// scroll deltas
fn calc_scroll_deltas(event: &xinput2::XIDeviceEvent,
                     axis_id: i32,
                     axis_value: f64,
                     axis_list: &[Axis],
                     prev_axis_values: &mut Vec<AxisValue>) -> (f64, f64) {
    let prev_value_pos = prev_axis_values.iter().position(|prev_axis| {
        prev_axis.device_id == event.sourceid &&
            prev_axis.axis_number == axis_id
    });
    let delta = match prev_value_pos {
        Some(idx) => axis_value - prev_axis_values[idx].value,
        None => 0.0
    };

    let new_axis_value = AxisValue{
        device_id: event.sourceid,
        axis_number: axis_id,
        value: axis_value
    };

    match prev_value_pos {
        Some(idx) => prev_axis_values[idx] = new_axis_value,
        None => prev_axis_values.push(new_axis_value)
    }

    let mut scroll_delta = (0.0, 0.0);

    for axis in axis_list.iter() {
        if axis.id == event.sourceid &&
            axis.axis_number == axis_id {
                match axis.axis_type {
                    AxisType::HorizontalScroll => scroll_delta.0 = delta,
                    AxisType::VerticalScroll => scroll_delta.1 = delta,
                    _ => {}
                }
            }
    }

    scroll_delta
}

#[cfg(not(all(feature = "xlib", feature = "xinput")))]
fn main () {
    panic!("this example requires `--features 'xlib xinput'`");
}

#[cfg(all(feature = "xlib", feature = "xinput"))]
fn main () {
    let mut demo_window = DemoWindow::new(TITLE, DEFAULT_WIDTH, DEFAULT_HEIGHT);

    // query XInput support
    let mut opcode: c_int = 0;
    let mut event: c_int = 0;
    let mut error: c_int = 0;
    let xinput_str = CString::new("XInputExtension").unwrap();
    let xinput_available = unsafe {
        xlib::XQueryExtension(demo_window.display, xinput_str.as_ptr(), &mut opcode, &mut event, &mut error)
    };
    if xinput_available == xlib::False {
        panic!("XInput not available")
    }

    let mut xinput_major_ver = xinput2::XI_2_Major;
    let mut xinput_minor_ver = xinput2::XI_2_Minor;
    if unsafe{xinput2::XIQueryVersion(demo_window.display,
      &mut xinput_major_ver, &mut xinput_minor_ver)} != xlib::Success as c_int {
        panic!("XInput2 not available");
    }
    println!("XI version available {}.{}", xinput_major_ver, xinput_minor_ver);

    // init XInput events
    let mut mask: [c_uchar; 1] = [0];
    let mut input_event_mask = xinput2::XIEventMask {
        deviceid: xinput2::XIAllMasterDevices,
        mask_len: mask.len() as i32,
        mask: mask.as_mut_ptr()
    };
    let events = &[
        xinput2::XI_ButtonPress,
        xinput2::XI_ButtonRelease,
        xinput2::XI_KeyPress,
        xinput2::XI_KeyRelease,
        xinput2::XI_Motion
    ];
    for &event in events {
        xinput2::XISetMask(&mut mask, event);
    }

    match unsafe{xinput2::XISelectEvents(demo_window.display,
      demo_window.window, &mut input_event_mask, 1)} {
        status if status as u8 == xlib::Success => (),
        err => panic!("Failed to select events {:?}", err)
    }

    // Show window
    demo_window.show();

    // Main loop
    let display = demo_window.display;
    let axis_list = read_input_axis_info(display);

    let mut prev_state = InputState{
        cursor_pos: (0.0, 0.0),
        axis_values: Vec::new()
    };

    demo_window.run_event_loop(|event| {
        match event.get_type() {
            xlib::GenericEvent => {
                let mut cookie: xlib::XGenericEventCookie = From::from(*event);
                if unsafe{xlib::XGetEventData(display, &mut cookie)} != xlib::True {
                    println!("Failed to retrieve event data");
                    return;
                }
                match cookie.evtype {
                    xinput2::XI_KeyPress | xinput2::XI_KeyRelease => {
                        let event_data: &xinput2::XIDeviceEvent = unsafe{transmute(cookie.data)};
                        if cookie.evtype == xinput2::XI_KeyPress {
                            if event_data.flags & xinput2::XIKeyRepeat == 0 {
                                println!("Key {} pressed", event_data.detail);
                            }
                        } else {
                            println!("Key {} released", event_data.detail);
                        }
                    },
                    xinput2::XI_ButtonPress | xinput2::XI_ButtonRelease => {
                        let event_data: &xinput2::XIDeviceEvent = unsafe{transmute(cookie.data)};
                        if cookie.evtype == xinput2::XI_ButtonPress {
                            println!("Button {} pressed", event_data.detail);
                        } else {
                            println!("Button {} released", event_data.detail);
                        }
                    },
                    xinput2::XI_Motion => {
                        let event_data: &xinput2::XIDeviceEvent = unsafe{transmute(cookie.data)};
                        let axis_state = event_data.valuators;
                        let mask = unsafe{ from_raw_parts(axis_state.mask, axis_state.mask_len as usize) };
                        let mut axis_count = 0;

                        let mut scroll_delta = (0.0, 0.0);
                        for axis_id in 0..axis_state.mask_len {
                            if xinput2::XIMaskIsSet(&mask, axis_id) {
                                let axis_value = unsafe{*axis_state.values.offset(axis_count)};
                                let delta = calc_scroll_deltas(event_data, axis_id, axis_value, &axis_list, &mut prev_state.axis_values);
                                scroll_delta.0 += delta.0;
                                scroll_delta.1 += delta.1;
                                axis_count += 1;
                            }
                        }

                        if scroll_delta.0.abs() > 0.0 || scroll_delta.1.abs() > 0.0 {
                            println!("Mouse wheel/trackpad scrolled by ({}, {})", scroll_delta.0, scroll_delta.1);
                        }

                        let new_cursor_pos = (event_data.event_x, event_data.event_y);
                        if new_cursor_pos != prev_state.cursor_pos {
                            println!("Mouse moved to ({}, {})", new_cursor_pos.0, new_cursor_pos.1);
                            prev_state.cursor_pos = new_cursor_pos;
                        }
                    },
                    _ => ()
                }
                unsafe{xlib::XFreeEventData(display, &mut cookie)};
            },
            _ => ()
        }
    });
}
