/* opts.c
   Copyright (C) 2005,2006,2007 Eugene K. Ressler, Jr.

This file is part of Sketch, a small, simple system for making 
3d drawings with LaTeX and the PSTricks or TikZ package.

Sketch 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 3, or (at your option)
any later version.

Sketch 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 Sketch; see the file COPYING.txt.  If not, see
http://www.gnu.org/copyleft */

#include "opts.h"
#include "geometry.h"

DECLARE_DYNAMIC_ARRAY_FUNCS (OPT_LIST, OPT, opt_list, elt, n_elts, NO_OTHER_INIT)

// ---- useful string stuff ----------------------------------------------------
// slice a string using Perl/Python position indexing conventions
// (position == dst_size is always at the end of the string)

char *str_slice (char *dst, int dst_size, char *src, int beg, int end)
{
  int len;

  if (dst_size > 0)
    {
      len = strlen (src);
      if (beg < 0)
	beg = len + beg;
      else if (beg > len)
	beg = len;
      if (end < 0)
	end = len + end;
      else if (end > len)
	end = len;
      len = end - beg;
      if (len <= 0)
	{
	  dst[0] = '\0';
	}
      else
	{
	  if (len >= dst_size)
	    len = dst_size - 1;
	  memcpy (dst, &src[beg], len);
	  dst[len] = '\0';
	}
    }
  return dst;
}

// a modified version of C library strtok
// uses state variable p, which should be
// initially set to zero.
char *
istrtok (int *p, char *s, char sep)
{
  int i, r;

  // advance r to next non-space character
  for (r = *p; s[r] == ' ' || s[r] == '\t'; r++)
    /* skip */ ;

  // if we're at terminating null, return null
  if (s[r] == '\0')
    {
      *p = r;
      return NULL;
    }
  // look for a separator character
  for (i = r; s[i] != '\0'; i++)
    {
      if (s[i] == sep)
	{
	  // found one; set to null char,
	  // advance state variable, and
	  // return pointer to first char
	  s[i] = '\0';
	  *p = i + 1;
	  return &s[r];
	}
    }
  // did not find a terminator, so this
  // is the last token; return it
  *p = i;
  return &s[r];
}

int
str_last_occurance (char *src, char *set)
{
  int i;

  for (i = 0; src[i]; i++)
    /* skip */ ;
  for (--i; i >= 0 && !strchr (set, src[i]); --i)
    /* skip */ ;
  return i;
}

// ---- options ----------------------------------------------------------------

void
init_opts (OPTS * opts)
{
  init_opt_list (opts->list);
}

OPTS *
raw_opts (void)
{
  OPTS *r = safe_malloc (sizeof *r);
  init_opts (r);
  return r;
}

void
setup_opts (OPTS * opts, char *opts_str, SRC_LINE line)
{
  int p_pair, p_side;
  char *pair, *key, *val, *buf;
  OPT *opt;

  clear_opts (opts);
  buf = safe_strdup (opts_str);
  p_pair = 0;
  while ((pair = istrtok (&p_pair, buf, ',')) != NULL)
    {
      p_side = 0;
      key = istrtok (&p_side, pair, '=');
      if (key == NULL)
	{
	  err (line, "null keyword in option");
	  key = "";
	}
      val = istrtok (&p_side, pair, ',');
      if (val == NULL)
	{
	  err (line, "null value in option");
	  val = "";
	}
      opt = pushed_opt_list_elt (opts->list);
      opt->key = safe_strdup (key);
      opt->val = safe_strdup (val);
    }
  safe_free (buf);
}

OPTS *
new_opts (char *opts_str, SRC_LINE line)
{
  OPTS *r = raw_opts ();
  setup_opts (r, opts_str, line);
  return r;
}

void
clear_opts (OPTS * opts)
{
  int i;

  for (i = 0; i < opts->list->n_elts; ++i)
    {
      safe_free (opts->list->elt[i].key);
      safe_free (opts->list->elt[i].val);
    }
  clear_opt_list (opts->list);
}

char *
opt_val (OPTS * opts, char *opt)
{
  int i;

  if (!opts)
    return 0;

  for (i = 0; i < opts->list->n_elts; i++)
    if (strcmp (opts->list->elt[i].key, opt) == 0)
      return opts->list->elt[i].val;
  return NULL;
}

int
bool_opt_p (OPTS * opts, char *opt, int default_p)
{
  char *r = opt_val (opts, opt);
  if (!r)
    return default_p;
  return strcmp (r, "false") != 0;	// all not false is true
}

typedef struct opt_desc_t
{
  char *opt;
  int type;
}
OPT_DESC;

typedef struct opt_desc_tbl_t
{
  OPT_DESC *key_desc;
  int n_key_desc;
  OPT_DESC *val_desc;
  int n_val_desc;
}
OPT_DESC_TBL;

static OPT_DESC key_tbl_pst[] = {
  {"arrows", OPT_LINE},
  {"cull", OPT_INTERNAL},
  {"dash", OPT_LINE},
  {"dotsep", OPT_LINE},
  {"fillcolor", OPT_POLYGON | OPT_FILL_COLOR},
  {"fillstyle", OPT_POLYGON | OPT_FILL_STYLE},
  {"lay", OPT_INTERNAL},
  {"linecolor", OPT_LINE},
  {"linestyle", OPT_LINE | OPT_LINE_STYLE},
  {"linewidth", OPT_LINE},
  {"opacity", OPT_POLYGON},
  {"showpoints", OPT_LINE | OPT_POLYGON},
  {"split", OPT_INTERNAL},
  {"strokeopacity", OPT_LINE },
  {"transpalpha", OPT_POLYGON}
};

OPT_DESC_TBL opt_desc_tbl_pst[1] = { {
				      key_tbl_pst, ARRAY_SIZE (key_tbl_pst),
				      NULL, 0}
};

static OPT_DESC opt_key_tbl_tikz[] = {
  {"arrows", OPT_LINE},
  {"cap", OPT_LINE},
  {"color", OPT_LINE | OPT_POLYGON | OPT_FILL_COLOR},
  {"cull", OPT_INTERNAL},
  {"dash pattern", OPT_LINE},
  {"dash phase", OPT_LINE},
  {"double distance", OPT_LINE},
  {"draw", OPT_LINE | OPT_LINE_STYLE},
  {"draw opacity", OPT_LINE},
  {"fill", OPT_POLYGON | OPT_FILL_COLOR},
  {"fill opacity", OPT_POLYGON},
  {"fill style", OPT_POLYGON | OPT_FILL_COLOR | OPT_EMIT_VAL},
  {"join", OPT_LINE},
  {"lay", OPT_INTERNAL},
  {"line style", OPT_LINE | OPT_EMIT_VAL},
  {"line width", OPT_LINE},
  {"miter limit", OPT_LINE},
  {"pattern", OPT_POLYGON | OPT_FILL_COLOR},
  {"pattern color", OPT_POLYGON},
  {"split", OPT_INTERNAL},
  {"style", OPT_TYPE_IN_VAL | OPT_EMIT_VAL},
};

static OPT_DESC opt_val_tbl_tikz[] = {
  {"dashed", OPT_LINE},
  {"densely dashed", OPT_LINE},
  {"densely dotted", OPT_LINE},
  {"dotted", OPT_LINE},
  {"double", OPT_LINE},
  {"loosely dashed", OPT_LINE},
  {"loosely dotted", OPT_LINE},
  {"nearly opaque", OPT_POLYGON},
  {"nearly transparent", OPT_POLYGON},
  {"semithick", OPT_LINE},
  {"semitransparent", OPT_POLYGON},
  {"solid", OPT_LINE},
  {"thick", OPT_LINE},
  {"thin", OPT_LINE},
  {"transparent", OPT_POLYGON},
  {"ultra nearly transparent", OPT_POLYGON},
  {"ultra thick", OPT_LINE},
  {"ultra thin", OPT_LINE},
  {"very nearly transparent", OPT_POLYGON},
  {"very thick", OPT_LINE},
  {"very thin", OPT_LINE},
};

OPT_DESC_TBL opt_desc_tbl_tikz[1] = { {
				       opt_key_tbl_tikz,
				       ARRAY_SIZE (opt_key_tbl_tikz),
				       opt_val_tbl_tikz,
				       ARRAY_SIZE (opt_val_tbl_tikz),
				       }
};

int
opt_index (char *opt, OPT_DESC * desc, int n_desc)
{
  int hi, lo, mid, cmp_val;

  hi = n_desc - 1;
  lo = 0;
  while (hi >= lo)
    {
      mid = (hi + lo) / 2;
      cmp_val = strcmp (opt, desc[mid].opt);
      if (cmp_val < 0)
	hi = mid - 1;
      else if (cmp_val > 0)
	lo = mid + 1;
      else
	return mid;
    }
  return -1;
}

static OPT_DESC_TBL *lang_to_opt_desc_tbl[] = {
  opt_desc_tbl_pst,
  opt_desc_tbl_tikz,
  opt_desc_tbl_pst,
  opt_desc_tbl_tikz,
};

int
simple_opt_type (OPT * opt, int default_type, int lang)
{
  OPT_DESC_TBL *desc;
  int i;

  if (lang < 0)
    return default_type;
  desc = lang_to_opt_desc_tbl[lang];
  i = opt_index (opt->key, desc->key_desc, desc->n_key_desc);
  return (i < 0) ? default_type : desc->key_desc[i].type;
}

int
opt_type (OPT * opt, int default_type, int lang)
{
  OPT_DESC_TBL *desc;
  int i, type;

  type = simple_opt_type (opt, default_type, lang);
  if (type & OPT_TYPE_IN_VAL)
    {
      desc = lang_to_opt_desc_tbl[lang];
      i = opt_index (opt->val, desc->val_desc, desc->n_val_desc);
      if (i < 0)
	return default_type;
      type = desc->val_desc[i].type;
    }
  return type;
}

typedef struct opts_desc_t
{
  OPT *opts;
  int n_opts;
}
OPTS_DESC;

OPT no_edges_opts_pst[] = {
  {"linestyle", "none"}
};

OPT no_edges_opts_tikz[] = {
  {"draw", "none"}
};

OPTS_DESC no_edges_opts_desc_tbl[] = {
  {no_edges_opts_pst, ARRAY_SIZE (no_edges_opts_pst)},
  {no_edges_opts_tikz, ARRAY_SIZE (no_edges_opts_tikz)},
  {no_edges_opts_pst, ARRAY_SIZE (no_edges_opts_pst)},
  {no_edges_opts_tikz, ARRAY_SIZE (no_edges_opts_tikz)},
};

static int
any_opt_p (OPTS * opts, int type, int lang)
{
  int i;

  if (!opts)
    return 0;
  for (i = 0; i < opts->list->n_elts; i++)
    if (type & opt_type (&opts->list->elt[i], OPT_NONE, lang))
      return 1;
  return 0;
}

static void
add_default_opt (OPTS ** opts_ptr, OPT * default_opt, int lang)
{
  OPT *opt;
  OPTS *opts;
  int default_type;

  opts = *opts_ptr;
  default_type = opt_type (default_opt, OPT_NONE, lang) & OPT_DEFAULTS;
  if (any_opt_p (opts, default_type, lang))
    return;
  if (!opts)
    opts = raw_opts ();
  opt = pushed_opt_list_elt (opts->list);
  opt->key = safe_strdup (default_opt->key);
  opt->val = safe_strdup (default_opt->val);
  *opts_ptr = opts;
}

static void
add_default_opts (OPTS ** opts_ptr, OPTS_DESC * opts_desc, int lang)
{
  int i;
  for (i = 0; i < opts_desc->n_opts; i++)
    add_default_opt (opts_ptr, &opts_desc->opts[i], lang);
}

void
add_no_edges_default_opt (OPTS ** opts_ptr, int lang)
{
  add_default_opts (opts_ptr, &no_edges_opts_desc_tbl[lang], lang);
}

OPT solid_white_opts_pst[] = {
  {"fillstyle", "solid"},
  {"fillcolor", "white"}
};

OPT solid_white_opts_tikz[] = {
  {"fill", "white"}
};

OPTS_DESC solid_white_opts_desc_tbl[] = {
  {solid_white_opts_pst, ARRAY_SIZE (solid_white_opts_pst)},
  {solid_white_opts_tikz, ARRAY_SIZE (solid_white_opts_tikz)},
  {solid_white_opts_pst, ARRAY_SIZE (solid_white_opts_pst)},
  {solid_white_opts_tikz, ARRAY_SIZE (solid_white_opts_tikz)},
};

void
add_solid_white_default_opt (OPTS ** opts_ptr, int lang)
{
  add_default_opts (opts_ptr, &solid_white_opts_desc_tbl[lang], lang);
}

void
check_opts (OPTS * opts,
	    int allowed, char *allowed_msg, int lang, SRC_LINE line)
{
  int i, type;

  if (!opts)
    return;

  for (i = 0; i < opts->list->n_elts; i++)
    {
      type = opt_type (&opts->list->elt[i], OPT_NONE, lang);
      if ((type & allowed) == 0)
	warn (line, allowed_msg, opts->list->elt[i].key,
	      opts->list->elt[i].val);
    }
}

// selective copy for splitting option lists by type
OPTS *
copy_opts (OPTS * opts, int type_mask, int lang)
{
  int i;
  OPTS *r;
  OPT *opt;

  if (!opts)
    return NULL;

  r = raw_opts ();
  for (i = 0; i < opts->list->n_elts; i++)
    if (type_mask & opt_type (&opts->list->elt[i], OPT_NONE, lang))
      {
	opt = pushed_opt_list_elt (r->list);
	opt->key = safe_strdup (opts->list->elt[i].key);
	opt->val = safe_strdup (opts->list->elt[i].val);
      }
  return r;
}

OPTS *
cat_opts (OPTS * dst, OPTS * src)
{
  int i;
  OPT *opt;

  for (i = 0; i < src->list->n_elts; i++) 
    {
      opt = pushed_opt_list_elt (dst->list);
      opt->key = safe_strdup(src->list->elt[i].key);
      opt->val = safe_strdup(src->list->elt[i].val);
    }
  return dst;
}

// selective copy for splitting out line options and modifying arrows
OPTS *
copy_line_opts (OPTS * opts, int first_p, int last_p, int lang)
{
  int i;
  OPTS *r;
  char buf[100];

  if (!opts)
    return NULL;

  // no modifications necessary if line contains first and last points
  if (first_p && last_p)
    return opts;

  // make a clean copy and modify the arrows
  r = copy_opts (opts, OPT_LINE, lang);

  for (i = 0; i < r->list->n_elts; i++)
    {
      if (strcmp ("arrows", r->list->elt[i].key) == 0)
	{
	  char *val = r->list->elt[i].val;
	  char *dash = strchr (val, '-');
	  if (!dash)
	    {
	      warn (no_line,
		    "could not find '-' while splitting arrows option");
	      continue;
	    }
	  if (first_p)
	    {
	      str_slice (buf, sizeof buf, val, 0, dash - val + 1);
	    }
	  else if (last_p)
	    {
	      str_slice (buf, sizeof buf, val, dash - val, SLICE_TO_END);
	    }
	  else
	    {
	      // could just delete option entirely, but this is good for debugging
	      str_slice (buf, sizeof buf, val, dash - val, dash - val + 1);
	    }
	  r->list->elt[i].val = safe_strdup (buf);
	}
    }
  return r;
}

static int member_p(char *str, char **str_list)
{
  if (str_list == NULL)
    return 0;

  while (*str_list) 
    {
      if (strcmp(str, *str_list) == 0)
	return 1;
      ++str_list;
    }
  return 0;
}

static void
emit_opts_internal (FILE * f, OPTS * opts, char ** exceptions, int brackets_p, int lang)
{
  int i, n, type;

  // do nothing if no options
  if (!opts || !opts->list || opts->list->n_elts == 0)
    return;

  // do nothing if no non-excepted options
  for (n = i = 0; i < opts->list->n_elts; i++)
    {
      if ( !member_p(opts->list->elt[i].key, exceptions) )
	++n;
    }
  if (n == 0)
    return;

  if (brackets_p)
    fputc ('[', f);

  for (n = i = 0; i < opts->list->n_elts; i++)
    {

      if ( member_p(opts->list->elt[i].key, exceptions) )
	continue;

      type = simple_opt_type (&opts->list->elt[i], OPT_NONE, lang);
      if ((type & OPT_INTERNAL) == 0)
	{
	  if (n > 0)
	    fprintf (f, ",");
	  if (type & OPT_EMIT_VAL)
	    fprintf (f, "%s", opts->list->elt[i].val);
	  else
	    fprintf (f, "%s=%s", opts->list->elt[i].key,
		     opts->list->elt[i].val);
	  ++n;
	}
    }
  if (brackets_p)
    fputc (']', f);
}

void
emit_opts_raw (FILE * f, OPTS * opts, int lang)
{
  emit_opts_internal (f, opts, NULL, 0, lang);
}

void
emit_opts (FILE * f, OPTS * opts, int lang)
{
  emit_opts_internal (f, opts, NULL, 1, lang);
}

void
emit_opts_with_exceptions (FILE * f, OPTS * opts, char ** exceptions, int lang)
{
  emit_opts_internal (f, opts, exceptions, 1, lang);
}
