// Currently there is only X11 Selections support and no way to handle Wayland
// but since Wayland isn't extremely adopted, we are covering almost all Linux distros.
module x11

import time
import sync

$if freebsd {
	#flag -I/usr/local/include
	#flag -L/usr/local/lib
} $else $if openbsd {
	#flag -I/usr/X11R6/include
	#flag -L/usr/X11R6/lib
}
#flag -lX11

#include <X11/Xlib.h> # Please install a package with the X11 development headers, for example: `apt install libx11-dev`
// X11

@[typedef]
pub struct C.Display {}

fn (d &C.Display) str() string {
	return 'C.Display{}'
}

type Window = u64
type Atom = u64

fn C.XInitThreads() int

fn C.XCloseDisplay(d &C.Display)

fn C.XFlush(d &C.Display)

fn C.XDestroyWindow(d &C.Display, w Window)

fn C.XNextEvent(d &C.Display, e &C.XEvent)

fn C.XSetSelectionOwner(d &C.Display, a Atom, w Window, time int)

fn C.XGetSelectionOwner(d &C.Display, a Atom) Window

fn C.XChangeProperty(d &C.Display, requestor Window, property Atom, typ Atom, format int, mode int, data voidptr,
	nelements int) int

fn C.XSendEvent(d &C.Display, requestor Window, propagate int, mask i64, event &C.XEvent)

fn C.XInternAtom(d &C.Display, typ &u8, only_if_exists int) Atom

fn C.XCreateSimpleWindow(d &C.Display, root Window, x int, y int, width u32, height u32, border_width u32,
	border u64, background u64) Window

fn C.XOpenDisplay(name &u8) &C.Display

fn C.XConvertSelection(d &C.Display, selection Atom, target Atom, property Atom, requestor Window, time int) int

fn C.XSync(d &C.Display, discard int) int

fn C.XGetWindowProperty(d &C.Display, w Window, property Atom, offset i64, length i64, delete int, req_type Atom,
	actual_type_return &Atom, actual_format_return &int, nitems &u64, bytes_after_return &u64, prop_return &&u8) int

fn C.XDeleteProperty(d &C.Display, w Window, property Atom) int

fn C.DefaultScreen(display &C.Display) int

fn C.RootWindow(display &C.Display, screen_number int) Window

fn C.BlackPixel(display &C.Display, screen_number int) u32

fn C.WhitePixel(display &C.Display, screen_number int) u32

fn C.XFree(data voidptr)

fn todo_del() {}

@[typedef]
pub struct C.XSelectionRequestEvent {
mut:
	display   &C.Display = unsafe { nil } // Display the event was read from
	owner     Window
	requestor Window
	selection Atom
	target    Atom
	property  Atom
	time      int
}

@[typedef]
pub struct C.XSelectionEvent {
mut:
	type      int
	display   &C.Display = unsafe { nil } // Display the event was read from
	requestor Window
	selection Atom
	target    Atom
	property  Atom
	time      int
}

@[typedef]
pub struct C.XSelectionClearEvent {
mut:
	window    Window
	selection Atom
}

@[typedef]
pub struct C.XDestroyWindowEvent {
mut:
	window Window
}

@[typedef]
union C.XEvent {
mut:
	type              int
	xdestroywindow    C.XDestroyWindowEvent
	xselectionclear   C.XSelectionClearEvent
	xselectionrequest C.XSelectionRequestEvent
	xselection        C.XSelectionEvent
}

const atom_names = ['TARGETS', 'CLIPBOARD', 'PRIMARY', 'SECONDARY', 'TEXT', 'UTF8_STRING',
	'text/plain', 'text/html']

// UNSUPPORTED TYPES: MULTIPLE, INCR, TIMESTAMP, image/bmp, image/jpeg, image/tiff, image/png
// all the atom types we need
// currently we only support text
// in the future, maybe we can extend this
// to support other mime types
enum AtomType {
	xa_atom     = 0 // value 4
	xa_string   = 1 // value 31
	targets     = 2
	clipboard   = 3
	primary     = 4
	secondary   = 5
	text        = 6
	utf8_string = 7
	text_plain  = 8
	text_html   = 9
}

@[heap]
pub struct Clipboard {
	display &C.Display = unsafe { nil }
mut:
	selection Atom // the selection atom
	window    Window
	atoms     []Atom
	mutex     &sync.Mutex = sync.new_mutex()
	text      string // text data sent or received
	got_text  bool   // used to confirm that we have got the text
	is_owner  bool   // to save selection owner state
}

struct Property {
	actual_type   Atom
	actual_format int
	nitems        u64
	data          &u8 = unsafe { nil }
}

// new_clipboard returns a new `Clipboard` instance allocated on the heap.
// The `Clipboard` resources can be released with `free()`
pub fn new_clipboard() &Clipboard {
	return new_x11_clipboard(.clipboard)
}

// new_x11_clipboard initializes a new clipboard of the given selection type.
// Multiple clipboard instance types can be initialized and used separately.
fn new_x11_clipboard(selection AtomType) &Clipboard {
	if selection !in [.clipboard, .primary, .secondary] {
		panic('Wrong AtomType. Must be one of .primary, .secondary or .clipboard.')
	}
	// init x11 thread support
	status := C.XInitThreads()
	if status == 0 {
		println('WARN: this system does not support threads; clipboard will cause the program to lock.')
	}

	display := new_display()

	if display == C.NULL {
		println('ERROR: No X Server running. Clipboard cannot be used.')
		return &Clipboard{
			display: unsafe { nil }
			mutex:   sync.new_mutex()
		}
	}

	mut cb := &Clipboard{
		display: display
		window:  create_xwindow(display)
		mutex:   sync.new_mutex()
	}
	cb.intern_atoms()
	cb.selection = cb.get_atom(selection)
	// start the listener on another thread or
	// we will be locked and will have to hard exit
	spawn cb.start_listener()
	return cb
}

// check_availability returns `true` if the clipboard is available for use.
pub fn (cb &Clipboard) check_availability() bool {
	return cb.display != C.NULL
}

// free releases the clipboard resources.
pub fn (mut cb Clipboard) free() {
	C.XDestroyWindow(cb.display, cb.window)
	cb.window = Window(0)
	// FIXME: program hangs when closing display
	// XCloseDisplay(cb.display)
}

// clear clears the clipboard (sets it to an empty string).
pub fn (mut cb Clipboard) clear() {
	cb.mutex.lock()
	C.XSetSelectionOwner(cb.display, cb.selection, Window(0), C.CurrentTime)
	C.XFlush(cb.display)
	cb.is_owner = false
	cb.text = ''
	cb.mutex.unlock()
}

// has_ownership returns `true` if the `Clipboard` has the content ownership.
pub fn (cb &Clipboard) has_ownership() bool {
	return cb.is_owner
}

fn (cb &Clipboard) take_ownership() {
	C.XSetSelectionOwner(cb.display, cb.selection, cb.window, C.CurrentTime)
	C.XFlush(cb.display)
}

// set_text stores `text` in the system clipboard.
pub fn (mut cb Clipboard) set_text(text string) bool {
	if cb.window == Window(0) {
		return false
	}
	cb.mutex.lock()
	cb.text = text
	cb.is_owner = true
	cb.take_ownership()
	C.XFlush(cb.display)
	cb.mutex.unlock()
	// sleep a little bit
	time.sleep(1 * time.millisecond)
	return cb.is_owner
}

// get_text returns the current entry as a `string` from the clipboard.
pub fn (mut cb Clipboard) get_text() string {
	if cb.window == Window(0) {
		return ''
	}
	if cb.is_owner {
		return cb.text
	}
	cb.got_text = false

	// Request a list of possible conversions, if we're pasting.
	C.XConvertSelection(cb.display, cb.selection, cb.get_atom(.targets), cb.selection,
		cb.window, C.CurrentTime)

	// wait for the text to arrive
	mut retries := 5
	for {
		if cb.got_text || retries == 0 {
			break
		}
		time.sleep(50 * time.millisecond)
		retries--
	}
	return cb.text
}

// transmit_selection is crucial to handling all the different data types.
// If we ever support other mimetypes they should be handled here.
fn (mut cb Clipboard) transmit_selection(xse &C.XSelectionEvent) bool {
	if xse.target == cb.get_atom(.targets) {
		targets := cb.get_supported_targets()
		C.XChangeProperty(xse.display, xse.requestor, xse.property, cb.get_atom(.xa_atom),
			32, C.PropModeReplace, targets.data, targets.len)
	} else if cb.is_supported_target(xse.target) && cb.is_owner && cb.text != '' {
		cb.mutex.lock()
		C.XChangeProperty(xse.display, xse.requestor, xse.property, xse.target, 8, C.PropModeReplace,
			cb.text.str, cb.text.len)
		cb.mutex.unlock()
	} else {
		return false
	}
	return true
}

fn (mut cb Clipboard) start_listener() {
	event := C.XEvent{}
	mut sent_request := false
	mut to_be_requested := Atom(0)
	for {
		time.sleep(1 * time.millisecond)
		C.XNextEvent(cb.display, &event)
		if unsafe { event.type == 0 } {
			println('error')
			continue
		}
		match unsafe { event.type } {
			C.DestroyNotify {
				if unsafe { event.xdestroywindow.window == cb.window } {
					// we are done
					return
				}
			}
			C.SelectionClear {
				if unsafe { event.xselectionclear.window == cb.window } && unsafe {
					event.xselectionclear.selection == cb.selection
				} {
					cb.mutex.lock()
					cb.is_owner = false
					cb.text = ''
					cb.mutex.unlock()
				}
			}
			C.SelectionRequest {
				if unsafe { event.xselectionrequest.selection == cb.selection } {
					mut xsre := &C.XSelectionRequestEvent{
						display: 0
					}
					xsre = unsafe { &event.xselectionrequest }

					mut xse := C.XSelectionEvent{
						type:      C.SelectionNotify // 31
						display:   xsre.display
						requestor: xsre.requestor
						selection: xsre.selection
						time:      xsre.time
						target:    xsre.target
						property:  xsre.property
					}
					if !cb.transmit_selection(&xse) {
						xse.property = Atom(0)
					}
					C.XSendEvent(cb.display, xse.requestor, 0, C.PropertyChangeMask, voidptr(&xse))
					C.XFlush(cb.display)
				}
			}
			C.SelectionNotify {
				if unsafe {
					event.xselection.selection == cb.selection
						&& event.xselection.property != Atom(0)
				} {
					if unsafe { event.xselection.target == cb.get_atom(.targets) && !sent_request } {
						sent_request = true
						prop := read_property(cb.display, cb.window, cb.selection)
						to_be_requested = cb.pick_target(prop)
						if to_be_requested != Atom(0) {
							C.XConvertSelection(cb.display, cb.selection, to_be_requested,
								cb.selection, cb.window, C.CurrentTime)
						}
					} else if unsafe { event.xselection.target == to_be_requested } {
						sent_request = false
						to_be_requested = Atom(0)
						cb.mutex.lock()
						prop := unsafe {
							read_property(event.xselection.display, event.xselection.requestor,
								event.xselection.property)
						}
						unsafe {
							C.XDeleteProperty(event.xselection.display, event.xselection.requestor,
								event.xselection.property)
						}
						if cb.is_supported_target(prop.actual_type) {
							cb.got_text = true
							unsafe {
								cb.text = prop.data.vstring() // TODO: return byteptr to support other mimetypes
							}
						}
						cb.mutex.unlock()
					}
				}
			}
			C.PropertyNotify {}
			else {}
		}
	}
}

/*
* Helpers
*/
// intern_atoms initializes all the atoms we need.
fn (mut cb Clipboard) intern_atoms() {
	cb.atoms << Atom(4) // XA_ATOM
	cb.atoms << Atom(31) // XA_STRING
	for i, name in atom_names {
		only_if_exists := if i == int(AtomType.utf8_string) { 1 } else { 0 }
		cb.atoms << C.XInternAtom(cb.display, &char(name.str), only_if_exists)
		if i == int(AtomType.utf8_string) && cb.atoms[i] == Atom(0) {
			cb.atoms[i] = cb.get_atom(.xa_string)
		}
	}
}

fn read_property(d &C.Display, w Window, p Atom) Property {
	actual_type := Atom(0)
	actual_format := 0
	nitems := u64(0)
	bytes_after := u64(0)
	ret := &u8(unsafe { nil })
	mut read_bytes := 1024
	for {
		if ret != 0 {
			C.XFree(ret)
		}
		C.XGetWindowProperty(d, w, p, 0, read_bytes, 0, 0, &actual_type, &actual_format,
			&nitems, &bytes_after, &ret)
		read_bytes *= 2
		if bytes_after == 0 {
			break
		}
	}
	return Property{actual_type, actual_format, nitems, ret}
}

// pick_target finds the best target given a local copy of a property.
fn (cb &Clipboard) pick_target(prop Property) Atom {
	// The list of targets is a list of atoms, so it should have type XA_ATOM
	// but it may have the type TARGETS instead.
	if (prop.actual_type != cb.get_atom(.xa_atom) && prop.actual_type != cb.get_atom(.targets))
		|| prop.actual_format != 32 {
		// This would be really broken. Targets have to be an atom list
		// and applications should support this. Nevertheless, some
		// seem broken (MATLAB 7, for instance), so ask for STRING
		// next instead as the lowest common denominator
		return cb.get_atom(.xa_string)
	} else {
		atom_list := &Atom(voidptr(prop.data))

		mut to_be_requested := Atom(0)

		// This is higher than the maximum priority.
		mut priority := int(max_i32)

		for i in 0 .. prop.nitems {
			// See if this data type is allowed and of higher priority (closer to zero)
			// than the present one.

			target := unsafe { atom_list[i] }
			if cb.is_supported_target(target) {
				index := cb.get_target_index(target)
				if priority > index && index >= 0 {
					priority = index
					to_be_requested = target
				}
			}
		}
		return to_be_requested
	}
}

fn (cb &Clipboard) get_atoms(types ...AtomType) []Atom {
	mut atoms := []Atom{}
	for typ in types {
		atoms << cb.atoms[typ]
	}
	return atoms
}

fn (cb &Clipboard) get_atom(typ AtomType) Atom {
	return cb.atoms[typ]
}

fn (cb &Clipboard) is_supported_target(target Atom) bool {
	return cb.get_target_index(target) >= 0
}

fn (cb &Clipboard) get_target_index(target Atom) int {
	for i, atom in cb.get_supported_targets() {
		if atom == target {
			return i
		}
	}
	return -1
}

fn (cb &Clipboard) get_supported_targets() []Atom {
	return cb.get_atoms(AtomType.utf8_string, .xa_string, .text, .text_plain, .text_html)
}

fn create_xwindow(display &C.Display) Window {
	n := C.DefaultScreen(display)
	return C.XCreateSimpleWindow(display, C.RootWindow(display, n), 0, 0, 1, 1, 0, C.BlackPixel(display,
		n), C.WhitePixel(display, n))
}

fn new_display() &C.Display {
	return C.XOpenDisplay(C.NULL)
}

// new_primary returns a new X11 `PRIMARY` type `Clipboard` instance allocated on the heap.
// Please note: new_primary only works on X11 based systems.
pub fn new_primary() &Clipboard {
	return new_x11_clipboard(.primary)
}
