/*
 * Copyright © 2008 Keith Packard
 *
 * This library is free software; you can redistribute it and/or
 * modify it either under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation
 * (the "LGPL") or, at your option, under the terms of the Mozilla
 * Public License Version 1.1 (the "MPL"). If you do not alter this
 * notice, a recipient may use your version of this file under either
 * the MPL or the LGPL.
 *
 * You should have received a copy of the LGPL along with this library
 * in the file COPYING-LGPL-2.1; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 * You should have received a copy of the MPL along with this library
 * in the file COPYING-MPL-1.1
 *
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY
 * OF ANY KIND, either express or implied. See the LGPL or the MPL for
 * the specific language governing rights and limitations.
 *
 * The Original Code is the cairo graphics library.
 *
 * The Initial Developer of the Original Code is Keith Packard
 *
 * Contributor(s):
 *      Keith Packard <keithp@keithp.com>
 */
autoload Cairo;
autoimport Mutex;

namespace Nichrome {
    import Cairo;
    
    /*
     * Event types
     */
    public typedef enum { press, release, double, triple } button_type_t;
    
    public typedef struct {
	int		time;
	real		x;
	real		y;
    } pos_event_t;

    public typedef pos_event_t + struct {
	button_type_t	type;
	int		button;
    } button_event_t;

    public typedef enum { inside, outside, entering, leaving } motion_type_t;
    
    public typedef pos_event_t + struct {
	motion_type_t	type;
    } motion_event_t;
    
    public typedef enum { press, release } key_type_t;
    
    public typedef struct {
	key_type_t	type;
	int		time;
	string		key;
	string		text;
    } key_event_t;
    
    public typedef nichrome_t;
    public typedef widget_t;
    public typedef container_t;
    public typedef contained_t;

    /*
     * Containers, as seen by the toolkit
     */

    public typedef void(&container_t, &contained_t) resize_handler_t;

    public typedef void(&contained_t, rect_t) configure_handler_t;

    public typedef struct {
	resize_handler_t    resize;
    } container_t;

    public typedef void(cairo_t cr, &widget_t widget) draw_t;
    public typedef void(&widget_t, &button_event_t) button_handler_t;
    public typedef void(&widget_t, &motion_event_t) motion_handler_t;
    public typedef void(&widget_t, &key_event_t) key_handler_t;
    public typedef void(&widget_t, bool focus) focus_handler_t;
    public typedef void(&contained_t) remove_handler_t;
    public typedef void(&poly object, int indent) print_handler_t;
    public typedef bool(&key_event_t) global_key_handler_t;

    public void do_indent (int indent) {
	while (indent-- > 0)
	    printf(" ");
    }

    public typedef struct {
	&container_t	    container;
	configure_handler_t configure;
	print_handler_t	    print;
	remove_handler_t    remove;
	bool		    active;
    } contained_t;

    /*
     * Widgets, as seen by the toolkit
     */

    public typedef contained_t + struct {
	&nichrome_t	    nichrome;
	rect_t		    geometry;
	rect_t		    extents;
	draw_t		    outline;
	draw_t		    draw;
	draw_t		    natural;
	button_handler_t    button;
	motion_handler_t    motion;
	key_handler_t	    key;
	focus_handler_t	    focus;
    } widget_t;

    /*
     * An application surface
     */
    public typedef void(&nichrome_t nichrome) callback_t;

    public typedef struct {
	surface_t	    surface;
	file		    event;
	bool		    draw;
	bool		    resize;
	bool		    shown;
	bool		    want_shown;
	point_t		    scale;
	(&widget_t)[...]    widgets;
	&widget_t	    key_focus;
	&widget_t	    mouse_grab;
	int		    button_down;
	&widget_t	    mouse_in;
	callback_t	    destroy;
	callback_t	    configure;
	cairo_t		    outline_cr;
	&widget_t	    outline_widget;
	bool		    running;
	mutex		    drawing;
	thread		    self;
	int		    suspend_draw;
	global_key_handler_t	global_key;
    } nichrome_t;

    public widget_t no_widget;

    public cairo_t cairo (&nichrome_t nichrome)
    {
	cairo_t cr = create (nichrome.surface);
	scale (cr, nichrome.scale.x, nichrome.scale.y);
	return cr;
    }
    
    public void add (&nichrome_t nichrome, &widget_t widget) {
	&nichrome.widgets[dim(nichrome.widgets)] = &widget;
	nichrome.draw = true;
    }

    public exception unknown_widget (&nichrome_t nichrome, &widget_t widget);
    
    int widget_pos (&nichrome_t nichrome, &widget_t widget) {
	for (int i = 0; i < dim(nichrome.widgets); i++)
	    if (&nichrome.widgets[i] == &widget)
		return i;
	raise unknown_widget (&nichrome, &widget);
    }

    public void remove (&nichrome_t nichrome, &widget_t widget) {
	twixt (Mutex::acquire (nichrome.drawing);
	       Mutex::release (nichrome.drawing)) {
		for (int i = widget_pos (&nichrome, &widget); 
		     i < dim(nichrome.widgets) - 1;
		     i++)
		{
			&nichrome.widgets[i] = &nichrome.widgets[i + 1];
		}
		setdim (nichrome.widgets, dim(nichrome.widgets) - 1);
		nichrome.draw = true;
	}
    }

    void draw (&nichrome_t nichrome);
	
    public void redraw (&nichrome_t nichrome) {
	if (is_uninit (&nichrome.self))
	    return;
        if (Thread::current() != nichrome.self && nichrome.suspend_draw == 0)
	    draw (&nichrome);
	else
	    nichrome.draw = true;
    }

    public void suspend_draw(&nichrome_t nichrome) {
	++nichrome.suspend_draw;
    }

    public void release_draw(&nichrome_t nichrome) {
	assert (nichrome.suspend_draw > 0, "mismatched suspend_draw/release_draw calls");
	--nichrome.suspend_draw;
	if (!nichrome.running)
	    return;
	if (nichrome.suspend_draw == 0 && nichrome.draw && Thread::current() != nichrome.self)
	    draw(&nichrome);
    }

    public void set_active(&nichrome_t nichrome, &contained_t contained, bool active) {
	if (active != contained.active) {
	    contained.active = active;
	    redraw (&nichrome);
	}
    }

    public void set_global_key(&nichrome_t nichrome, global_key_handler_t global_key) {
	nichrome.global_key = global_key;
    }

    public namespace Widget {
	protected void configure (&widget_t widget, rect_t geometry) {
	    widget.geometry = geometry;
	}

	protected void natural (cairo_t cr, &widget_t widget) = ◊;

	protected void remove (&widget_t widget) {
	    Nichrome::remove (&widget.nichrome, &widget);
	}

	protected void print (&widget_t widget, int indent) {
	    do_indent(indent);
	    printf ("widget %v\n", widget.geometry);
	}

	protected void init (&nichrome_t nichrome, &widget_t widget) {
	    widget.geometry = (rect_t) {
		x = 0, y = 0, width = 0, height = 0
	    };
	    widget.extents = widget.geometry;
	    &widget.nichrome = &nichrome;
	    widget.configure = configure;
	    widget.natural = natural;
	    widget.outline = natural;
	    widget.draw = natural;
	    widget.remove = remove;
	    widget.print = print;
	    widget.active = true;
	    Nichrome::add (&nichrome, &widget);
	}
	
	protected void redraw (&widget_t widget) {
	    Nichrome::redraw (&widget.nichrome);
	}
	
	protected void resize (&widget_t widget) {
	    if (!is_uninit (&widget.container)) {
		if (widget.nichrome.drawing->owner != (mutex_owner.owner) Thread::current())
		{
		    twixt (Mutex::acquire (widget.nichrome.drawing);
			   Mutex::release (widget.nichrome.drawing))
			widget.container.resize (&widget.container, &widget);
		} else
			widget.container.resize (&widget.container, &widget);
	    } else
		redraw (&widget);
	}

	protected void reoutline (&widget_t widget) {
	    if (&widget.nichrome.outline_widget == &widget)
		&widget.nichrome.outline_widget = &no_widget;
	}

	protected bool in_widget (&widget_t widget, real x, real y)
	{
	    bool    in;

	    if (x < widget.extents.x || 
		widget.extents.x + widget.extents.width <= x)
		return false;
	    
	    if (y < widget.extents.y || 
		widget.extents.y + widget.extents.height <= y)
		return false;
	    
	    cairo_t cr = widget.nichrome.outline_cr;
	    
	    if (&widget.nichrome.outline_widget != &widget) {
		new_path (cr);
		save (cr);
		translate (cr, widget.geometry.x, widget.geometry.y);
		widget.outline (cr, &widget);
		restore (cr);
		&widget.nichrome.outline_widget = &widget;
	    }
	    in = in_fill (cr, x, y);
	    return in;
	}
	
	protected string default_font = "sans";
	protected rgba_color_t default_color = {
	    red = 0.6, green = 0.6,
	    blue = 0.6, alpha = 0.75
	};
    }

    public *widget_t xy_to_widget (&nichrome_t nichrome, real x, real y) {
	twixt (Mutex::acquire(nichrome.drawing); Mutex::release(nichrome.drawing)) {
	    point_t a = device_to_user (nichrome.outline_cr, (point_t) { x = x, y = y });
	    x = a.x; y = a.y;
	    for (int w = 0; w < dim (nichrome.widgets); w++) {
		&widget_t widget = &nichrome.widgets[w];
		if (widget.active && Widget::in_widget (&widget, x, y))
		    return &widget;
	    }
	}
	return &no_widget;
    }

    /*
     * As usual, mouse grabs make this more complicated than we'd like.
     *
     * nichrome.mouse_in tracks the widget containing the pointer.
     * nichrome.mouse_grab tracks the grabbing widget.*
     * nichrome.button_down tracks which buttons are down
     *
     * While grabbed, events are delivered only to the grab widget.
     * Otherwise, events are delivered to the containing widget.
     * Pressing any button grabs the mouse.
     * Releasing all buttons ungrabs the mouse. This can also
     * generate an entering motion event when the ungrab
     * occurs over another widget.
     */
    
    void deliver_button (&widget_t widget, button_event_t event)
    {
	if (!is_uninit (&widget.button)) {
	    event.x -= widget.geometry.x;
	    event.y -= widget.geometry.y;
	    widget.button (&widget, &event);
	}
    }

    void deliver_motion (&widget_t widget, motion_event_t event)
    {
	if (!is_uninit (&widget.motion)) {
	    event.x -= widget.geometry.x;
	    event.y -= widget.geometry.y;
	    widget.motion (&widget, &event);
	}
    }
    
    public void dispatch_button (&nichrome_t nichrome, &button_event_t event) {
	&widget_t   widget;

	if (&nichrome.mouse_grab != &no_widget)
	    &widget = &nichrome.mouse_grab;
	else {
	    &widget = &nichrome.mouse_in;
	    if (&widget == &no_widget)
		return;
	}

	enum switch (event.type) {
	case press:
	    &nichrome.mouse_grab = &widget;
	    nichrome.button_down |= 1 << event.button;
	    break;
	case release:
	    if (&nichrome.mouse_grab == &no_widget)
		return;
	    break;
	default:
	    break;
	}
	
	deliver_button (&widget, event);

	if (event.type == button_type_t.release)
	{
	    nichrome.button_down &= ~(1 << event.button);
	    if (nichrome.button_down == 0)
	    {
		&nichrome.mouse_grab = &no_widget;
		
		/*
		 * On ungrab, deliver an entering motion
		 * to the widget under the pointer
		 */
		if (&widget != &nichrome.mouse_in)
		    deliver_motion (&nichrome.mouse_in,
				    (motion_event_t) {
					type = motion_type_t.entering,
					time = event.time,
					x = event.x,
					y = event.y });
	    }
	}
    }

    public void dispatch_motion (&nichrome_t nichrome, &motion_event_t event) {
	&widget_t   in = xy_to_widget (&nichrome, event.x, event.y);

	if (&nichrome.mouse_grab == &no_widget) {
	    /*
	     * Without a mouse grab, deliver events to the containing widget
	     */
	    if (&in != &nichrome.mouse_in) {
		
		event.type = motion_type_t.leaving;
		deliver_motion (&nichrome.mouse_in, event);
		
		event.type = motion_type_t.entering;
	    } else
		event.type = motion_type_t.inside;
	    deliver_motion (&in, event);
	    
	} else {
	    /*
	     * With mouse grab, deliver events to the grab widget
	     */
	    if (&in != &nichrome.mouse_grab) {
		if (&nichrome.mouse_in == &nichrome.mouse_grab)
		    event.type = motion_type_t.leaving;
		else
		    event.type = motion_type_t.outside;
	    } else {
		if (&nichrome.mouse_in == &nichrome.mouse_grab)
		    event.type = motion_type_t.inside;
		else
		    event.type = motion_type_t.entering;
	    }
	    deliver_motion (&nichrome.mouse_grab, event);
	}
        &nichrome.mouse_in = &in;
    }

    public void dispatch_key (&nichrome_t nichrome, &key_event_t event) {
	if (!is_uninit(&nichrome.global_key))
	    if (nichrome.global_key(&event))
		return;

	&widget_t   widget = &nichrome.key_focus;

	if (&widget != &no_widget)
	    if (!is_uninit (&widget.key))
		widget.key (&widget, &event);
    }

    public void set_key_focus (&nichrome_t nichrome, &widget_t widget) {
	if (&widget != &nichrome.key_focus) {
	    if (!is_uninit (&nichrome.key_focus))
		if (!is_uninit(&nichrome.key_focus.focus))
		    nichrome.key_focus.focus(&nichrome.key_focus, false);
	    &nichrome.key_focus = &widget;
	    if (&widget != &no_widget)
		if (!is_uninit(&widget.focus))
		    widget.focus(&widget, true);
	}
    }

    string hex_utf8 (int[...] bytes)
    {
	string	s = "";
	for (int i = 0; i < dim (bytes); i++) {
	    int first = bytes[i];
	    int c;
	    int extra = 0;
	    if ((first & 0x80) == 0) {
		c = first;
	    } else if ((first & 0xc0) != 0xc0) {
		c = first;
	    } else {
		int mask = 0x40;
		while ((first & mask) != 0)
		{
		    extra++;
		    mask >>= 1;
		}
		c = first & (mask - 1);
		while (i < dim (bytes) && extra-- > 0)
		    c = c << 6 | (bytes[i++] & 0x3f);
	    }
	    s += String::new (c);
	}
	return s;
    }

    void draw (&nichrome_t nichrome) {
	if (Mutex::owner(nichrome.drawing) == (mutex_owner.owner) Thread::current()) {
	    nichrome.draw = true;
	    return;
	}
	twixt (Mutex::acquire (nichrome.drawing);
	       Mutex::release (nichrome.drawing))
	{
	    if (nichrome.shown != nichrome.want_shown) {
		Cairo::Surface::set_window_shown(nichrome.surface, nichrome.want_shown);
		nichrome.shown = nichrome.want_shown;
	    }
	    if (!nichrome.shown)
		return;

	    if (nichrome.resize) {
		nichrome.resize = false;
		Cairo::destroy (nichrome.outline_cr);
		nichrome.outline_cr = cairo (&nichrome);
		&nichrome.outline_widget = &no_widget;
		if (!is_uninit (&nichrome.configure))
		    nichrome.configure (&nichrome);
	    }
	    nichrome.draw = false;
	    cairo_t cr = cairo (&nichrome);
	    int	i;
    
	    disable (cr);
	    set_source_rgba (cr, 1, 1, 1, 1);
	    paint (cr);
	    for (i = dim (nichrome.widgets) - 1; i >= 0; i--)
	    {
		&widget_t	widget = &nichrome.widgets[i];

		if (!widget.active)
		    continue;

		if (!is_uninit (&widget.draw)) {
		    save (cr);
		    translate (cr, widget.geometry.x, widget.geometry.y);
		    widget.draw (cr, &widget);
		    restore (cr);
		    if (status (cr) != status_t.SUCCESS)
			printf ("draw status %g for %g\n",
				status (cr), &widget);
		}
		if (!is_uninit (&widget.outline)) {
		    save (cr);
		    translate (cr, widget.geometry.x, widget.geometry.y);
		    widget.outline (cr, &widget);
		    restore (cr);
		    widget.extents = fill_extents (cr);
		    new_path (cr);
		    if (status (cr) != status_t.SUCCESS)
			printf ("draw status %g for %g\n",
				status (cr), &widget);
		}
	    }
	    enable (cr);
	    destroy (cr);
	}
    }
    
    public void show (&nichrome_t nichrome) {
	nichrome.want_shown = true;
	if (!nichrome.shown)
	    redraw(&nichrome);
    }

    public void hide (&nichrome_t nichrome) {
	if (nichrome.shown)
	    Cairo::Surface::set_window_shown(nichrome.surface, false);
	nichrome.shown = nichrome.want_shown = false;
    }

    public void destroy (&nichrome_t nichrome) {
	Cairo::Surface::destroy (nichrome.surface);
	nichrome.running = false;
    }
    
    public *nichrome_t new (string name, real size ...) {
	nichrome_t nichrome;
	real width, height;

	switch (dim (size)) {
	case 0:	width = height = 300; break;
	case 1: width = height = size[0]; break;
	default: width = size[0], height = size[1]; break;
	}
	
	nichrome.surface = Cairo::Surface::create_window_hidden (name, width, height);

	nichrome.event = Cairo::Surface::open_event (nichrome.surface);
	nichrome.draw = false;
	nichrome.resize = true;
	nichrome.shown = false;
	nichrome.want_shown = true;
	nichrome.running = false;
	nichrome.widgets = ((&widget_t)[...]) {};
	nichrome.scale = (point_t) { x = 1, y = 1 };
	nichrome.destroy = destroy;
	&nichrome.key_focus = &no_widget;
	&nichrome.mouse_grab = &no_widget;
	nichrome.button_down = 0;
	&nichrome.mouse_in = &no_widget;
	&nichrome.outline_widget = &no_widget;
	nichrome.outline_cr = cairo (&nichrome);
	nichrome.drawing = Mutex::new ();
	nichrome.suspend_draw = 0;
	return &nichrome;
    }
    
    public void resize (&nichrome_t nichrome, real width, real height) {
	Cairo::Surface::resize_window(nichrome.surface, max(1, floor(width)), max(1, floor(height)));
    }

    public void top (&nichrome_t nichrome, &widget_t widget) {
	for (int i = widget_pos (&nichrome, &widget);
	     i > 0;
	     i--)
	{
	    &nichrome.widgets[i] = &nichrome.widgets[i-1];
	}
	&nichrome.widgets[0] = &widget;
	nichrome.draw = true;
    }
    
    public void main_loop (&nichrome_t nichrome) {
	
	nichrome.running = true;
	nichrome.draw = true;
	nichrome.self = Thread::current ();

	string event_line;

	semaphore has_event = Semaphore::new(0);
	semaphore read_event = Semaphore::new(0);
	
	void event_reader () {
	    for (;;) {
		event_line = File::fgets (nichrome.event);
		Semaphore::signal (has_event);
		Semaphore::wait (read_event);
	    }
	}
	
	thread event_thread = fork event_reader ();

	while (nichrome.running) {
	    if (nichrome.draw)
		if (Semaphore::count (has_event) == 0)
		    draw (&nichrome);
	    
	    Semaphore::wait (has_event);
	    string line = event_line;
	    Semaphore::signal (read_event);

	    file l = File::string_read (line);

	    File::fscanf (l, "%d %s", &(int time), &(string name));

	    switch (name) {
	    case "press":
	    case "release":
	    case "double-press":
	    case "triple-press":
		File::fscanf (l, "%d %g %g", &(int button),
			      &(real x), &(real y));
		button_event_t bev = {
		    type = ((button_type_t[string]) {
			"press" => button_type_t.press,
			"release" => button_type_t.release,
			"double-press" => button_type_t.double,
			"triple-press" => button_type_t.triple }) [name],
		    time = time,
		    button = button,
		    x = x,
		    y = y };
		dispatch_button (&nichrome, &bev);
		break;
	    case "motion":
		File::fscanf (l, "%g %g", &(real x), &(real y));
		motion_event_t mev = {
		    type = motion_type_t.inside,
		    time = time,
		    x = x,
		    y = y };
		dispatch_motion (&nichrome, &mev);
		break;
	    case "key-down":
	    case "key-up":
		File::fscanf (l, "%s", &(string keyname));
		int[...] bytes = {};
		while (File::fscanf (l, "%x", &(int byte)) == 1)
		    bytes[dim(bytes)] = byte;
		string s = hex_utf8(bytes);
		key_event_t kev = {
		    type = (key_type_t[string]) {
			"key-down" => key_type_t.press,
			"key-up" => key_type_t.release } [name],
		    time = time,
		    key = keyname,
		    text = s };
		dispatch_key (&nichrome, &kev);
		break;
	    case "delete":
		nichrome.destroy (&nichrome);
		break;
	    case "configure":
		nichrome.resize = true;
		nichrome.draw = true;
		break;
	    }
	}

	Thread::kill (event_thread);
    }

    string preferences_file = ".nichromerc";
    
    public void load_preferences () {
	string	home = Environ::get ("HOME");
	if (home[String::length(home) - 1] != '/')
	    home += "/";
	try {
	    Command::lex_file (home + preferences_file);
	} catch File::open_error (string error_name,
			    File::error_type error,
			    string name) 
	{
	    if (error != File::error_type.NOENT)
		raise File::open_error (error_name, error, name);
	}
    }
}

Nichrome::load_preferences ();
