/*
 * Caudium - An extensible World Wide Web server
 * Copyright © 2000-2004 The Caudium Group
 * Copyright © 1994-2001 Roxen Internet Software
 * Some modifications by Francesco Chemolli
 * 
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */
/*
 * $Id: wizard.pike,v 1.16.2.6 2004/01/15 01:57:59 grendel Exp $
 */

//! Wizard generator for Caudium.
//! $Id: wizard.pike,v 1.16.2.6 2004/01/15 01:57:59 grendel Exp $
//! @example
//!   mapping(string:array) wizard_automaton = ([
//!     "foo": ({dispatch, prev_page, next_page, page_name}),
//!     ...
//!   ]);
//!
//!   dispatch:  A string redirects the wizard to that page. 0 or the
//!    name of this page continues with it.
//!   prev_page: A string is the page for the previous button. 0 if none.
//!   next_page: A string is the page for the next button. 0 makes an
//!    ok button instead. -1 gives neither button.
//!   page_name: Optional page name. Defaults to "".
//!
//!   Any of these may be a function that returns the value:
//!
//!   lambda (object id, string page, mixed ... args);
//!
//!   id:    Request id.
//!   page:  The entry in wizard_automaton.
//!   args:  The extra args to wizard_menu() or wizard_for().
//!
//!   Other callbacks:
//!
//!   o  string|mapping page_foo (object id, mixed ... args);
//!
//!      The function that produces page "foo". May return 0 to cause a
//!      redirect to the next page (or the previous one if the user
//!      pressed previous to enter it). Goes to the done page if there
//!      isn't any such page.
//!
//!   o  int verify_foo (object id, mixed ... args);
//!
//!      A verify function that's run when the user presses next or ok in
//!      page "foo". Returns 0 if it's ok to leave the page, anything
//!      else re-runs the page function to redisplay the page.
//!
//!   o  string|mapping wizard_done (object id, mixed ... args);
//!
//!      The function for the done page, from which it's not possible to
//!      return to the other wizard pages. May return 0 to skip the page.
//!      (Differences from old behavior: -1 is not a meaningful return
//!      value and a string is run through parse_wizard_page().)
//!
//!   The dispatch function is always run before any other function for a
//!   page. That makes it suitable to contain all page init stuff and
//!   also state sanity checks, since it can bail to other pages on
//!   errors etc. The intention is also that it should be shared between
//!   several pages that has init code in common.
//!
//!   Special pages:
//!
//!   "start": Start page.
//!   "done":  Finish page. The page function is wizard_done(). Only
//!    dispatch is used in the wizard_automaton entry.
//!   "cancel":  Cancels the wizard. Has no wizard_automaton entry.
//!
//! @bugs
//!   There's no good way to share variables between the functions.
//!   Can we say id->misc? 

inherit "caudiumlib";


#ifdef DEBUG_WIZARD
#define DEBUGMSG(msg) report_debug (msg)
#else
#define DEBUGMSG(msg) do {} while (0)
#endif

//!
string loc_encode(string val, void|mapping args, void|string def)
{
  string quote = args->quote || def || "html";

  switch (quote) {
  case "html": return html_encode_string(val); break;
  case "none": return val; break;
  }
  return val;
}

//!
string wizard_tag_var(string n, mapping m, mixed a, mixed b)
{
  object id;
  string res;
  if(n=="cvar") // Container. Default value in 'a', id in 'b'.
  {
    id = b;
    if(m->type == "select" || m->type == "select_multiple")
      if (m->parse)
        m->options = (replace(parse_rxml( a, id ), ",", "__CoMma__") / "\n" -
          ({ "" })) * ",";
      else
  m->options = (a / "\n") * ",";
    else if (m->parse)
      m["default"] = parse_rxml( a, id );
    else
      m["default"] = a;
  } else // tag. No contents, id in 'a'.
    id = a;

  string current = id->variables[m->name] || m["default"];
  if(current)
    current = current +"";
  switch(m->type)
  {
   default: // String or password field or hidden value....
    if((m->type != "password") && (m->type != "hidden"))
      m->type = "text";
    m_delete(m,"default");
    m->value = loc_encode(current||m->value||"", m, "none");
    if(!m->size)m->size="60,1";
    return make_tag("input", m);

   case "list": // String....
    string n = m->name;
    res="<table cellpadding=0 cellspacing=0 border=0>";
    if(!id->variables[n]) id->variables[n]=current;
    
    m->type = "text";
    if(!m->size)m->size="60,1";
    m_delete(m,"default");
    foreach((current||"")/"\0"-({""}), string v)
    {
      res+="<tr><td>"+loc_encode(v, m, "html")+"</td><td><font size=-2>";
      m->name="_delete_"+n+":"+v;
      m->value = " Remove ";
      m->type = "submit";
      res+=make_tag("input",m)+"</td></tr>";
    }
    m->name = "_new_"+n;
    m->type = "text";
    m->value = "";
    res+= "<tr><td>"+make_tag("input", m)+"</td><td><font size=-2>";
    m->name="_Add";
    m->value = " Add ";
    m->type = "submit";
    res+=make_tag("input",m)+"</font></td></tr>";
    res+="</table>";
    return res;

   case "text":
    m_delete(m,"type");
    m_delete(m,"default");
    m_delete(m, "value");
    if(!m->rows)m->rows="6";
    if(!m->cols)m->cols="40";
    return make_container("textarea", m, loc_encode(current||"", m, "html"));

   case "radio":
    m_delete(m,"default");
    if((!id->variables[m->name] && current) || (current==m->value))
      m->checked="checked";
    return make_tag("input",m);

    // this should work, more or less. Now the case of two or more
    // checkboxes sharing the same name with different values _is_
    // supported, as is the case of a checkbox not having a value.
    // What is not well supported is the case
    // of a checkbox having value 0, so avoid it unless you're sure :)
    // /Francesco
    // FIXME: value cannot be "0".
    //        name cannot be "0" or "1".
    //          Should be fixed be separating current and default.
    // /ZinO
   case "checkbox":
    m_delete(m,"default");
    if (!m->value) m->value="on";
    if (current && current != "0" &&
  (current == "1"||mkmultiset(current/"\0")[m->value]))
      m->checked="checked";
    res=make_tag("input",m);
    m->type="hidden";
    m->value="0";
    return res+make_tag("input", m);

   case "int":
    m->type = "number";
    m_delete(m,"default");
    m->value = (string)((int)current);
    if(!m->size)m->size="8,1";
    return make_tag("input", m);

   case "float":
    m->type = "number";
    m_delete(m,"default");
    m->value = (string)((float)current);
    if(!m->size)m->size="14,1";
    return make_tag("input", m);

   case "color":
     int h, s, v;
     if(id->variables[m->name+".hsv"]) 
       sscanf(id->variables[m->name+".hsv"], "%d,%d,%d", h, s, v);
     else
     {
       array tmp = Colors.rgb_to_hsv(@Colors.parse_color(current||"black"));
       h = tmp[0]; s = tmp[1];  v = tmp[2];
     } 
     if(id->variables[m->name+".foo.x"]) {
       h = (int)id->variables[m->name+".foo.x"];
       v = 255-(int)id->variables[m->name+".foo.y"];
     } else if(id->variables[m->name+".bar.y"])
       s=255-(int)id->variables[m->name+".bar.y"];
     else if(id->variables[m->name+".entered"] &&
       strlen(current=id->variables[m->name+".entered"]))
     {
       array tmp = Colors.rgb_to_hsv(@Colors.parse_color(current||"black"));
       h = tmp[0]; s = tmp[1];  v = tmp[2];
     }

     m_delete(id->variables, m->name+".foo.x");
     m_delete(id->variables, m->name+".foo.y");
     m_delete(id->variables, m->name+".bar.x");
     m_delete(id->variables, m->name+".bar.y");
     id->variables[m->name+".hsv"] = h+","+s+","+v;

     array a=Colors.hsv_to_rgb(h,s,v);
     string bgcol=sprintf("#%02x%02x%02x",a[0],a[1],a[2]); 
     id->variables[m->name] = bgcol;
     return 
     ("<table><tr>\n"
      "<td width=258 rowspan=2>\n"
      "  <table bgcolor=black cellpadding=1 border=0 cellspacing=0 width=258><tr><td>\n"
      "  <input type=image name='"+m->name+".foo' src='/(internal,image)/caudium-colsel' width=256 height=256 border=0></td>"
      "</table>\n"
      "</td>\n"
      "<td width=30 rowspan=2></td>\n"
      "<td width=32 rowspan=2>\n"
      "  <table bgcolor=black cellpadding=1 border=0 cellspacing=0 width=32><tr><td>\n"
      "<input type=image src=\"/(internal,image)/colorbar:"+
      (string)h+","+(string)v+","+(string)s+"\" "
      "name='"+m->name+".bar' width=30 height=256 border=0></td>"
      "</table>\n"
      "</td>\n"
      "<td width=32 rowspan=2></td>\n"
      "<td width=120>\n"
      "  <table bgcolor=black cellpadding=1 border=3 cellspacing=0 width=90>\n"
      "  <tr><td height=90 width=90 bgcolor="+bgcol+">&nbsp;"+
      (m->tt?"<font color='"+m->tc+"'>"+m->tt+"</font>":"")
      +"</td></table>\n"
      "</td><tr>\n"
      "<td width=120>\n"
      "<b>R:</b> "+(string)a[0]+"<br>\n"
      "<b>G:</b> "+(string)a[1]+"<br>\n"
      "<b>B:</b> "+(string)a[2]+"<br>\n"
      "<hr size=2 align=left noshade width=70>\n"
      "<b>H:</b> "+(string)h+"<br>\n"
      "<b>S:</b> "+(string)s+"<br>\n"
      "<b>V:</b> "+(string)v+"<br>\n"
      "<hr size=2 align=left noshade width=70>\n"+
      "<font size=-1><input type=string name="+
      m->name+".entered size=8 value='"+
      Image.Color(@a)->name()+"'> <input type=submit value=Ok></font></td></table>\n");

  case "color-small":
     if(id->variables[m->name+".hsv"]) 
       sscanf(id->variables[m->name+".hsv"], "%d,%d,%d", h, s, v);
     else
     {
       array tmp = Colors.rgb_to_hsv(@Colors.parse_color(current||"black"));
       h = tmp[0]; s = tmp[1];  v = tmp[2];
     } 
     if(id->variables[m->name+".foo.x"]) {
       h = ((int)id->variables[m->name+".foo.x"])*2;
       v = 255-((int)id->variables[m->name+".foo.y"])*2;
     } else if(id->variables[m->name+".bar.y"])
       s = 255-((int)id->variables[m->name+".bar.y"])*2;
     else if(id->variables[m->name+".entered"] &&
       strlen(current=id->variables[m->name+".entered"]))
     {
       array tmp = Colors.rgb_to_hsv(@Colors.parse_color(current||"black"));
       h = tmp[0]; s = tmp[1];  v = tmp[2];
     }

     m_delete(id->variables, m->name+".foo.x");
     m_delete(id->variables, m->name+".foo.y");
     m_delete(id->variables, m->name+".bar.x");
     m_delete(id->variables, m->name+".bar.y");
     id->variables[m->name+".hsv"] = h+","+s+","+v;

     a=Colors.hsv_to_rgb(h,s,v);
     bgcol=sprintf("#%02x%02x%02x",a[0],a[1],a[2]); 
     id->variables[m->name] = bgcol;
     return 
     ("<table border=0 cellpadding=0 cellspacing=0><tr>\n"
      "<td rowspan=2>\n"
      "  <table bgcolor=black cellpadding=1 border=0 cellspacing=0><tr><td>\n"
      "    <input type=image name='"+m->name+".foo' "
      "      src='/(internal,image)/colsel-small' "
      "      width=128 height=128 border=0></td>"
      "  </table>\n"
      "</td>\n"
      "<td width='8' rowspan='2'><img src='/(internal,image)/unit' width='8'></td>\n"
      "<td width=18 rowspan=2>\n"
      "  <table bgcolor=black cellpadding=1 border=0 cellspacing=0><tr><td>\n"
      "    <input type=image src=\"/(internal,image)/colorbar:"+
             (string)h+","+(string)v+","+(string)s+"\" "
      "      name='"+m->name+".bar' width=16 height=128 border=0></td>"
      "  </table>\n"
      "</td>\n"
      "<td width='8' rowspan='2'><img src='/(internal,image)/unit' width='8'></td>\n"
      "<td>\n"
      "  <table bgcolor=black width=64 border=3 "
      "         cellpadding=1 cellspacing=0><tr>\n"
      "    <td height=64 width=64 bgcolor="+bgcol+">&nbsp;"+
             (m->tt?"<font color='"+m->tc+"'>"+m->tt+"</font>":"")+"\n"
      "    </td></tr>\n"
      "  </table>\n"
      "</td>\n"
      "<tr><td width=110>\n"
      "<font size=-1><input type=string name="+
      m->name+".entered size=8 value='"+
      Image.Color(@a)->name()+"'> <input type=submit value=Ok></font>"
      "</td></tr>\n"
      "</table>\n");

   case "font":
     res="";
     m->type = "select";
     m->lines = "20";
     m->choices = available_fonts()*",";
     if(id->conf && id->conf->modules["graphic_text"] && !m->noexample)
       res = ("<input type=submit value='Example'><br>"+
        ((current&&strlen(current))?
         "<gtext nfont='"+current+"'>Example Text</gtext><br>"
         :""));
     m_delete(m, "noexample");
     return make_tag("var", m)+res;

   case "toggle":
    m_delete(m,"default");
    return make_container("select", m,
        "<option"+((int)current?" selected":"")+" value=1>Yes"
        "<option"+(!(int)current?" selected":"")+" value=0>No");

   case "select":
     if(!m->choices && m->options)
       m->choices = m->options;
     m_delete(m,"default");
     m_delete(m,"type");
     mapping m2 = copy_value(m);
     m_delete(m2, "choices");
     m_delete(m2, "options");
     //escape the characters we need for internal purposes..
     m->choices=replace(m->choices,
      ({"\\,", "\\:"}), 
      ({"__CoMma__", "__CoLon__"}));

     return make_container("select", m2, Array.map(m->choices/",",
               lambda(string s, string c, mapping m) {
        string t;
        if(sscanf(s, "%s:%s", s, t) != 2)
    t = s;
  s=replace(s,({"__CoMma__", 
          "__CoLon__"}),({",",":"})); //can't be done before.
  t=replace(t,({"__CoMma__", 
          "__CoLon__"}),({",",":"}));

        return "<option value='"+s+"' "+(s==c?" selected":"")+">"+loc_encode(t, m, "html")+"\n";
     },current,m)*"");


   case "select_multiple":
     if(!m->choices && m->options)
       m->choices = m->options;
    m_delete(m,"default");
    m_delete(m,"type");
    m2 = copy_value(m);
    m_delete(m2, "choices");
    m_delete(m2, "options");
    m2->multiple="1";
    //escape the characters we need for internal purposes..
    m->choices=replace(m->choices,
           ({"\\,", "\\:"}), 
           ({"__CoMma__", "__CoLon__"}));

    return make_container("select", m2, Array.map(m->choices/",",
         lambda(string s, array c, mapping m) {
      string t;
      if(sscanf(s, "%s:%s", s, t) != 2)
        t = s;
      s=replace(s,({"__CoMma__", 
        "__CoLon__"}),({",",":"})); //can't be done before.
      t=replace(t,({"__CoMma__", 
        "__CoLon__"}),({",",":"}));

      return "<option value='"+s+"' "+(search(c,s)!=-1?"selected":"")+">"+loc_encode(t, m, "html")+"\n";
    },(current||"")/"\0",m)*"");
  }
}

//!
mapping decompress_state(string from)
{
  if(!from) return ([]);
  catch {
    from = MIME.decode_base64(from);
    object gz = Gz;
    if(sizeof(indices(gz)))
      from = gz->inflate()->inflate(from);
    return decode_value(from);
  };
  return ([]);
}
  
//!
string compress_state(mapping state)
{
  state = copy_value(state);
  m_delete(state,"_state");
  m_delete(state,"next_page");
  m_delete(state,"prev_page");
  m_delete(state,"help");
  m_delete(state,"action");
  m_delete(state,"unique");

//  werror(sprintf("State=%O\n", state));

  string from = encode_value(state);
  object gz = Gz;
  if(sizeof(indices(gz)))
  {
    string from2 = gz->deflate()->deflate(from);
    /*if(strlen(from2)<strlen(from))*/ from=from2;
  }
  return MIME.encode_base64( from );
}

//!
string parse_wizard_help(string t, mapping m, string contents, object id,
       mapping v)
{
  v->help=1;
  if(!id->variables->help) return "";
  return contents;
}

//!
string make_title()
{
  string s = (this_object()->wizard_name||this_object()->name||"No name") -
    "<p>";
  sscanf(s, "%*s//%s", s);
  sscanf(s, "%*d:%s", s);
  return s;
}

//!
int num_pages(string wiz_name)
{
  int max_page;
  for(int i=0; i<100; i++)
    if(!this_object()[wiz_name+i])
    {
      max_page=i-1;
      break;
    }
  return max_page+1;
}
#define Q(X) replace(X,({"<",">","&","\""}),({"&lt;","&gt;","&amp;","&quote;"}))

#define PAGE Q((this_object()->page_label?this_object()->page_label:"Page "))
#define OK Q((this_object()->ok_label?this_object()->ok_label:"Ok"))
#define CANCEL Q((this_object()->cancel_label?this_object()->cancel_label:"Cancel"))
#define NEXT Q((this_object()->next_label?this_object()->next_label:"Next ->"))
#define PREVIOUS Q((this_object()->previous_label?this_object()->previous_label:"<- Previous"))
#define COMPLETED Q((this_object()->completed_label?this_object()->completed_label:"Completed"))

//!
string parse_wizard_page(string form, object id, string wiz_name, void|string page_name)
{
  mapping(string:array) automaton = this_object()->wizard_automaton;
  int max_page = !automaton && num_pages(wiz_name)-1;
  string res;
  string page = id->variables->_page;
  int pageno = (int)page;
  mapping foo = ([]);
  // Cannot easily be inlined below, believe me... Side-effects.
  form = parse_html(form,(id->misc->extra_wizard_tags || ([])) +
        ([ "var":wizard_tag_var, ]),
        (id->misc->extra_wizard_container || ([])) +
        ([ "cvar":wizard_tag_var, 
           "help":parse_wizard_help]), id, foo );

  // We commonly feed the action variable both from the URL with
  // "...?action=foo.pike" and with an <input> tag from the previous
  // page. Netscape ignores one of them, but IE sends both. Thus we
  // have to discard the extra value in the IE case. (We simply assume
  // both values are the same here; maybe it could be done better.)
  if (stringp (id->variables->action))
    id->variables->action = (id->variables->action/"\0")[0];
  string formname="wizard";
  if(id->misc->wizardformname) formname=id->misc->wizardformname;
  res = ("<!--Wizard-->\n"
         "<form method=get name=\"" + formname + "\">\n" +
   (stringp (id->variables->action) ?
    " <input type=hidden name=action value=\""+id->variables->action+"\">\n" :
    "") +
   " <input type=hidden name=_page value=\""+page+"\">\n"
   " <input type=hidden name=_state value=\""+compress_state(id->variables)+"\">\n"
   "<table bgcolor=black cellpadding=1 border=0 cellspacing=0 width=80%>\n"
   "  <tr><td><table bgcolor=#eeeeee cellpadding=0 "
   "         cellspacing=0 border=0 width=100%>\n"
   "    <tr><td valign=top><table width=100% cellspacing=0 cellpadding=5>\n<tr><td valign=top>\n"
   "<font size=+2>"+make_title()+"</font>"
   " </td>\n<td align=right>"+
   (wiz_name=="done"
    ?COMPLETED
    :page_name || (max_page?PAGE+(pageno+1)+"/"+(max_page+1):""))+
   "</td>\n"
    " \n<td align=right>"+
   (foo->help && !id->variables->help?
    "<font size=-1><input type=image name=help src="+
    (id->conf?"/(internal,image)/help":"/image/help.gif")+
    " border=0 value=\"Help\"></font>":"")
   +"</td>\n"
   " </tr><tr><td colspan=3><table cellpadding=0 cellspacing=0 border=0 width=100%><tr  bgcolor=#000000><td><img src="+
   (id->conf?"/(internal,image)/unit":"/image/unit.gif")+
   " width=1 height=1 alt=\"\"></td></tr></table></td></tr>\n"
   "  </table><table cellpadding=6><tr><td>\n"
   "<!-- The output from the page function -->\n"
   +form+
   "\n<!-- End of the output from the page function -->\n"
   "\n</td></tr></table>\n"
   "      <table width=100%><tr><td width=33%>"+
   (((automaton ? stringp (id->variables->_prev) : pageno>0) &&
     wiz_name!="done")?
    "        <input type=submit name=prev_page value=\""+PREVIOUS+"\">":"")+
   "</td><td width=33% align=center >"+
   (wiz_name!="done"
    ?(((automaton ? !id->variables->_next : pageno==max_page)
       ?"        <input type=submit name=ok value=\" "+OK+" \">"
       :"")+
      "         <input type=submit name=cancel value=\" "+CANCEL+" \">")
    :"         <input type=submit name=cancel value=\" "+OK+" \">")+
    "</td>"
   "</td><td width=33% align=right >"+
   (((automaton ? stringp (id->variables->_next) : pageno!=max_page) &&
     wiz_name!="done")?
    "        <input type=submit name=next_page value=\""+NEXT+"\">":"")+
   "</td></tr></table>"
   "    </td><tr>\n"
   "  </table>\n"
   "  </td></tr>\n"
         "</table>\n"
         " </form>\n"
    );
  return res;
}


#define PAGE(X)  ((string)(((int)v->_page)+(X)))

//!
mapping|string wizard_for(object id,string cancel,mixed ... args)
{
  string data;
  int offset = 1;
  mapping v=id->variables;
  string wiz_name = "page_";

  mapping s = decompress_state(v->_state);

  if(v->cancel) 
  {
     return http_redirect(s->cancel_url||cancel||id->not_query, @(id->conf?({id}):({})));
  }

  foreach(indices(s), string q)
     v[q] = v[q]||s[q];

  mapping(string:array) automaton = this_object()->wizard_automaton;
  function dispatcher;
  string oldpage, page_name;
  if (automaton && (!v->_page || v->next_page || v->prev_page || v->ok)) {
    if (!v->_page && automaton->start) v->_page = "start";
    oldpage = v->_page;
    if (v->_page) {
      array page_state = automaton[v->_page];
      if (!page_state) return "Internal error in wizard code: "
       "No entry " + v->_page + " in automaton.";
      function|string redirect = page_state[0];
      if (functionp (redirect)) {
  dispatcher = redirect;
  DEBUGMSG (sprintf ("Wizard: Running dispatch function %O for page %s\n",
         redirect, v->_page));
  redirect = redirect (id, v->_page, @args);
      }
      if (stringp (redirect) && redirect != v->_page) {
  DEBUGMSG ("Wizard: Internal redirect to page " + redirect + "\n");
  // Redirect takes precedence over the user choice.
  m_delete (v, "next_page");
  m_delete (v, "prev_page");
  m_delete (v, "ok");
  v->_page = redirect;
      }
    }
  }

  if(v->next_page)
  {
    function c=this_object()["verify_"+v->_page];
    int fail = 0;
    if (functionp (c)) {
      fail = c (id, @args);
      DEBUGMSG (sprintf ("Wizard: Verify function %O %s\n", c,
       fail ? "failed" : "succeeded"));
    }
    if (!fail) {
      v->_page = automaton ? v->_next : PAGE(1);
      DEBUGMSG ("Wizard: Going to next page\n");
    }
  }
  else if(v->prev_page)
  {
    v->_page = automaton ? v->_prev : PAGE(-1);
    DEBUGMSG ("Wizard: Going to previous page\n");
    offset=-1;
  }
  else if(v->ok)
  {
    function c=this_object()["verify_"+v->_page];
    int fail = 0;
    if (functionp (c)) {
      fail = c (id, @args);
      DEBUGMSG (sprintf ("Wizard: Verify function %O %s\n", c,
       fail ? "failed" : "succeeded"));
    }
    if(!fail)
      if (automaton) v->_page = 0; // Handle done state in the automaton code below.
      else
      {
  mixed res;
  if(c=this_object()->wizard_done) {
    DEBUGMSG ("Wizard: \"Ok\" pressed; running wizard_done\n");
    res = c(id,@args);
  }
  if(res != -1)
    return (res
      || http_redirect(s->cancel_url||cancel||id->not_query, 
           @(id->conf?({id}):({}))));
  DEBUGMSG ("Wizard: -1 from wizard_done; continuing\n");
      }
  }
  else if(v["help.x"])
  {
    m_delete(v, "help.x");
    m_delete(v, "help.y");
    v->help="1";
  } 
  foreach(indices(id->variables), string n)
  {
    string q,on=n;
    if(sscanf(n, "_new_%s", n))
    {
      if((v->_Add) && strlen(v[on]-"\r"))
      {
  if(v[n]) v[n]+="\0"+v[on];
  else v[n]=v[on];
  m_delete(v, on);
  m_delete(v, "_Add");
      }
    } else if(sscanf(n, "_delete_%s:%s", n,q)==2) {
      if(v[n]) v[n]=replace(replace(v[n]/"\0",q,"")*"\0","\0\0","\0");
      m_delete(v, on);
    } 
  }

  if (automaton) {
    int i = 0;
    while (1) {
      if (++i == 4711) return "Internal error in wizard code: "
       "Probably infinite redirect loop in automaton.";

      if (v->_page == "cancel") {
  string to = s->cancel_url||cancel||id->not_query;
  DEBUGMSG ("Wizard: Canceling with redirect to " + to + "\n");
  return http_redirect(to, @(id->conf?({id}):({})));
      }

      if (!v->_page) v->_page = "done", oldpage = 0;
      function|string redirect = 0;
      array page_state = automaton[v->_page];
      if (!page_state && v->_page != "done")
  return "Internal error in wizard code: No entry " + v->_page + " in automaton.";

      if (page_state && v->_page != oldpage) {
  redirect = page_state[0];
  if (functionp (redirect)) {
    if (dispatcher == redirect)
      // The previous page state had the same dispatcher as this
      // one; it's unnecessary to re-run it since it shouldn't
      // change its mind. This is also important since most of
      // the heavy work is often done there.
      dispatcher = redirect = 0;
    else {
      dispatcher = redirect;
      DEBUGMSG (sprintf ("Wizard: Running dispatch function %O for page %s\n",
             dispatcher, v->_page));
      redirect = dispatcher (id, v->_page, @args);
    }
  }
  else dispatcher = 0;
      }
      oldpage = 0;

      if (redirect && redirect != v->_page) {
  DEBUGMSG ("Wizard: Internal redirect to page " + redirect + "\n");
  v->_page = redirect;
      }
      else if (v->_page == "done") {
  function donefn = this_object()->wizard_done;
  if (!functionp (donefn))
    return "Internal error in wizard code: No wizard_done function.";
  DEBUGMSG ("Wizard: Running wizard_done\n");
  data = donefn (id, @args);
  if (!data) return http_redirect(cancel||id->not_query,
          @(id->conf?({id}):({})));
  wiz_name = "done";
  break;
      }
      else {
  function pagefn = this_object()[wiz_name + v->_page];
  if (!functionp (pagefn)) return "Internal error in wizard code: "
           "No page function for " + v->_page + ".";
  DEBUGMSG (sprintf ("Wizard: Running page function %O\n", pagefn));
  data = pagefn (id, @args);
  if (data) {
    id->variables->_prev = functionp (page_state[1]) ?
      page_state[1] (id, v->_page, @args) : page_state[1];
    id->variables->_next = functionp (page_state[2]) ?
      page_state[2] (id, v->_page, @args) : page_state[2];

    // So that dispatch functions may be used for prev/next too.
    if ((<"cancel", "done">)[id->variables->_prev]) id->variables->_prev = 0;
    if ((<"cancel", "done">)[id->variables->_next]) id->variables->_next = 0;

    page_name = sizeof (page_state) < 4 ? "" : functionp (page_state[3]) ?
      page_state[3] (id, v->_page, @args) : page_state[3];
    DEBUGMSG ("Wizard: prev_page " + id->variables->_prev + ", next_page " +
        id->variables->_next + ", page_name \"" + page_name + "\"\n");
    break;
  }
  else {
    int dir = offset > 0 ? 2 : 1;
    v->_page =
      functionp (page_state[dir]) ?
      page_state[dir] (id, v->_page, @args) : page_state[dir];
    DEBUGMSG ("Wizard: No data from page function; going to " +
        (stringp (v->_page) ? (offset > 0 ? "next" : "previous") +
         " page " + v->_page : "done page") + "\n");
    if (!stringp (v->_page)) v->_page = 0;
  }
      }
    }
  }
  else
    for(; !data; v->_page=PAGE(offset))
    {
      function pg=this_object()[wiz_name+((int)v->_page)];
      function c = !pg && this_object()["wizard_done"];
      if(functionp(c)) {
  DEBUGMSG ("Wizard: Running wizard_done\n");
  mixed res = c(id,@args);
  if(res != -1) 
    return (res
      || http_redirect(cancel||id->not_query, 
           @(id->conf?({id}):({}))));
      }
      if(!pg) return "Internal error in wizard code: Invalid page ("+v->_page+")!";
      DEBUGMSG (sprintf ("Wizard: Running page function %O\n", pg));
      if(data = pg(id,@args)) break;
      DEBUGMSG ("Wizard: No data from page function; going to " +
    (offset > 0 ? "next" : "previous") + " page\n");
    }

  // If it's a mapping we can presume it is an http response, and return
  // it directly.
  if (mappingp(data))
    return data;

  return parse_wizard_page(data,id,wiz_name,page_name);
}

mapping wizards = ([]);
string err;

//!
object get_wizard(string act, string dir, mixed ... args)
{
  act-="/";
  if(!wizards[dir+act]) wizards[dir+act]=compile_file(dir+act)(@args);
  return wizards[dir+act];
}

int zonk=time();

//!
mapping get_actions(object id, string base,string dir, array args)
{
  mapping acts = ([  ]);
  if(id->pragma["no-cache"]) wizards=([]);
  
  foreach(get_dir(dir), string act)
  {
    object e = ErrorContainer();
    mixed err;
    master()->set_inhibit_compile_errors(e);
    err = catch
    {
      if(!(<'#', '_'>)[act[0]] && act[-1]=='e')
      {
  string sm,rn = (get_wizard(act,dir,@args)->name||act), name;
  if(sscanf(rn, "%*s:%s", name) != 2) name = rn;
  sscanf(name, "%s//%s", sm, name);
  if(!acts[sm]) acts[sm] = ({ ([]) });

  if(id->misc->raw_wizard_actions)
    acts[sm][0][name]=
      ({ name, base, (["action":act,"unique":(string)(zonk++) ]),
      (get_wizard(act,dir,@args)->doc||"") });
  else
    acts[sm]+=
      ({"<!-- "+rn+" --><dt><font size=\"+2\">"
        "<a href=\""+base+"?action="+act+"&unique="+(zonk++)+"\">"+
        name+"</a></font><dd>"+(get_wizard(act,dir,@args)->doc||"")});
      }
    };
    if(strlen(e->get()))
      report_error("Error compiling action:\n"+ e->get());
    else if(err)
      report_error(describe_backtrace(err));
    e->print_warnings("Warnings while compiling action:");
  }
  master()->set_inhibit_compile_errors(0);
  master()->clear_compilation_failures();
  return acts;
}

//!
string act_describe_submenues(array menues, string base,string sel)
{
  if(sizeof(menues)==1) return "";
  string res = "<font size=+3>";
  foreach(sort(menues), string s)
    res+=
      (s==sel?"<li>":"<font color=#eeeeee><li></font><a href=\""+base+"?sm="+replace(s||"Misc"," ","%20")+
       "&uniq="+(++zonk)+"\">")+(s||"Misc")+
      (s==sel?"<br>":"</a><br>")+"";
  return res + "</font>";
}

string focused_wizard_menu;

//!
mixed wizard_menu(object id, string dir, string base, mixed ... args)
{
  mapping acts;
  if(id->pragma["no-cache"]) wizards=([]);
  
  if(!id->variables->sm)
    id->variables->sm = focused_wizard_menu;
  else
    focused_wizard_menu = id->variables->sm=="0"?0:id->variables->sm;
  
  if(!id->variables->action)
  {
    mixed wizbug;
    wizbug = catch {
      mapping acts = get_actions(id, base, dir, args);
      if(id->misc->raw_wizard_actions)
  return acts[id->variables->sm];
      string res;
      res= ("<table cellpadding=3><tr><td valign=top bgcolor=#eeeeee>"+
      act_describe_submenues(indices(acts),base,id->variables->sm)+
      "</td>\n\n<td valign=top>"+
      (sizeof(acts)>1 && acts[id->variables->sm]?"<font size=+3>"+
       (id->variables->sm||"Misc")+"</font><dl>":"<dl>")+
      (sort(acts[id->variables->sm]||({}))*"\n")+
      "</dl></td></tr></table>"+
      (err && strlen(err)?"<pre>"+err+"</pre>":""));
      err="";
      return res;
    };
    if(wizbug)
      err = describe_backtrace(wizbug);
    if(err && strlen(err)) {
      string res="<pre>"+err+"</pre>";
      err="";
      return res;
    }
  } else {
    object o = get_wizard(id->variables->action,dir);
    if(!o) {
      mixed res = "<pre>"+err+"</pre>";
      err="";
      return res;
    }
    mixed res= o->wizard_for(id,base,@args);
    err="";
    return res;
  }
  return "<pre>The Wizard is confused.</pre>";
}

/*** Additional Action Functions ***/

//!
string format_numeric(string s, string|void sep)
{
  sep = reverse(sep||"&nbsp;");
  array(string) as = s/" ";
  string t = "";
  s = reverse(as[0]);
  while(sizeof(s)) {
    if(sizeof(s) > 3)
      t += s[0..2]+sep;
    else
      t += s;
    s = s[3..];
  }
  return (({reverse(t)})+as[1..])*" "; 
}

//!
string html_table(array(string) subtitles, array(array(string)) table,
      mapping|void opt)
{
  /* Options:
   *   bgcolor, titlebgcolor, titlecolor, fgcolor0, fgcolor1, modulo
   * Containers:
   *   <fields>[num|text, ...]</fields>
   */

  string r = "";

  if(!opt) opt = ([]);
  int m = (int)(opt->modulo?opt->modulo:1);
  r += ("<table bgcolor=\""+(opt->bgcolor||"black")+"\" border=\"0\" "
  "cellspacing=\"0\" cellpadding=\"1\">\n"
  "<tr><td>\n");
  r += "<table border=\"0\" cellspacing=\"0\" cellpadding=\"4\">\n";
  r += "<tr bgcolor=\""+(opt->titlebgcolor||"#113377")+"\">\n";
  int cols;
  foreach(subtitles, mixed s)
  {
    if(stringp(s))
    {
      r+=("<th nowrap align=\"left\"><font color=\""+
    (opt->titlecolor||"#ffffff")+"\">"+s+" &nbsp; </font></th>");
      cols++;
    } else {
      r+=("</tr><tr bgcolor=\""+(opt->titlebgcolor||"#113377")+"\">"
    "<th nowrap align=\"left\" colspan=\""+cols+"\">"
    "<font color=\""+(opt->titlecolor||"#ffffff")+"\">"+s[0]+
    " &nbsp; </font></th>");
    }
  }      
  r += "</tr>";
  
  for(int i = 0; i < sizeof(table); i++) {
    string tr;
    r += tr = "<tr bgcolor=\""+((i/m)%2?opt->fgcolor1||"#ddeeff":
            opt->fgcolor0||"#ffffff")+"\">";
    for(int j = 0; j < sizeof(table[i]); j++) {
      mixed s = table[i][j];
      if(arrayp(s))
  r += "</tr>"+tr+"<td colspan=\""+cols+"\">"+s[0]+" &nbsp;</td>";
      else {
  string type = "text";
  if(arrayp(opt->fields) && j < sizeof(opt->fields))
    type = opt->fields[j];
  switch(type) {
  case "num":
    array a = s/".";
    r += "<td nowrap align=\"right\">";
    if(sizeof(a) > 1) {
      r += (format_numeric(a[0])+"."+
      reverse(format_numeric(reverse(a[1]), ";psbn&")));
    } else
      r += format_numeric(s, "&nbsp;");
    break;
  case "text":
  default:
    r += "<td nowrap>"+s;
  }
  r += "&nbsp;&nbsp;</td>";
      }
    }
    r += "</tr>\n";
  }
  r += "</table></td></tr>\n";
  r += "</table><br>\n";
  return r;
}

//!
string html_notice(string notice, object id)
{
  return ("<table><tr><td valign=top><img \nalt=Notice: src=\""+
        (id->conf?"/(internal,image)/":"/image/")
        +"err_1.gif\"></td><td valign=top>"+notice+"</td></tr></table>");
}

//!
string html_warning(string notice, object id)
{
  return ("<table><tr><td valign=top><img \nalt=Warning: src=\""+
        (id->conf?"/(internal,image)/":"/image/")
        +"err_2.gif\"></td><td valign=top>"+notice+"</td></tr></table>");
}

//!
string html_error(string notice, object id)
{
  return ("<table><tr><td valign=top><img \nalt=Error: src=\""+
        (id->conf?"/(internal,image)/":"/image/")
        +"err_3.gif\"></td><td valign=top>"+notice+"</td></tr></table>");
}

//!
string html_border(string what, int|void width, int|void ww,
       string|void bgcolor, string|void bdcolor)
{
  return ("<table border=0 cellpadding="+(width+1)+" cellspacing=0 "
    "bgcolor="+(bdcolor||"black")+
    "><tr><td><table border=0 cellpadding="+(ww)+
    " cellspacing=0 bgcolor="+(bgcolor||"white")+
    "><tr><td>"+what+"</tr></td></table>"
          "</td></tr></table>");
}

//!
void filter_checkbox_variables(mapping v)
{
  foreach(indices(v), string s) {
    if(!v[s] || (v[s]=="0"))
      m_delete(v,s);
    else 
      v[s]-="\00";
  }
}

/*
 * If you visit a file that doesn't contain these lines at its end, please
 * cut and paste everything from here to that file.
 */

/*
 * Local Variables:
 * c-basic-offset: 2
 * End:
 *
 * vim: softtabstop=2 tabstop=2 expandtab autoindent formatoptions=croqlt smartindent cindent shiftwidth=2
 */
