/* Dia -- an diagram creation/manipulation program
 * Copyright (C) 1998 Alexander Larsson
 *
 * This code renders boolean equations, as needed by the transitions' 
 * receptivities.
 *
 * Copyright (C) 2000 Cyrille Chepelov
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <config.h>
#include "boolequation.h"

#define OVERLINE_RATIO .1

typedef enum {BLOCK_COMPOUND, BLOCK_OPERATOR, BLOCK_OVERLINE,
	      BLOCK_PARENS, BLOCK_TEXT} BlockType;
typedef enum {OP_AND, OP_OR, OP_XOR, OP_RISE, OP_FALL,
              OP_EQUAL,OP_LT,OP_GT} OperatorType;

typedef void (*BlockGetBBFunc)(Block *block, Point *relpos, Boolequation *booleq,
			       Rectangle *rect);
typedef void (*BlockDrawFunc)(Block *block, 
			      Boolequation *booleq, 
			      DiaRenderer *render);
typedef void (*BlockDestroyFunc)(Block *block);

typedef struct {
  BlockGetBBFunc get_boundingbox;
  BlockDrawFunc draw;
  BlockDestroyFunc destroy;
} BlockOps;
  
struct _Block {
  BlockType type;
  BlockOps *ops;
  Point bl, ur, pos;
  union {
    Block *inside; /* overline, parens */
    const gchar *text;
    GSList *contained;
    OperatorType operator;
  } d;
};


/* utility */
inline static gboolean isspecial(gunichar c) 
{
  switch (c) {
  case '!':
  case '(':
  case ')':
  case '&':
  case '|':
  case '^':
  case '+':
  case '.':
  case '*':
  case '{':
  case '}':
    return TRUE;
  default:
    return FALSE;
  }
}

/* Text block definition */
static void 
textblock_get_boundingbox(Block *block, Point *relpos, 
			  Boolequation *booleq, Rectangle *rect)
{
  g_assert(block); g_assert(block->type == BLOCK_TEXT);

  block->pos = *relpos;
  block->bl.x = block->pos.x;
  block->bl.y = block->pos.y + dia_font_descent(block->d.text,
                                                booleq->font,
                                                booleq->fontheight);
  block->ur.y = block->pos.y - dia_font_ascent(block->d.text,
                                               booleq->font,
                                               booleq->fontheight);
  block->ur.x = block->bl.x + dia_font_string_width(block->d.text,
                                                    booleq->font,
                                                    booleq->fontheight);
  rect->left = block->bl.x;
  rect->top = block->ur.y;
  rect->bottom = block->bl.y;
  rect->right = block->ur.x;
}

static void 
textblock_draw(Block *block,Boolequation *booleq,DiaRenderer *renderer)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS (renderer);
  g_assert(block); g_assert(block->type == BLOCK_TEXT);
  renderer_ops->set_font(renderer,booleq->font,booleq->fontheight);
  renderer_ops->draw_string(renderer,block->d.text,
			     &block->pos,ALIGN_LEFT,&booleq->color);
}

static void 
textblock_destroy(Block *block)
{
  if (!block) return;
  g_assert(block->type == BLOCK_TEXT);
  g_free((void *)block->d.text);
  g_free(block);
}

static BlockOps text_block_ops = {
  textblock_get_boundingbox,
  textblock_draw,
  textblock_destroy};
  
static Block *textblock_create(const gchar **str) 
{
  const gchar *p = *str;
  Block *block;

  while (**str) {
    gunichar c;
    gchar *p1;
    c = g_utf8_get_char(*str);
    p1 = g_utf8_next_char(*str);
    if (isspecial(c)) break;
    *str = p1;
  }
  
  block = g_new0(Block,1);
  block->type = BLOCK_TEXT;
  block->ops = &text_block_ops;
  block->d.text = g_strndup(p,*str-p);

  return block;
}

/* Operator block */
static const gchar xor_symbol[] = { 226, 138, 149, 0 }; /* 0x2295 */
static const gchar and_symbol[] = { 226, 136, 153, 0 }; /* 0x2219 */
static const gchar rise_symbol[] = { 226, 134, 145, 0 }; /* 0x2191 */
static const gchar fall_symbol[] = { 226, 134, 147, 0 }; /* 0x2193 */
static const gchar or_symbol[] = { 43, 0 };
static const gchar lt_symbol[] = { 60,0 };
static const gchar equal_symbol[] = { 61,0 };
static const gchar gt_symbol[] = { 62,0 };

static const gchar *opstring(OperatorType optype)
{
  switch (optype) {

  case OP_AND: return and_symbol;
  case OP_OR: return or_symbol;
  case OP_XOR: return xor_symbol;
  case OP_RISE: return rise_symbol;
  case OP_FALL: return fall_symbol;
  case OP_EQUAL: return equal_symbol;
  case OP_LT: return lt_symbol;
  case OP_GT: return gt_symbol;
  default:
    break;
  }
  g_assert_not_reached();
  return NULL; /* never */
}

static void
opblock_get_boundingbox(Block *block, Point *relpos,
			Boolequation *booleq, Rectangle *rect)
{
  const gchar* ops;
  g_assert(block); g_assert(block->type == BLOCK_OPERATOR);
  
  ops = opstring(block->d.operator);
  
  block->pos = *relpos;
  block->bl.x = block->pos.x;
  block->bl.y = block->pos.y +
      dia_font_descent(ops,booleq->font,booleq->fontheight);
  block->ur.y = block->bl.y - booleq->fontheight;
  block->ur.x = block->bl.x + dia_font_string_width(ops, booleq->font,
                                                    booleq->fontheight);
  rect->left = block->bl.x;
  rect->top = block->ur.y;
  rect->bottom = block->bl.y;
  rect->right = block->ur.x;
}

static void 
opblock_draw(Block *block, Boolequation *booleq,DiaRenderer *renderer)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS (renderer);
  g_assert(block); g_assert(block->type == BLOCK_OPERATOR);
  renderer_ops->draw_string(renderer,opstring(block->d.operator),&block->pos,
			     ALIGN_LEFT,&booleq->color);
}
    
static void opblock_destroy(Block *block)
{
  if (!block) return;
  g_assert(block->type == BLOCK_OPERATOR);
  g_free(block);
}

static BlockOps operator_block_ops = {
  opblock_get_boundingbox,
  opblock_draw,
  opblock_destroy};

static Block *opblock_create(const gchar **str) 
{
  Block *block;
  gunichar c;

  c = g_utf8_get_char(*str);
  *str = g_utf8_next_char(*str);

  block = g_new0(Block,1);
  block->type = BLOCK_OPERATOR;
  block->ops = &operator_block_ops;
  switch(c) {
  case '&': 
  case '.':
    block->d.operator = OP_AND;
    break;
  case '|':
  case '+':
    block->d.operator = OP_OR;
    break;
  case '^':
  case '*':
    block->d.operator = OP_XOR;
    break;
  case '=':
    block->d.operator = OP_EQUAL;
    break;
  case '<': 
    block->d.operator = OP_LT;
    break;
  case '>':
    block->d.operator = OP_GT;
    break;
  case '{':
    block->d.operator = OP_RISE;
    break;
  case '}':
    block->d.operator = OP_FALL;
    break;
  default:
    g_assert_not_reached();
  }
  return block;
}

/* Overlineblock : */
static void
overlineblock_get_boundingbox(Block *block, Point *relpos,
			      Boolequation *booleq, Rectangle *rect)
{
  g_assert(block); g_assert(block->type == BLOCK_OVERLINE);
  
  block->d.inside->ops->get_boundingbox(block->d.inside,relpos,booleq,rect);

  block->bl = block->d.inside->bl;
  block->ur.x = block->d.inside->ur.x;
  block->ur.y = 
    block->d.inside->ur.y - (3.0 * OVERLINE_RATIO * booleq->fontheight);

  /*rect->left = bl.x; */
  rect->top = block->ur.y;
  /*rect->bottom = bl.y;
    rect->right = ur.x;*/
}

static void 
overlineblock_draw(Block *block,Boolequation *booleq,DiaRenderer *renderer)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS (renderer);
  Point ul,ur;
  g_assert(block); g_assert(block->type == BLOCK_OVERLINE);
  block->d.inside->ops->draw(block->d.inside,booleq,renderer);
  renderer_ops->set_linestyle(renderer,LINESTYLE_SOLID);
  renderer_ops->set_linewidth(renderer,booleq->fontheight * OVERLINE_RATIO);
  ul.x = block->bl.x;
  ur.y = ul.y = block->ur.y;

      /* FIXME: try to get the actual block width */
  ur.x = block->ur.x -
      (dia_font_string_width("_", booleq->font,booleq->fontheight) / 2);
  
  renderer_ops->draw_line(renderer,&ul,&ur,&booleq->color);
}

static void
overlineblock_destroy(Block *block)
{
  if (!block) return;
  g_assert(block->type == BLOCK_OVERLINE);
  block->d.inside->ops->destroy(block->d.inside);
  g_free(block);
}

static BlockOps overline_block_ops = {
  overlineblock_get_boundingbox,
  overlineblock_draw,
  overlineblock_destroy};

static Block *overlineblock_create(Block *inside)
{
  Block *block;
  /* *str is already in the inside */
  block = g_new0(Block,1);
  block->type = BLOCK_OVERLINE;
  block->ops = &overline_block_ops;
  block->d.inside = inside;

  return block;
}

/* Parensblock : */
static void
parensblock_get_boundingbox(Block *block, Point *relpos,
			    Boolequation *booleq, Rectangle *rect)
{
  real pheight,pwidth;
  Point temppos;
  g_assert(block); g_assert(block->type == BLOCK_PARENS);

  temppos = block->pos = *relpos;
  block->d.inside->ops->get_boundingbox(block->d.inside,&temppos,booleq,rect);
  pheight = 1.1 * (block->d.inside->bl.y - block->d.inside->ur.y);
  pwidth = dia_font_string_width("()",booleq->font,pheight) / 2;
  temppos.x += pwidth;
  block->d.inside->ops->get_boundingbox(block->d.inside,&temppos,booleq,rect);
  
  block->bl.x = block->pos.x;
  block->bl.y = block->pos.y + dia_font_descent("()",booleq->font,pheight);
  block->ur.x = block->d.inside->ur.x + pwidth;
  block->ur.y = block->bl.y - pheight;

  rect->left = block->bl.x;
  rect->top = block->ur.y;
  rect->bottom = block->bl.y;
  rect->right = block->ur.x;
}

static void 
parensblock_draw(Block *block,Boolequation *booleq,DiaRenderer *renderer)
{
  DiaRendererClass *renderer_ops = DIA_RENDERER_GET_CLASS (renderer);
  Point pt;
  real pheight;

  g_assert(block); g_assert(block->type == BLOCK_PARENS);

  pheight = block->d.inside->bl.y - block->d.inside->ur.y;
  block->d.inside->ops->draw(block->d.inside,booleq,renderer);

  renderer_ops->set_font(renderer,booleq->font,pheight);
  pt.y = block->pos.y;
  pt.x = block->d.inside->ur.x;

  renderer_ops->draw_string(renderer,"(",&block->pos,ALIGN_LEFT,&booleq->color);
  renderer_ops->draw_string(renderer,")",&pt,ALIGN_LEFT,&booleq->color);
}
 
static void
parensblock_destroy(Block *block)
{
  if (!block) return;
  g_assert(block->type == BLOCK_PARENS);
  block->d.inside->ops->destroy(block->d.inside);
  g_free(block);
}

static BlockOps parens_block_ops = {
  parensblock_get_boundingbox,
  parensblock_draw,
  parensblock_destroy};
  
static Block *parensblock_create(Block *inside) 
{
  Block *block;
  /* *str is already in the inside */
  block = g_new0(Block,1);
  block->type = BLOCK_PARENS;
  block->ops = &parens_block_ops;
  block->d.inside = inside;

  return block;
}

/* Compoundblock : */
static void 
compoundblock_get_boundingbox(Block *block, Point *relpos,
			      Boolequation *booleq, Rectangle *rect)
{
  GSList *elem;
  Block *inblk;
  Point pos;
  Rectangle inrect;

  g_assert(block); g_assert(block->type == BLOCK_COMPOUND);

  pos = block->pos = *relpos;

  inrect.right = inrect.left = pos.x;
  inrect.top = inrect.bottom = pos.y;

  *rect = inrect;

  elem = block->d.contained;
  while (elem) {
    inblk = (Block *)(elem->data);
    if (!inblk) break;
    
    inblk->ops->get_boundingbox(inblk,&pos,booleq,&inrect);
    rectangle_union(rect,&inrect);
    
    pos.x = inblk->ur.x;

    elem = g_slist_next(elem);
  }
  block->ur.x = rect->right;
  block->ur.y = rect->top;
  block->bl.x = rect->left;
  block->bl.y = rect->bottom;
}

static void compoundblock_draw(Block *block,
			       Boolequation *booleq,
			       DiaRenderer *renderer)
{
  GSList *elem;
  Block *inblk;
  g_assert(block); g_assert(block->type == BLOCK_COMPOUND);

  elem = block->d.contained;
  while (elem) {
    inblk = (Block *)(elem->data);
    if (!inblk) break;
    
    inblk->ops->draw(inblk,booleq,renderer);
    
    elem = g_slist_next(elem);
  }
}

static void 
compoundblock_destroy(Block *block)
{
  GSList *elem;
  Block *inblk;
  
  if (!block) return;
  g_assert(block->type == BLOCK_COMPOUND);

  elem = block->d.contained;
  while (elem) {
    inblk = (Block *)(elem->data);
    if (!inblk) break;

    inblk->ops->destroy(inblk);
    elem->data = NULL;
    
    elem = g_slist_next(elem);
  }

  g_slist_free(block->d.contained);
  g_free(block);
}

static BlockOps compound_block_ops = {
  compoundblock_get_boundingbox,
  compoundblock_draw,
  compoundblock_destroy};

static Block *
compoundblock_create(const gchar **str) 
{
  Block *block, *inblk;
  
  block = g_new0(Block,1);
  block->type = BLOCK_COMPOUND;
  block->ops = &compound_block_ops;
  block->d.contained = NULL;

  while (*str && **str) {
    gunichar c;
    gchar *p;
    c = g_utf8_get_char(*str);
    p = g_utf8_next_char(*str);

    inblk = NULL;
    switch (c) {
    case '&':
    case '^':
    case '|':
    case '+':
    case '.':
    case '*':
    case '=':
    case '>':
    case '<':
    case '}':
    case '{':
      inblk = opblock_create(str);
      break;
    case '(':
      *str = p;
      inblk = parensblock_create(compoundblock_create(str));
      break;
    case '!':
      *str = p;
      c = g_utf8_get_char(*str);
      p = g_utf8_next_char(*str);
      if (c == '(') {
	*str = p;
	inblk = overlineblock_create(compoundblock_create(str));
      } else { /* single identifier "not" */
	inblk = overlineblock_create(textblock_create(str));
      }
      break;
    case ')': /* Whooops ! outta here ! */
      *str = p;
      return block;
    default:
      inblk = textblock_create(str);
    }
    if (inblk) {
      block->d.contained = g_slist_append(block->d.contained,inblk);
    }
  }
  /* We should fall here exactly once : at the end of the root block. */
  return block;
}


/* Boolequation : */
void 
boolequation_set_value(Boolequation *booleq, const gchar *value)
{
  g_return_if_fail(booleq);
  if (booleq->value) g_free((gchar *)booleq->value);
  if (booleq->rootblock) booleq->rootblock->ops->destroy(booleq->rootblock);

  booleq->value = g_strdup(value);
  booleq->rootblock = compoundblock_create(&value);
  /* a good bounding box recalc here would be nice. */
}


Boolequation *
boolequation_create(const gchar *value, DiaFont *font, real fontheight,
		   Color *color)
{
  Boolequation *booleq;

  booleq = g_new0(Boolequation,1);
  booleq->font = dia_font_ref(font);
  booleq->fontheight = fontheight;
  booleq->color = *color;
  boolequation_set_value(booleq,value);

  return booleq;
}

void 
boolequation_destroy(Boolequation *booleq)
{
  g_return_if_fail(booleq);
  dia_font_unref(booleq->font);
  if (booleq->value) g_free((gchar *)booleq->value);
  if (booleq->rootblock) booleq->rootblock->ops->destroy(booleq->rootblock);
  g_free(booleq);
}

void save_boolequation(ObjectNode obj_node, const gchar *attrname,
			     Boolequation *booleq)
{
  data_add_string(new_attribute(obj_node,attrname),(gchar *)booleq->value);
}

Boolequation *
load_boolequation(ObjectNode obj_node,
		 const gchar *attrname,
		 const gchar *defaultvalue,
		 DiaFont *font,
		 real fontheight, Color *color)
{
  gchar *value = NULL;
  Boolequation *booleq;
  AttributeNode attr;

  booleq = boolequation_create(NULL,font,fontheight,color);
  attr = object_find_attribute(obj_node,attrname);
  if (attr) 
    value = data_string(attribute_first_data(attr));
  else if (defaultvalue)
    value = g_strdup (defaultvalue);
  if (value) 
    boolequation_set_value(booleq,value);
  g_free(value);

  return booleq;
}
 
void 
boolequation_draw(Boolequation *booleq, DiaRenderer *renderer)
{
  if (booleq->rootblock) {
    booleq->rootblock->ops->draw(booleq->rootblock,booleq,renderer);
  }
}

void boolequation_calc_boundingbox(Boolequation *booleq, Rectangle *box)
{
        /*
          booleq->ascent = dia_font_ascent(booleq->font,booleq->fontheight);
          booleq->descent = dia_font_descent(booleq->font,booleq->fontheight);
        */
          
  box->left = box->right = booleq->pos.x;
  box->top = box->bottom = booleq->pos.y;

  if (booleq->rootblock) {
    booleq->rootblock->ops->get_boundingbox(booleq->rootblock,
                                            &booleq->pos,booleq,box);
  }  

  booleq->width = box->right - box->left;
  booleq->height = box->bottom - box->top;
}

