/**
 * function tries to check if the color contains just hex-characters
 * if so, it returns the color-definition prefixed with a '#'
 * otherwise it assumes the color is a named one
 */
function renderColorAsString(c) {
  if (c && c.length == 6) {
    var nonhex = new RegExp("[^0-9,a-f]");
    nonhex.ignoreCase = true;
    var found = c.match(nonhex);
    if (!found) {
      // color-string contains just hex-characters, so we prefix it with '#'
      return("#" + c);
    }
  }
  return(c);
}


/**
 * renders a color as hex or named string.
 * you may provide parameters modifying the
 * appearance of the color:
 * 1.) param may contain one or more of
 * these strings:
 * shiftHue, shiftSaturation, shiftLuminance.
 * these strings must specify float values (see
 * function modifyColHsl for sensible value ranges).
 * 2.) param may also contain one or more of these
 * strings:
 * setHue, setSaturation, setLuminance.
 * these strings must also specify float values,
 * which will override the respective parameters
 * with the absolute values specified.
 * 3.) param may also contain one or both of
 * these parameters:
 * invertSaturation, invertLuminance.
 * the value of these parameters does not matter;
 * when it is given, the respective variable
 * is inverted.
 * you may use any combination of 1.), 2.) and 3.).
 * note that inverting hue is a matter of
 * setting shiftSaturation="180".
 */
function renderColor(c, param) {
  if (param) {
    c = modifyCol(c, param);
  }
  res.write(renderColorAsString(c));
}

/**
 * does the same as modifyColHsl, but takes the
 * parameters in the same form as renderColor.
 * it's used as a bridge between these two
 * functions.
 */
function modifyCol(c, param) {
  var modifiedCol = c;
  if (param.shiftHue || param.shiftSaturation ||
      param.shiftLuminance || param.setHue ||
      param.setSaturation || param.setLuminance ||
      param.invertLuminance || param.invertSaturation) {
    // initialize h, s and l deltas (differences):
    if (param.shiftHue) {
      dH = parseFloat(param.shiftHue);
    } else {
      dH = 0.0;
    }
    if (param.shiftSaturation) {
      dS = parseFloat(param.shiftSaturation);
    } else {
      dS = 0.0;
    }
    if (param.shiftLuminance) {
      dL = parseFloat(param.shiftLuminance);
    } else {
      dL = 0.0;
    }
    // modify colour using deltas
    modifiedCol = modifyColHsl(c, dH, dS, dL, param);
  }
  return modifiedCol;
}


/**
 * modifyColHsl takes a string denoting a colour in hex
 * and changes its hue, saturation and luminance
 * (brightness) according to the respective parameters,
 * which can indicate changes of +/- 0-360 degrees (hue)
 * or +/- 0.0-1.0 (saturation, luminance). overflows
 * are truncated. the modified colour is returned again
 * as a string denoting the colour in hex.
 * | example: modifyCol("400000", 0, 0, 0.25) returns
 * | "C00000". easy!
 * if one or more parameter strings (setHue, setSaturation,
 * setLuminance) are given, these settings override the
 * specified colour and set the respective variable to the
 * specified value. you can use this to shift to absolute
 * values instead relatively.
 * invertSaturation and invertLuminance work similar and
 * act on relative values as well as on absolute ones.
 */
function modifyColHsl(colour, dH, dS, dL, param) {
  // extract normalised RGB values
  var r = parseInt(colour.substring(0, 2), 16) / 255.0;
  var g = parseInt(colour.substring(2, 4), 16) / 255.0;
  var b = parseInt(colour.substring(4, 6), 16) / 255.0;
  // modify the colour in HSL space and truncate overflow
  var colourHsl = new rgbToHsl(r, g, b);
  var modifiedHue = (colourHsl.hue + dH) % 360;
  var modifiedSaturation = Math.max(
        Math.min((colourHsl.saturation + dS), 1.0), 0.0);
  var modifiedLuminance = Math.max(
        Math.min((colourHsl.luminance + dL), 1.0), 0.0);
  // override values as necessary
  if (param) {
    if (param.setHue) {
      modifiedHue = parseFloat(param.setHue) % 360;
    }
    if (param.setSaturation) {
      modifiedSaturation = Math.max(Math.min(
            parseFloat(param.setSaturation), 1.0), 0.0);
    }
    if (param.setLuminance) {
      modifiedLuminance = Math.max(Math.min(
            parseFloat(param.setLuminance), 1.0), 0.0);
    }
    if (param.invertSaturation) {
      modifiedSaturation = 1.0 - modifiedSaturation;
    }
    if (param.invertLuminance) {
      modifiedLuminance = 1.0 - modifiedLuminance;
    }
  }
  var modifiedRgb = new hslToRgb(modifiedHue,
        modifiedSaturation, modifiedLuminance);  
  // convert result to return value
  r = Math.round(modifiedRgb.red * 255.0);
  g = Math.round(modifiedRgb.green * 255.0);
  b = Math.round(modifiedRgb.blue * 255.0);
  return wordToHexStr((r << 16) + (g << 8) + b);
}


/**
 * function converting float RGB values in the range 0-1
 * to float HSL values in the ranges 0-360, 0-1, 0-1
 */
function rgbToHsl(r, g, b) {
  var h, s, l;
  var minVal = Math.min(Math.min(r, g), b);
  var maxVal = Math.max(Math.max(r, g), b);
  var range = maxVal - minVal;
  if (minVal == maxVal) { // r == g == b; grey tone
    s = 0;
    h = 0;
    l = r;
  } else {
    l = (maxVal + minVal) / 2;
    if (l < 0.5) {
      s = (range) / (maxVal + minVal);
    } else {
      s = (range) / (2.0 - maxVal - minVal);
    }
    if (r == maxVal) {
      h = (g - b) / range;
    } else if (g == maxVal) {
      h = 2.0 + (b - r) / range;
    } else { // b == maxVal
      h = 4.0 + (r - g) / range;
    }
  }
  // h is not normalised to the standard range yet  
  this.hue = h * 60;
  this.saturation = s;
  this.luminance = l;
}

/**
 * function converting float HSL values in the ranges 0-360,
 * 0-1, 0-1 to float RGB values in the range 0-1
 */
function hslToRgb(h, s, l) {
  var r, g, b;
  if (s == 0) {
    r = g = b = l;
  } else {
    // in order to reverse the algorithm of RgbToHsl, we need
    // five helper variables with silly names...
    var h1, h2, h3r, h3g, h3b;
    if (l < 0.5) {
      h1 = l * (1.0 + s);
    } else {
      h1 = l + s - (l * s);
    }
    h2 = (2.0 * l) - h1;
    // normalise hue to the range 0-1
    h = (h % 360) / 360;
    h3r = h + (1.0 / 3.0);
    h3g = h;
    h3b = h - (1.0 / 3.0);
    // derive r
    // instead of the following to IFs, it should be
    // possible to write "h3r %= 1;", but that didn't
    // work (???). feel free to correct.
    if (h3r < 0) {
      h3r += 1;
    }
    if (h3r > 1) {
      h3r -= 1;
    }
    if (h3r < (1.0 / 6.0)) {
      r = h2 + ((h1 - h2) * 6.0 * h3r);
    } else if (h3r < 0.5) {
      r = h1;
    } else if (h3r < (2.0 / 3.0)) {
      r = h2 + ((h1 - h2) * ((2.0 / 3.0) - h3r) * 6.0);
    } else {
      r = h2;
    }
    // derive g. this is a copy&paste of the code above
    // with r replaced with g. this is as elegant as
    // the queen's wardrobe. feel free to correct.
    if (h3g < 0) {
      h3g += 1;
    }
    if (h3g > 1) {
      h3g -= 1;
    }
    if (h3g < (1.0 / 6.0)) {
      g = h2 + ((h1 - h2) * 6.0 * h3g);
    } else if (h3g < 0.5) {
      g = h1;
    } else if (h3g < (2.0 / 3.0)) {
      g = h2 + ((h1 - h2) * ((2.0 / 3.0) - h3g) * 6.0);
    } else {
      g = h2;
    }
    // derive b. same as above.
    if (h3b < 0) {
      h3b += 1;
    }
    if (h3b > 1) {
      h3b -= 1;
    }
    if (h3b < (1.0 / 6.0)) {
      b = h2 + ((h1 - h2) * 6.0 * h3b);
    } else if (h3b < 0.5) {
      b = h1;
    } else if (h3b < (2.0 / 3.0)) {
      b = h2 + ((h1 - h2) * ((2.0 / 3.0) - h3b) * 6.0);
    } else {
      b = h2;
    }
  }
  this.red = r;
  this.green = g;
  this.blue = b;
}

/**
 * this function converts a 32-bit number to a string.
 * i copied it from somewhere, it's used in the function
 * shiftCol. if ECMA-Script has something like that built in
 * already, feel free to correct.
 */
function wordToHexStr(word) {
  var h="0123456789ABCDEF";
  var retVal = "";
  var hexStr = "";
  var remainder, i;
  while (word != 0) {
    remainder = word % 16;
    hexStr += h.charAt(remainder);
    word >>= 4;
  }
  // hexStr is backwards, turn it around
  for(i = 5; i >= 0; i--) {
    retVal += hexStr.charAt(i);
  }
  while (retVal.length < 6) { // pad missing 0s
    retVal = "0" + retVal;
  }
  return retVal;
}






/**
 * open an html link element
 * @param String URL to use in a-tag
 */
function openLink(url) {
   var attr = new Object();
   attr.href = url;
   openMarkupElement("a", attr);
}

/**
 * close an html link element
 */
function closeLink() {
  closeMarkupElement("a");
}

/**
 * Opens an arbitrary x/html element ("begin tag")
 * @param name String containing the element's name
 * @param attr Object containing the element's attributes as properties
 */
function openMarkupElement(name, attr) {
  renderMarkupPart(name, attr);
  res.write(">");
}


/**
 * Closes an arbitray x/html element ("end tag")
 * @param name String containing the element's name
 */
function closeMarkupElement(name) {
  res.write("</" + name + ">");
}


/**
 * Outputs an arbitrary empty x/html element ("contentless tag")
 * @param name String containing the element's name
 * @param attr Object containing the element's attributes as properties
 */
function renderMarkupElement(name, attr) {
  renderMarkupPart(name, attr);
  res.write(" />");
}


/**
 * Outputs the first part of an arbitrary x/html element
 * except for the closing ">" or "/>" which is done by
 * openMarkupElement() or renderMarkupElement(), resp.
 * @param name String containing the element's name
 * @param attr Object containing the element's attributes as properties
 */
function renderMarkupPart(name, attr) {
  res.write("<" + name);
  if (attr) {
    // temporary mapping of class attribute
    // if attr.style contains class definition
    // (due to backwards-compatibility)
    if (attr.style && attr.style.indexOf(":") < 0) {
      attr["class"] = attr.style;
      delete attr.style;
    }
    delete attr.as;
    var attributes = "";
    // creating the attribute string
    for (var i in attr) {
      if (!attr[i])
        continue;
      res.write(" " + i + "=\"" + attr[i] + "\"");
  	}
  }
}


/**
 * renders image element
 * @param img Object contains the images's properties
 * @param param Object contains user-defined properties
 */
function renderImage(img, param) {
   if (!param.title)
      param.title = img.alttext;
   param.src = getProperty("imgUrl");
   param.src += img.site ? img.site.alias + "/" : "";
   param.src += img.filename + "." + img.fileext;
   if (!param.width)
      param.width = img.width;
   if (!param.height)
      param.height = img.height;
   if (!param.border)
      param.border = "0";
   param.alt = param.description ? param.description : img.alttext;
   renderMarkupElement("img", param);
}


/**
 * renders a textarea
 * @param param Object contains the element's attributes
 */
function renderInputTextarea(param) {
   if (param.width)
      param.cols = param.width;
   if (param.height)
      param.rows = param.height;
   if (!param.cols)
      param.cols = 40;
   if (!param.rows)
      param.rows = 5;
   if (!param.wrap)
      param.wrap = "virtual";
   var value = param.value ? encodeForm(param.value) : "";
   delete param.value;
   delete param.width;
   delete param.height;
   delete param.as;
   openMarkupElement("textarea", param);
   res.write(value);
   closeMarkupElement("textarea");
}


/**
 * renders a submit-button
 * @param param Object contains the element's attributes
 */
function renderInputButton(param) {
   if (!param)
      return;
   param.type = "submit";
   if (param.content) {
      param.value = param.content;
      delete param.content;
   }
   if (!param.name)
      param.name = param.type;
   param.value = param.value ? encodeForm(param.value) : param.type;
   renderMarkupElement("input", param);  
}


/**
 * renders an input type text
 * @param param Object contains the element's attributes
 */
function renderInputText(param) {
   if (!param)
      return;
   param.type = "text";
   // this one is left for backwards-compatibility
   if (param.width)
      param.size = param.width;
   if (!param.size)
      param.size = 20;
   delete param.width;
   renderMarkupElement("input", param);
}


/**
 * renders an input type password
 * @param param Object contains the element's attributes
 */
function renderInputPassword(param) {
  if (!param)
    return;
  param.type = "password";
  param.size = param.width ? param.width : "20";
  delete param.width;
  renderMarkupElement("input", param);
}


/**
 * function renders an input type file
 * @param param Object contains the element's attributes
 */
function renderInputFile(param) {
  if (!param)
    return;
  param.type = "file";
  renderMarkupElement("input", param);
} 


/**
 * renders an input type checkbox
 * @param param Object contains the element's attributes
 */
function renderInputCheckbox(param) {
  if (!param || !param.name)
    return;
  param.type = "checkbox";
  param.checked = param.check;
  delete param.check;
  if (parseInt(param.value, 10) == 1 || param.value == true)
    param.checked = "checked";
  param.value = "1";
  renderMarkupElement("input", param);
}


/**
 *  Renders a drop down box from an Array and an optional 
 *  current selection index. This is a simpler alternative 
 *  for the drop-down framework in hopobject. Its main 
 *  advantage is that Arrays are much simpler to set up in 
 *  JavaScript than (Hop)Objects:
 */
function renderDropDownBox(name, options, selectedIndex, firstoption) {
  var param = new Object();
  param.name = name;
  param.size = "1";
  openMarkupElement("select", param);
  if (firstoption) {
    param = new Object();
    param.value = "";
    openMarkupElement("option", param);
    res.write(firstoption);
    closeMarkupElement("option");
  }
  for (var i in options) {
    param = new Object();
    param.name = encode(options[i]);
    param.value = i; 
    if (param.value == selectedIndex)
      param.selected = "true";
    openMarkupElement("option", param);
    res.write(param.name);
    closeMarkupElement("option");
  }
  closeMarkupElement("select");
}


/**
 * function retuns only a part of the text passed as argument
 * length of the string to show is defined by argument "limit"
 */
function renderTextPreviewAsString(text, limit) {
   var limit = Math.min(limit, text.length);
   var text = stripTags(text);
   var idx = 0;
   while (idx < limit) {
      var nIdx = text.indexOf(" ", idx);
      if (nIdx < 0)
         break;
      idx = ++nIdx;
   }
   var prev = text.substring(0,(idx > 1 ? idx : limit));
   // and now we "enrich" the text with <wbr>-tags
   var str = "";
   for (var i=0; i<prev.length; i=i+30)
      str += prev.substring(i, i+30) + "<wbr>";
   return(str);
}


/**
 * function renders only a part of the text passed as argument
 * length of the string to show is defined by argument "limit"
 */
function renderTextPreview(text, limit) {
  res.write(renderTextPreviewAsString(text, limit));
}


/**
 * Do Wiki style substitution, transforming
 * stuff contained between asterisks into links.
 */
function doWikiStuff (src) {
  // robert, disabled: didn't get the reason for this:
  // var src= " "+src;
  if (src.indexOf ("<*") < 0)
     return src;

  // do the Wiki link thing, <*asterisk style*>
  var regex = new RegExp ("<[*]([^*]+)[*]>");
  regex.ignoreCase=true;

  var text = "";
  var start = 0;
  while (true) {
    var found = regex.exec (src.substring(start));
    var to = found == null ? src.length : start + found.index;
    text += src.substring(start, to);
    if (found == null)
      break;
    var name = ""+(new java.lang.String (found[1])).trim();
    var item = path.site.topics.get (name);
    if (item == null && name.lastIndexOf("s") == name.length-1)
      item = path.site.topics.get (name.substring(0, name.length-1));
    if (item == null || !item.size())
      text += format(name)+" <small>[<a href=\""+path.site.stories.href("create")+"?topic="+escape(name)+"\">define "+format(name)+"</a>]</small>";
    else
      text += "<a href=\""+item.href()+"\">"+name+"</a>";
    start += found.index + found[1].length+4;
  }
  return text;
}


/**
 * DEPRECATED!
 * use openMarkupElement(), closeMarkupElement() and 
 * renderMarkupElement() instead
 *
 * Returns an arbitrary x/html element as string
 * @param name String containing the element's name (tag)
 * @param content String containing the element's content
 * @param attr Object containing the element's attributes as properties
 */
function renderMarkupElementAsString(name, content, attr) {
  if (!content)
    content = "";
  // temporary mapping of class attribute
  // (due to backwards-compatibility)
  if (!attr["class"]) {
    attr["class"] = attr.style;
    delete attr.style;
  }
  var attributes = "";
  // creating the attribute string
  for (var i in attr) {
    if (!attr[i])
      continue;
    attributes += " " + i + "=\"" + attr[i] + "\"";
	}
  return("<" + name + attributes + ">" + content + "</" + name + ">");
}


/**
 * function renders a dropdown-box containing all available
 * locales
 * @param Obj Locale-Object to preselect
 */

function renderLocaleChooser(loc) {
   var locs = java.util.Locale.getAvailableLocales();
   var options = new Array();
   // get the defined locale of this site for comparison
   for (var i in locs) {
      options[i] = locs[i].getDisplayName();
      if (loc && locs[i].equals(loc))
         var selectedIndex = i;
   }
   renderDropDownBox("locale",options,selectedIndex);
}