/*
 * 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>
 */

/*
 * Carl Worth wrote a C program to play the same game, with a slightly
 * different user interface. This program doesn't use any of the code, but
 * borrows some of the ideas for drawing the cards.
 */
 
autoimport Cairo;
autoimport PRNG;
autoload Nichrome;
autoload Nichrome::Button;
autoload Nichrome::Box;
autoload Nichrome::Label;
autoload Nichrome::Toggle;

dev_srandom (32);

/*
 * Set cards come in three colors
 */
typedef enum { red, green, purple } color_t;
color_t[*] colors = { color_t.red, color_t.green, color_t.purple };

/*
 * with three patterns,
 */
typedef enum { empty, striped, solid } pat_t;
pat_t[*] pats = { pat_t.empty, pat_t.striped, pat_t.solid };

/*
 * three shapes
 */
typedef enum { diamond, oval, squiggle } shape_t;
shape_t[*] shapes = { shape_t.diamond, shape_t.oval, shape_t.squiggle };

/*
 * and three colors
 */
rgb_color_t[color_t] rgb_colors = {
    color_t.red => (rgb_color_t) { red = 0.8, green = 0, blue = 0 },
    color_t.green => (rgb_color_t) { red = 0, green = 0.6, blue = 0 },
    color_t.purple => (rgb_color_t) { red = 0.6, green = 0, blue = 0.7 },
};

typedef struct {
    color_t color;
    pat_t pat;
    shape_t shape;
    int number;
} card_t;

/*
 * Construct a path for the object shape
 */
void
draw_shape (cairo_t cr, shape_t shape)
{
    enum switch (shape) {
    case diamond:
	move_to (cr, 0.0, -0.5);
	line_to (cr, 0.5, 0.0);
	line_to (cr, 0.0, 0.5);
	line_to (cr, -0.5, 0.0);
	close_path (cr);
	break;
    case oval:
	arc (cr, 0, 0, 0.5, 0, 2 * pi);
	break;
    case squiggle:
	real sqf = 0.75;
	move_to (cr, 0.25, -0.5);
	curve_to (cr,
		  0.25 + sqf, sqf / 3.0 - 0.5,
		  0.25 - sqf, 0.5 - (sqf / 3.0),
		  0.25, 0.5);
	line_to (cr, -0.25, 0.5);
	curve_to (cr,
		  -0.25 - sqf, 0.5 - (sqf / 3.0),
		  -0.25 + sqf, sqf / 3.0 - 0.5,
		  -0.25, -0.5);
	close_path (cr);
	break;
    }
}

void
set_color (cairo_t cr, color_t color) {
    rgb_color_t	rgb = rgb_colors[color];
    set_source_rgb (cr, rgb.red, rgb.green, rgb.blue);
}

void
set_transform (cairo_t cr, int n, &card_t card) {
    translate (cr, .5, .5);
    scale (cr, .9, .9);
    scale (cr, 1/3, 1);
    translate (cr, -(card.number + 1) / 2 + n, 0);
    if (card.pat == pat_t.empty)
	scale (cr, 0.81, 0.81);
    else
	scale (cr, 0.9, 0.9);
}

void
draw_card (cairo_t cr, &card_t card, int cols, int rows) {
    set_line_join (cr, line_join_t.MITER);
    set_miter_limit (cr, 100);
    set_source_rgb (cr, 0, 0, 0);
    set_fill_rule (cr, fill_rule_t.EVEN_ODD);
    rectangle (cr, 0, 0, 1, 1);

    real x_inset = .025;
    real y_inset = x_inset * rows / cols;
    rectangle (cr, x_inset, y_inset, 1 - x_inset * 2, 1 - y_inset * 2);
    fill (cr);
    for (int n = 1; n <= card.number; n++) {
	save (cr);
	set_transform (cr, n, &card);
	draw_shape (cr, card.shape);
	restore (cr);
	enum switch (card.pat) {
	case empty:
	    /* XXX work around bug in cairo 1.4.12 */
	    set_tolerance (cr, 0.01);
	    save (cr);
	    scale (cr, 1, rows/cols);
	    set_line_width (cr, 0.04);
	    set_source_rgb (cr, 1, 1, 1);
	    fill_preserve (cr);
	    set_color (cr, card.color);
	    stroke (cr);
	    restore (cr);
	    break;
	case solid:
	    set_color (cr, card.color);
	    fill (cr);
	    break;
	case striped:
	    pattern_t stripes = Pattern::create_linear (0, 0, .05, .05);
	    rgb_color_t	rgb = rgb_colors[card.color];
	    Pattern::set_extend (stripes, Pattern::extend_t.REPEAT);
	    
	    Pattern::add_color_stop_rgb (stripes, 0.0,
					 rgb.red, rgb.green, rgb.blue);
	    Pattern::add_color_stop_rgb (stripes, 0.48,
					 rgb.red, rgb.green, rgb.blue);
	    Pattern::add_color_stop_rgba (stripes, 0.50, 1, 1, 1, 1);
	    Pattern::add_color_stop_rgba (stripes, 0.98, 1, 1, 1, 1);
	    Pattern::add_color_stop_rgb (stripes, 1.0,
					 rgb.red, rgb.green, rgb.blue);
	    set_source (cr, stripes);
	    fill_preserve (cr);
	    save (cr);
	    scale (cr, 1, rows/cols);
	    set_line_width (cr, 0.01);
	    set_source_rgb (cr, rgb.red, rgb.green, rgb.blue);
	    set_tolerance (cr, 0.01);
	    stroke (cr);
	    restore (cr);
	    break;
	}
    }
}

typedef card_t[...] deck_t;

deck_t
make_deck () {
    deck_t  deck;
    int	    d = 0;

    for (int n = 1; n <= 3; n++)
	for (int c = 0; c < dim (colors); c++)
	    for (int p = 0; p < dim (pats); p++)
		for (int s = 0; s < dim (shapes); s++) {
		    card_t card = (card_t) {
			number = n,
			color = colors[c],
			shape = shapes[s],
			pat = pats[p]
		    };
		    deck[d++] = card;
		}
    shuffle (&deck);
    return deck;
}

typedef struct {
    card_t  card;
    bool    selected;
    pattern_t[...]	highlight;
} board_card_t;

typedef union {
    void	    empty;
    board_card_t    card;
} board_spot_t;
    
typedef board_spot_t[4,3] board_t;

board_t make_board () = (board_t) { { { empty = ◊ } ... } ... };

void
fill_board (&board_t board, &deck_t deck)
{
    int[2] d = dims (board);

    for (int y = 0; y < d[0]; y++)
	for (int x = 0; x < d[1]; x++)
	    if (board[y,x] == board_spot_t.empty && dim(deck) > 0) {
		board[y,x].card = (board_card_t) {
		    card = deck[dim(deck)-1],
		    selected = false,
		    highlight = {}
		};
		setdim (deck, dim(deck)-1);
	    }
}

void
draw_board (cairo_t cr, &board_t board, real width, real height)
{
    int[2] d = dims (board);
    real xinc = width / d[1];
    real yinc = height / d[0];

    save (cr);
    scale (cr, xinc, yinc);
    for (int y = 0; y < d[0]; y++)
	for (int x = 0; x < d[1]; x++) {
	    save (cr);
	    translate (cr, x + 0.5, y + 0.5);
	    scale (cr, 0.9, 0.9);
	    translate (cr, -0.5, -0.5);
	    enum switch (board[y,x]) {
	    case empty:
		break;
	    case card c:
		for (int h = 0; h < dim (c.highlight); h++)
		{
		    set_source (cr, c.highlight[h]);
		    rectangle (cr, 0, 0, 1, 1);
		    fill (cr);
		}
		draw_card (cr, &c.card, d[1], d[0]);
		break;
	    }
	    restore (cr);
	}
    restore (cr);
}

int nset = 3;

typedef struct {
    board_t		board;
    deck_t		deck;
    int			nselected;
    void()		unhint;
} game_t;

void add_highlight (&game_t game, int x, int y, pattern_t p, bool selects)
{
    enum switch (game.board[y,x]) {
    case card:
	&(pattern_t[...]) h = &game.board[y,x].card.highlight;
	h[dim(h)] = p;
	if (selects) {
	    game.board[y,x].card.selected = true;
	    game.nselected++;
	}
	break;
    case empty:
	break;
    }
}

void clear_highlight (&game_t game, int x, int y)
{
    setdim (game.board[y,x].card.highlight, 0);
    if (game.board[y,x].card.selected) {
	game.board[y,x].card.selected = false;
	--game.nselected;
    }
}

void select (&game_t game, int x, int y)
{
    enum switch (game.board[y,x]) {
    case card:
	if (game.board[y,x].card.selected) {
	    clear_highlight (&game, x, y);
	} else {
	    add_highlight (&game, x, y,
			   Pattern::create_rgba (0, 0, 0.5, 0.25),
			   true);
	}
	break;
    case empty:
	break;
    }
}

pattern_t
highlight_pattern (int i)
{
    real    x0, y0, x1, y1;
    real    angle = i * 2.8 / 4;
    real    scale = 0.05;
    real    c = cos(angle) * scale;
    real    s = sin(angle) * scale;
    rgb_color_t color = hsv_to_rgb ((hsv_color_t) {
	hue = i / 6, saturation = 1, value = 0.8 });

    pattern_t	pat = Pattern::create_linear (-c, -s, c, s);
    Pattern::set_extend (pat, Pattern::extend_t.REPEAT);
    Pattern::add_color_stop_rgba (pat, 0.0, 0, 0, 0, 0);
    Pattern::add_color_stop_rgba (pat, 0.38, 0, 0, 0, 0);
    Pattern::add_color_stop_rgba (pat, 0.4,
				  color.red, color.green,
				  color.blue, 1);
    Pattern::add_color_stop_rgba (pat, 0.6,
				  color.red, color.green,
				  color.blue, 1);
    Pattern::add_color_stop_rgba (pat, 0.62, 0, 0, 0, 0);
    Pattern::add_color_stop_rgba (pat, 1.0, 0, 0, 0, 0);
    return pat;
}

void unhighlight (&game_t game) {
    int[2] d = dims(game.board);

    for (int y = 0; y < d[0]; y++)
	for (int x = 0; x < d[1]; x++)
	    enum switch (game.board[y,x]) {
	    case card:
		clear_highlight (&game, x, y);
		break;
	    case empty:
		break;
	    }
}

(poly(card_t c))[*] getters = {
    poly func (card_t c) = c.number,
    poly func (card_t c) = c.color,
    poly func (card_t c) = c.shape,
    poly func (card_t c) = c.pat,
};

bool
all_same (&card_t[*] c, poly(card_t c) f)
{
    for (int i = 1; i < dim (c); i++)
	if (f(c[0]) != f(c[i]))
	    return false;
    return true;
}

bool
all_different (&card_t[*] c, poly(card_t c) f)
{
    for (int j = 0; j < dim (c); j++)
	for (int i = j+1; i < dim (c); i++)
	    if (f(c[j]) == f(c[i]))
		return false;
    return true;
}

bool
check_set (&card_t[*] c,  poly(card_t c) f)
{
    return all_same (&c, f) || all_different (&c, f);
}

bool
is_set (&card_t[*] c)
{
    for (int g = 0; g < dim (getters); g++)
	if (!check_set (&c, getters[g]))
	    return false;

    return true;
}

bool
check_game (&game_t game)
{
    card_t[3]  cards;
    
    if (game.nselected != 3)
	return false;
    int c = 0;
    int[2] d = dims(game.board);
    for (int y = 0; y < d[0]; y++)
	for (int x = 0; x < d[1]; x++)
	    enum switch (game.board[y,x]) {
	    case card:
		if (game.board[y,x].card.selected)
		    cards[c++] = game.board[y,x].card.card;
		break;
	    case empty:
		break;
	    }
    return is_set (&cards);
}

game_t
make_game ()
{
    game_t game = {
	deck = make_deck (),
	board = make_board (),
	nselected = 0
    };
    fill_board (&game.board, &game.deck);
    return game;
}

void
new_game (&game_t game)
{
    game.deck = make_deck();
    game.board = make_board ();
    unhighlight (&game);
    fill_board (&game.board, &game.deck);
}

void
clear_selected (&game_t game)
{
    int[2] d = dims (game.board);
    game.nselected = 0;
    for (int y = 0; y < d[0]; y++)
	for (int x = 0; x < d[1]; x++)
	    enum switch (game.board[y,x]) {
	    case card:
		if (game.board[y,x].card.selected) {
		    game.board[y,x] = board_spot_t.empty;
		}
		break;
	    case empty:
		break;
	    }
}

void
reshuffle (&game_t game)
{
    int[2] d = dims (game.board);
    for (int y = 0; y < d[0]; y++)
	for (int x = 0; x < d[1]; x++)
	    enum switch (game.board[y,x]) {
	    case card:
		game.deck[dim(game.deck)] = game.board[y,x].card.card;
		break;
	    case empty:
		break;
	    }
    shuffle (&game.deck);
    game.board = make_board ();
    fill_board (&game.board, &game.deck);
}

typedef struct { int x, y; } card_pos_t;

bool
find_sets (&game_t game, bool (card_pos_t[3]) callback) {
    card_t[3]  cards;
    int[2] d = dims(game.board);
    card_pos_t [d[0]*d[1]] order = {
	[i] = { x = i // d[0], y = i % d[0] }
    };
    
    bool get_card (int o, int c) {
	enum switch (game.board[order[o].y, order[o].x]) {
	case card:
	    cards[c] = game.board[order[o].y, order[o].x].card.card;
	    return true;
	case empty:
	    return false;
	}
    }
    
    bool ret = false;
    for (int i = 0; i < dim (order); i++) {
	if (!get_card (i, 0))
	    continue;
	for (int j = i + 1; j < dim (order); j++) {
	    if (!get_card (j, 1))
		continue;
	    for (int k = j + 1; k < dim (order); k++) {
		if (!get_card (k, 2))
		    continue;
		if (is_set (&cards)) {
		    ret = true;
		    if (!callback ((card_pos_t[3]) {
			{ x = order[i].x, y = order[i].y },
			{ x = order[j].x, y = order[j].y },
			{ x = order[k].x, y = order[k].y } }))
		    {
			return true;
		    }
		}
	    }
	}
    }
	return ret;
}

int
count_sets (&game_t game)
{
    int sets = 0;
    find_sets (&game, bool func (card_pos_t[3] pos) { sets++; return true; });
    return sets;
}

void
find_set (&game_t game)
{
    int	set = 0;
    unhighlight (&game);
    find_sets (&game, bool func (card_pos_t[3] pos) 
	       {
		pattern_t pat = highlight_pattern (set++);
		for (int i = 0; i < 3; i++)
		    add_highlight (&game, pos[i].x, pos[i].y, pat, false);
		return true;
	       });
}

extend namespace Nichrome {
    public namespace Setwidget {
	public typedef widget_t + struct {
	    &game_t game;
	    void() deal;
	} set_widget_t;

	void draw (cairo_t cr, &set_widget_t widget) {
	    draw_board (cr, &widget.game.board, widget.geometry.width,
			widget.geometry.height);
	}

	void outline (cairo_t cr, &set_widget_t widget)
	{
	    rectangle (cr, 0, 0, widget.geometry.width,
		       widget.geometry.height);
	}

	void natural (cairo_t cr, &set_widget_t widget)
	{
	    rectangle (cr, 0, 0, 640, 480);
	}
	
	void button (&set_widget_t widget, &button_event_t event) {
	    &game_t game = &widget.game;
	    if (event.type == button_type_t.press) {
		int[2] d = dims(game.board);
		int x_i = floor (event.x * d[1] / widget.geometry.width);
		int y_i = floor (event.y * d[0] / widget.geometry.height);
		if (!is_uninit (&game.unhint))
		    game.unhint ();
		select (&game, x_i, y_i);
		if (check_game (&game)) {
		    clear_selected (&game);
		    fill_board (&game.board, &game.deck);
		    widget.deal ();
		}
		Widget::redraw (&widget);
	    }
	}

	public *set_widget_t new (&nichrome_t nichrome, &game_t game, void () deal) {
	    set_widget_t    widget;
	    
	    Widget::init (&nichrome, &widget);
	    widget.button = button;
	    widget.draw = draw;
	    widget.outline = outline;
	    widget.natural = natural;
	    widget.deal = deal;
	    &widget.game = &game;
	    return &widget;
	}
    }
}

import Nichrome;
import Button;
import Widget;
import Setwidget;
import Box;
import Label;
import Toggle;

void play () {
    Widget::default_font = "sans-14:bold:italic";
    &nichrome_t	    ui = Nichrome::new ("Set", 400, 400);
    game_t	    g = make_game ();
    &label_t	    ngames = Label::new (&ui, "Moves: 0");

    &toggle_t	    hint = Toggle::new (&ui, "Hint",
					void func (&widget_t w, bool state) {
					    if (state)
						find_set (&g);
					    else
						unhighlight (&g);
					});

    g.unhint = void func () {
	Toggle::set (&hint, false);
    };
    void update_moves () {
	Label::relabel (&ngames, sprintf ("Moves: %d", count_sets (&g)));
	Toggle::set (&hint, false);
    }

    &set_widget_t   widget = Setwidget::new (&ui, &g, update_moves);

    &button_t	    quit = Button::new (&ui, "Quit",
					void func (&widget_t w, bool state) {
					    w.nichrome.running = false;
					});
    &button_t	    shuffle = Button::new (&ui, "Shuffle",
					   void func (&widget_t w, bool state) {
					    reshuffle (&g);
					    update_moves ();
					   });
    &button_t	    new = Button::new (&ui, "New Game",
				       void func (&widget_t widget, bool state) {
					new_game (&g);
					update_moves ();
				       });
    &box_t	    menu = Box::new (Box::dir_t.horizontal,
				     Box::widget_item (&shuffle, 0),
				     Box::widget_item (&hint, 0),
				     Box::widget_item (&new, 0),
				     Box::widget_item (&quit, 0),
				     Box::glue_item (1),
				     Box::widget_item (&ngames, 0));
    &box_t	    box = Box::new (Box::dir_t.vertical,
				    Box::box_item (&menu),
				    Box::widget_item (&widget));
    Nichrome::set_box (&ui, &box);
    update_moves ();
    Nichrome::main_loop (&ui);
}

play ();
