/* fl-xport.js

   Copyright (c) 2005 W3C (MIT, ERCIM, Keio), All Rights Reserved.
   W3C liability, trademark, document use and software licensing
   rules apply, see:

   http://www.w3.org/Consortium/Legal/copyright-documents
   http://www.w3.org/Consortium/Legal/copyright-software

   This is designed to be used together with forms-lite.js and
   provides support for export to the XForms model and instance

Implementation issues/questions:

 - how to create XML dom in new window with namespaces
   that can be viewed on IE,FF,Opera, etc.
 - how to do view source for XHTML+XForms mix?
 - xsd types for number (xsd:double) and date (xsd:date)
 - conversion of date values to xsd format
 - how to translate min, max for bind?
 - note: html can't generate attributes in XForms instance
 - how to translate expressions into xpath?
 - how to translate disabled fields?
 - how to translate repeated fieldsets?
 - how to translate radio buttons in general?
 - how to translate check boxes within fieldsets?
 - how to present in new tab as view-source?
 - is there a way to set this along with MIME type?
 - should I set xmlns="" on xf:instance and avoid my: ??
 - how to deal with sumover(x,y) and other functions?

 http://www.w3.org/MarkUp/Forms/2003/xforms-for-html-authors.html
 http://xformsinstitute.com/essentials/browse/book.php#ch01-10-fm2xml

XPath: mappings (modulo escaping of & < and >)

  true    becomes   true()
  false   becomes   false()
  eval(x) becomes   x
  bool(x) becomes   boolean(x)
   ||     becomes   or
   &&     becomes   and
   /      becomes   div
   %      becomes   mod
   ==     becomes   =
   !x     becomes   not(x)
   a.b    becomes   a/b

 Parsing eval(x) is hard using regular expressions due to need
 for balancing brackets. May be easier to generate direct from
 HTML attributes!

 For radio buttons and for checkboxes with multiple fields per
 name the data model should have one entry per name. In the
 content, the first field for the name should become a select
 element, subsequent fields for the same name are discarded.

 In HTML each radio button get its own entry in form.fields,
 but there is only one entry per named group of checkboxes!

 For checkboxes with only one field per name, the datamodel
 is xsd:boolean and the content becomes a select:

   <xf:select ref="foo" appearence="full>
     <!-- no overall label -->
     <xf:item>
       <xf:label>Want foo</xf:label>
       <xf:value>foo</xf:value>
     <xf:item>
   </xf:select>

 select without "multiple" is mapped to xf:select1
 otherwise select is mapped t xf:select

 How should forms-lite support triggered actions
 e.g. to add or remove rows in repeating fieldset?

 "form.elements" includes all input, select, button and
 fieldset elements, except for konqueror which excludes
 fieldset elements from the list.

 ***************************************************
 Next steps:

 - use form element to initialize xf:submission element
 - named fieldset maps to nested elements in instance
 - textarea maps to xf:textarea
 - select maps to xf:select or xf:select1
 - radio group maps to single instance element + xf:select1
 - checkboxes within a named fieldset map to xf:select
 - generate bind element with constraint for min and max
 - generate range element with constraint for min, max and step
 - avoid eval() appearing in translated expressions (but how?)
 - repeating fieldsets (generated from data model)
 
*/

//given form object return DOM node for model
function exportModel(form)
{
  if (!form && !form.fields && !form.fields.length)
    return null;

  var model = document.createElement("xf:model");

  var instance = document.createElement("xf:instance");
  appendLineBreak(model, "  ");
  model.appendChild(instance);

  for (var i = 0; i < form.elements.length; ++i)
  {
    var field = form.elements[i];
     exportField(model, instance, field, "my");
  }

  appendLineBreak(instance, "  ");
  appendLineBreak(model);
  return model;
}

// ns is namespace prefix for instance elements
function exportField(model, instance, field, ns)
{
  if (!field.type || field.type == "fieldset" || !field.name)
    return;

  // exclude disabled fields from XForms model
  if (field.disabled)
    return;

  var form = field.form;
  var group = false;
  var f = form[field.name];

  if (f && f != field)
  {
    if (f.length && f[0] != field)
      return;

    group = true;
  }


  // for dates I need to generate value in the xsd:date format
  // the following is missing support for select-multiple
  // and an example needs to be added to 16/index.html

  var bind = null;
  var el = null;

  switch (field.datatype)
  {
    case "number":  // Map ECMAScript number type to double
      el = document.createElement(ns+":"+field.name);
      appendLineBreak(instance, "    ");
      instance.appendChild(el);
      bind = document.createElement("xf:bind");
      bind.setAttribute("nodeset", "/"+ns+":"+field.name);
      bind.setAttribute("type", "xsd:double");
      break;

    case "date":
      el = document.createElement(ns+":"+field.name);
      appendLineBreak(instance, "    ");
      instance.appendChild(el);
      bind = document.createElement("xf:bind");
      bind.setAttribute("nodeset", "/"+ns+":"+field.name);
      bind.setAttribute("type", "xsd:date");
      break;

    case "radio":
      var value = radioValue(field.form[field.name]);

      if (value)
      {
        el = document.createElement(ns+":"+field.name);
        appendLineBreak(instance, "    ");
        instance.appendChild(el);
        el.appendChild(document.createTextNode(value));
      }
      break;

    case "checkbox":
      if (group)
      {
        var fields = field.form[field.name];

        for (var i = 0; i < fields.length; ++i)
        {
          if (fields[i].checked)
          {
            el = document.createElement(ns+":"+field.name);
            appendLineBreak(instance, "    ");
            instance.appendChild(el);

            var value = fields[i].value;

            if (value)
              el.appendChild(document.createTextNode(fields[i].value));
          }
        }

        break;
      }

      // treat non-grouped checkbox as boolean
      el = document.createElement(ns+":"+field.name);
      appendLineBreak(instance, "    ");
      instance.appendChild(el);
      el.appendChild(document.createTextNode(
                     field.checked ? "true" : "false"));
      bind = document.createElement("xf:bind");
      bind.setAttribute("nodeset", "/"+ns+":"+field.name);
      bind.setAttribute("type", "xsd:boolean");
      break;

    default:
      el = document.createElement(ns+":"+field.name);
      appendLineBreak(instance, "    ");
      instance.appendChild(el);
      value = field.value;

      if (value)
        el.appendChild(document.createTextNode(field.value));
      break;
  }

  // is this correct or should it test the attribute?
  if (field.readonly)
  {
    if (!bind)
    {
      bind = document.createElement("xf:bind");
      bind.setAttribute("nodeset", "/"+ns+":"+field.name);
    }

    bind.setAttribute("readonly", "true()");
  }

  if (field.regexp)
  {
    if (!bind)
    {
      bind = document.createElement("xf:bind");
      bind.setAttribute("nodeset", "/"+ns+":"+field.name);
    }

    bind.setAttribute("pattern", field.getAttribute("pattern"));
  }

  // the following needs changing to read the attribute
  // rather than the munged string prepared for eval()

  if (field.reqexpr)
  {
    if (!bind)
    {
      bind = document.createElement("xf:bind");
      bind.setAttribute("nodeset", "/"+ns+":"+field.name);
    }

    bind.setAttribute("required", exportExpression(field.reqexpr, ns));
  }

  if (field.relevant)
  {
    if (!bind)
    {
      bind = document.createElement("xf:bind");
      bind.setAttribute("nodeset", "/"+ns+":"+field.name);
    }

    bind.setAttribute("relevant", exportExpression(field.relevant, ns));
  }

  if (field.constraint)
  {
    if (!bind)
    {
      bind = document.createElement("xf:bind");
      bind.setAttribute("nodeset", "/"+ns+":"+field.name);
    }

    if (!bind)
    {
      bind = document.createElement("xf:bind");
      bind.setAttribute("nodeset", "/"+ns+":"+field.name);
    }

    bind.setAttribute("constraint", exportExpression(field.constraint, ns));
  }

  if (field.calc)
  {
    bind.setAttribute("calculate", exportExpression(field.calc, ns));
  }

  if (bind)
  {
    appendLineBreak(model, "  ");
    model.appendChild(bind);
  }
}

function appendLineBreak(element, indent)
{
  var str = indent ? "\r\n"+indent : "\r\n";
  element.appendChild(document.createTextNode(str));
}

/*
 For expressions, 
   map "form." to "\my:"
   map ".value" to ""
   map ".checked" to ""
   map . to /my: except for \d\.\d  (but how??)
   map true to true() and false to false()

 what about infix operators in general?
*/
function exportExpression(e, ns)
{
  e = e.replace(/\.value/g, "");
  e = e.replace(/\.checked/g, "");
  e = e.replace(/true/g, "true()");
  e = e.replace(/false/g, "false()");

  if (ns)
  {
    e = e.replace(/form\./g, "/"+ns+":");
    e = e.replace(/\./g, "/"+ns+":");
  }
  else
  {
    e = e.replace(/form\./g, "/");
    e = e.replace(/\./g, "/");
  }

  return e;
}

/*
Month:
<select multiple name="spring">
      <option value="Mar">March</option>
      <option value="Apr">April</option>
      <option>May</option>
</select>

would be written:

<select ref="spring" appearance="minimal">
<label>Month:</label>
<item><label>March</label><value>Mar</value></item>
<item><label>April</label><value>Apr</value></item>
<item><label>May</label><value>May</value></item>
</select>

The button element

<input type="button" value="Show" onclick="show()">

can be written

<trigger><label>Show</label>
   <h:script ev:event="DOMActivate" type="text/javascript">show()</h:script>
</trigger>

or

<trigger ev:event="DOMActivate" ev:handler="#show">
    <label>Show</label>
</trigger>

Optgroup

Drink:
<select name="drink">
   <option selected value="none">None</option>
   <optgroup label="Soft drinks">
      <option value="h2o">Water</option>
      <option value="m">Milk</option>
      <option value="oj">Juice</option>
   </optgroup>
   <optgroup label="Wine and beer">
      <option value="rw">Red Wine</option>
      <option value="ww">White Wine</option>
      <option value="b">Beer</option>
   </optgroup>
</select>

is written

<select1 ref="drink">
   <label>Drink:</label>
   <item><label>None</label><value>none</value></item>
   <choices>
      <label>Soft drinks</label>
      <item><label>Water</label><value>h2o</value></item>
      <item><label>Milk</label><value>m</value></item>
      <item><label>Juice</label><value>oj</value></item>
   </choices>
   <choices>
      <label>Wine and beer</label>
      <item><label>Red wine</label><value>rw</value></item>
      <item><label>White wine</label><value>ww</value></item>
      <item><label>Beer</label><value>b</value></item>
   </choices>
</select1>
*/

//recursively duplicate source tree
// needs major surgery for radio, checkbox, select, ...
function cloneContent(dom, dst, src)
{
  var tag, node, label, hint, text, type, reset, form;
  var i, group, item, value;

  for (; src; src = src.nextSibling)
  {
    if (src.nodeType != 1)
    {
      dst.appendChild(src.cloneNode(true));
      continue;
    }

    tag = src.nodeName.toLowerCase();

    if (tag == "form")
    {
      cloneContent(dom, dst, src.firstChild);
      continue;
    }

    if (tag == "input")
    {
      if (src.datatype == "radio" || src.datatype == "checkbox")
      {
        form = src.form;
        group = form[src.name];
        if (group[0] != src)
          continue;

        if (src.datatype == "radio")
          node = dom.createElement("xf:select1");
        else
          node = dom.createElement("xf:select");

        node.setAttribute("ref", "my:"+src.getAttribute("name"));

        if (src.datatype == "checkbox")
          node.setAttribute("appearance", "full");

        dst.appendChild(node);

        if (group.mylabels && group.mylabels.length)
        {
          label = dom.createElement("xf:label");
          appendLineBreak(node, "  ");
          node.appendChild(label);
          cloneContent(dom, label, group.mylabels[0].firstChild);
          text = group.mylabels[0].getAttribute("title");

          if (text)
          {
            hint = dom.createElement("xf:hint");
            hint.appendChild(dom.createTextNode(text));
            appendLineBreak(node, "  ");
            node.appendChild(hint);
          }
        }

        for (i = 0; i < group.length; ++i)
        {
          // create item element for each radio button
          appendLineBreak(node, "  ");
          item = dom.createElement("xf:item");
          node.appendChild(item);

          value = group[i].mylabels;

          if (value && value.length)
          {
            appendLineBreak(item, "    ");
            label = dom.createElement("xf:label");
            item.appendChild(label);
            cloneContent(dom, label, value[0].firstChild);
          }

          appendLineBreak(item, "    ");
          value = dom.createElement("xf:value");
          item.appendChild(value);
          value.appendChild(dom.createTextNode(group[i].value));
          appendLineBreak(item, "  ");
        }
        appendLineBreak(node, "");
        continue;
      }

      if (src.datatype == "range")
        node = dom.createElement("xf:range");
      else
        node = dom.createElement("xf:input");

      dst.appendChild(node);

      node.setAttribute("ref", "my:"+src.getAttribute("name"));

      if (src.datatype == "range")
      {
        if (src.getAttribute("min"))
          node.setAttribute("start", src.getAttribute("min"));

        if (src.getAttribute("max"))
          node.setAttribute("end", src.getAttribute("max"));

        if (src.getAttribute("step"))
          node.setAttribute("step", src.getAttribute("step"));
      }

      if (src.mylabels && src.mylabels.length)
      {
        label = dom.createElement("xf:label");
        appendLineBreak(node, "  ");
        node.appendChild(label);
        cloneContent(dom, label, src.mylabels[0].firstChild);
        text = src.mylabels[0].getAttribute("title");

        if (text)
        {
          hint = dom.createElement("xf:hint");
          hint.appendChild(dom.createTextNode(text));
          appendLineBreak(node, "  ");
          node.appendChild(hint);
        }
      }

      appendLineBreak(node, "");
      continue;
    }

    if (tag == "textarea")
    {
      node = dom.createElement("xf:textarea");
      node.setAttribute("ref", "my:"+src.getAttribute("name"));
      dst.appendChild(node);

      if (src.mylabels && src.mylabels.length)
      {
        label = dom.createElement("xf:label");
        appendLineBreak(node, "  ");
        node.appendChild(label);
        cloneContent(dom, label, src.mylabels[0].firstChild);
        text = src.mylabels[0].getAttribute("title");

        if (text)
        {
          hint = dom.createElement("xf:hint");
          hint.appendChild(dom.createTextNode(text));
          appendLineBreak(node, "  ");
          node.appendChild(hint);
        }
      }

      appendLineBreak(node, "");
      continue;
    }

    if (tag == "button")
    {
      type = src.getAttribute("type");

      if (type == "reset")
      {
        node = dom.createElement("xf:trigger");
        dst.appendChild(node);
        label = dom.createElement("xf:label");
        appendLineBreak(node, "  ");
        node.appendChild(label);
        cloneContent(dom, label, src.firstChild);
        reset = dom.createElement("xf:reset");
        appendLineBreak(node, "  ");
        node.appendChild(reset);
        reset.setAttribute("ev:event", "DOMActivate");
        appendLineBreak(node, "");
        continue;
      }

      if (type != "button") // treat as submit
      {
        node = dom.createElement("xf:submit");
        dst.appendChild(node);
        label = dom.createElement("xf:label");
        appendLineBreak(node, "  ");
        node.appendChild(label);
        cloneContent(dom, label, src.firstChild);
        appendLineBreak(node, "");
        continue;
      }

      // this is a hack to elide button elements
      continue;
    }

    if (tag == "label")
      continue;

    if (tag == "fieldset")
    {
      node = dom.createElement("xf:group");
      dst.appendChild(node);
      cloneContent(dom, node, src.firstChild);
      continue;
    }

    if (tag == "legend")
    {
      node = dom.createElement("xf:label");
      dst.appendChild(node);
      cloneContent(dom, node, src.firstChild);
      continue;
    }

    node = src.cloneNode(false);
    dst.appendChild(node);
    cloneContent(dom, node, src.firstChild);
  }
}

// create XML document from string
function  createDocumentFromString(xmlString)
{
  var dom = null;
  xmlString = "<hello>world</hello>";

  if (document.implementation.createDocument)
  {
    var parser = new DOMParser();
    var dom = parser.parseFromString(html_template, "text/xml");
  }
  else if (window.ActiveXObject)
  {
    var dom = new ActiveXObject("Microsoft.XMLDOM")
    dom.async="false"
    dom.loadXML(xmlString)
  }

  return dom;
}

function createDocument(nsuri, qname, doctype)
{
  if (window.ActiveXObject)
  {
    var dom = new ActiveXObject("Microsoft.XMLDOM");
    dom.async = false;
    dom.validateOnParse = false;
    dom.resolveExternals = false;
    var doc = dom.create();
    
  }
  else
  {
    doc = document.implementation.createDocument(nsuri, qname, doctype);
  }

  return doc;
}

// this won't work on IE and needs further study
// in any case, it doesn't wrap attributes and hence
// causes problems when there are lots of namespaces
function serializeToString(xmldoc)
{
  return new XMLSerializer().serializeToString(xmldoc);
}

var html_template =
 '<html xmlns="http://www.w3.org/1999/xhtml"\n' +
 ' xmlns:xf="http://www.w3.org/2002/xforms"\n' +
 ' xmlns:xsd="http://www.w3.org/2001/XMLSchema"\n' +
 ' xmlns:ev="http://www.w3.org/2001/xml-events"\n' +
 ' xmlns:my="http://example.com/2006/ns">\n' +
 '<head>\n</head>\n<body></body>\n</html>';

function xshow(name)
{
//  alert("there are " + document.forms[0].fields.length + " fields");

  var dom = createDocumentFromString(html_template);
  var model = exportModel(document.forms[name]);
  var head = dom.getElementsByTagName("head");
  var title = document.getElementsByTagName("title");

  if (title.length && title[0])
    head[0].appendChild(title[0].cloneNode(true));

  appendLineBreak(head[0], "");
  head[0].appendChild(model);
  appendLineBreak(head[0]);

  var body = dom.getElementsByTagName("body");
  cloneContent(dom, body[0], document.body.firstChild);
  showSource(dom);
}

function showSource(dom)
{
  var win = window.open("","");
  var src = toSource(dom); //serializeToString(dom);
  src = src.replace(/<p \/>/g, "");
  src = src.replace(/\r/g, "");
  src = src.replace(/\n\n\n/g, "\n\n");
  src = src.replace(/\n\n\n/g, "\n\n");
  src = src.replace(/\n\n\n/g, "\n\n");
  src = src.replace(/\n\n\n/g, "\n\n");
  var pre = win.document.createElement("pre");
  pre.appendChild(win.document.createTextNode(src));
  win.document.body.appendChild(pre);
  win.document.title="XForms Model and Instance";
}


function getRoot(context)
{
  var node = context.firstChild;

  while (node)
  {
    if (node.nodeType == 1)
      break;

    node = node.nextSibling;
  }

  return node;
}

function toSource(node)
{
  // check if node is document
  if (node.nodeType == 9)
    node = getRoot(node);

  if (node.nodeType == 3)
  {
    var text = node.nodeValue;
    text = text.replace(/&/g, "&amp;");
    text = text.replace(/</g, "&lt;");
    text = text.replace(/>/g, "&gt;");
    return text;
  }

  if (node.nodeType == 1)
  {
    if (node.childNodes.length > 0)
    {
      var s = "<" + node.nodeName.toLowerCase() +
                              toAttribsSrc(node.attributes) + ">";

      for (var i = 0; i < node.childNodes.length; ++i)
        s += toSource(node.childNodes[i]);

      return s +  "</" + node.nodeName.toLowerCase() + ">";
    }

    toAttribsSrc(node);
    return "<" + node.nodeName.toLowerCase() +
                              toAttribsSrc(node.attributes) + " />";
  }

  return "";
}

function toAttribsSrc(attributes)
{
  var s = "";

  for (var i = 0; i < attributes.length; ++i)
  {
    var attr = attributes[i];
    s += (i > 0 ?"\n   ":" ") + attr.name + "=\"" + attr.nodeValue + "\"";
  }

  return s;
}

function collectNames(list)
{
  var s = "";

  for (var i = 0; i < list.length; ++i)
  {
    if (s != "")
      s += ", " + list[i];
    else
      s += list[i];
  }

  return s;
}
