/* 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);
}