// ==UserScript==
// @name           Color Coordinator
// @namespace      tag:wojcikm4@msu.edu,2007-10:scripts/public
// @description    Various functions for operating on color on a page
// ==/UserScript==


function buttonAction(action)
{
   /***
   Helper functions.
   ***/

   /* An iterator for the nodes that calls a callback on each node */
   function processNodes(visitor,nodes)
   {
      /* Iterate over nodes */
      for (var t = 0; t < nodes.snapshotLength; t++)
      {
         /* Get the item */
         var node = nodes.snapshotItem(t);
         visitor(node);
      }
   }

   /* Get all nodes after our div, matching the specified pattern */
   function getNodes(path)
   {

      /* Get XPath list of text nodes outside our special div */
      var nodes = document.evaluate
      (
         "//div[@id='visrhetctl']/following::" + path
       , document
       , null
       , XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE
       , null
      );

      GM_log("found " + nodes.snapshotLength + " nodes");
      return nodes;
   }

   /* Create or find an output area */
   function outputArea()
   {
      var oa = document.getElementById("visrhetout");
      if (! oa)
      {
         var vrctr = document.getElementById("visrhetctr");
         if (! vrctr) return null;
         oa = document.createElement("div");
         oa.id = "visrhetout";
         vrctr.appendChild(oa);
      }
      else
      {
         /* Clear it out */
         oa.innerHTML = "";
         oa.style.display = "";
      }
      return oa;
   }

   /* Compute HLS from RGB */
   function RgbToHls(r, g, b)
   {
      var h, l, s;

      /* Do all computations on 0..1, and then scale */
      r /= 0xff;
      g /= 0xff;
      b /= 0xff;

      var max = Math.max(r, g, b);
      var min = Math.min(r, g, b);

      /* lightness */
      l = (max + min) / 2;

      if (max == min)
      {
         /* achromatic: there's no saturation and hue is undefined */
         h = s = 0;
      }
      else
      {
         /* saturation */
         if (l < 0.5)
            s = (max - min) / (max + min);
         else
            s = (max - min) / (2 - max - min);

         /* hue */
         if (r == max)
            h = (g - b) / (max - min) / 6;
         else if (g == max)
            h = (2.0 + (b - r) / (max - min)) / 6;
         else
            h = (4.0 + (r - g) / (max - min)) / 6;

         if (h < 0) h += 1;
      }

      /* Scale up to 0..255 */
      h = Math.round(h * 0xff);
      l = Math.round(l * 0xff);
      s = Math.round(s * 0xff);

      return [h, l, s];
   }

   /* Compute RGB from HLS */
   function HlsToRgb(h, l, s)
   {
      var r, g, b;

      /* Calculate an RGB component */
      function RgbComp(x, y, h)
      {
         var c;

         /* normalize hue */
         if (h < 0) h += 1;
         if (h > 1) h -= 1;

         /* component value based on location in color space */
         if (h < 1/6) return x + (y - x) * h * 6;
         if (h < 3/6) return y;
         if (h < 4/6) return x + (y - x) * (2/3 - h) * 6;
         return x;
      };

      /* Do all computations on 0..1, and then scale */
      h /= 0xff;
      l /= 0xff;
      s /= 0xff;

      var x, y;

      if (l > 0.5)
         y = l + s - l * s;
      else
         y = l * (s + 1);

      x = l * 2 - y;

      r = RgbComp(x, y, h + 1/3);
      g = RgbComp(x, y, h);
      b = RgbComp(x, y, h - 1/3);

      /* Scale */
      r = Math.round(r * 0xff);
      g = Math.round(g * 0xff);
      b = Math.round(b * 0xff);

      return [r, g, b];
   }

   /* Regular expression used to parse an RGB spec */
   var rgbSpec = /rgb\((\d+), *(\d+), *(\d+)\)/;



   /***
   Define our various subfunctions.
   ***/

   /* Change all colors to have 100% red */
   function doRedden(nodes)
   {
      var colorList = "";

      processNodes
      (
         function(node)
         {
            var style = getComputedStyle(node, "");
            if (style && style.color)
            {
               // GM_log(node.tagName + " color: " + style.color);
               if (style.color.match(/^rgb/))
                  node.style.color = style.color.replace(/\d+/, "255");
               /*
               else
               {
                  if (colorList.indexOf(" " + style.color + " ") == -1)
                     colorList += " " + style.color + " ";
               }
               */
            }
            if (style && style.backgroundColor)
            {
               // GM_log(node.tagName + " backgroundColor: " +
               //        style.backgroundColor);
               if (style.backgroundColor.match(/^rgb/))
               {
                  node.style.backgroundColor =
                     style.backgroundColor.replace(/\d+/, "255");
               }
               /*
               else
               {
                  if (colorList.indexOf(" " + style.backgroundColor + " ")
                      == -1)
                     colorList += " " + style.backgroundColor + " ";
               }
               */
            }
         }
       , nodes? nodes : getNodes("*")
      );

      // GM_log("Unrecognized colors: " + colorList);
   };

   /* Swap red and blue */
   function doRedIsBlue(nodes)
   {
      processNodes
      (
         function(node)
         {
            function swapRedBlue(color)
            {
               // GM_log("checking " + color);
               var rgb = rgbSpec.exec(color);
               if (rgb)
               {
                  // GM_log("found " + rgb);
                  return "rgb(" + rgb[3] + "," + rgb[2] + "," + rgb[1] + ")";
               }
               else
                  return null;
            }

            var style = getComputedStyle(node, "");
            var newColor;
            if (style && style.color)
            {
               newColor = swapRedBlue(style.color);
               if (newColor)
                  node.style.color = newColor;
            }
            if (style && style.backgroundColor)
            {
               newColor = swapRedBlue(style.backgroundColor);
               if (newColor)
                  node.style.backgroundColor = newColor;
            }
         }
       , nodes? nodes : getNodes("*")
      );

      // GM_log("Unrecognized colors: " + colorList);
   };


   /* Build a list of the colors in the document in the output area */
   function doColorList(nodes)
   {
      var outDiv = outputArea();
      if (! outDiv)
      {
         alert("Fail: output area unavailable");
         return;
      }
      outDiv.style.backgroundColor = "white";

      var table;
      table = document.createElement("table");
      table.style.fontSize = "large";
      outDiv.appendChild(table);

      var colorList = [];

      function addColorIfNew(color)
      {
         var c;
         if (color == "transparent") return;
         for (c = 0; c < colorList.length; c++)
            if (colorList[c] == color) break;
         if (c == colorList.length)
            colorList.push(color);
      };

      processNodes
      (
         function(node)
         {
            var style = getComputedStyle(node, "");
            if (style && style.color)
               addColorIfNew(style.color);
            if (style && style.backgroundColor)
               addColorIfNew(style.backgroundColor);
         }
       , nodes? nodes : getNodes("*")
      );

      var c;
      while (c = colorList.shift())
      {
         var cEntry = document.createElement("tr"),
             label  = document.createElement("td"),
             swatch = document.createElement("td"),
             patch  = document.createElement("div");
         label.innerHTML = c;
         label.style.paddingTop = 
          label.style.paddingBottom = 
           label.style.paddingLeft = 
            label.style.paddingRight = 
             label.style.marginTop =
              label.style.marginBottom =
               label.style.marginLeft =
                label.style.marginRight = "2px";
         swatch.style.backgroundColor = "#c0c0c0";
         patch.style.color = patch.style.backgroundColor = c;
         patch.style.marginLeft = patch.style.marginRight = "3px";
         patch.innerHTML = "XXXX";
         swatch.appendChild(patch);
         cEntry.appendChild(label);
         cEntry.appendChild(swatch);
         table.appendChild(cEntry);
      }

      var closer = document.createElement("p");
      closer.innerHTML =
         "<a href='#' "
       + "onclick=\""
       + "   var oa = document.getElementById('visrhetout');"
       + "   oa.style.display='none';"
       + "\">"
       + "Close this!"
       + "</a>";
      outDiv.appendChild(closer);
   };

   /* Desaturate colors: scale saturation from 0..255 to 0..64 */
   function doPastel(nodes)
   {
      processNodes
      (
         function(node)
         {
            function desaturate(color)
            {
               var rgb = rgbSpec.exec(color);
               if (rgb)
               {
                  /* Note that this rgb is ["rgb(...)", r, g, b] */
                  var hls = RgbToHls(rgb[1], rgb[2], rgb[3]);
                  var newSat = hls[2] / 4;

                  /* This rgb will just be [r, g, b] */
                  rgb = HlsToRgb(hls[0], hls[1], newSat);
                  return "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
               }
               else
                  return null;
            }

            var style = getComputedStyle(node, "");
            var newColor;
            if (style && style.color)
            {
               newColor = desaturate(style.color);
               if (newColor)
                  node.style.color = newColor;
            }
            if (style && style.backgroundColor)
            {
               newColor = desaturate(style.backgroundColor);
               if (newColor)
                  node.style.backgroundColor = newColor;
            }
         }
       , nodes? nodes : getNodes("*")
      );
   };

   /* Darken colors: scale lightness from 0..255 to 0..128 */
   function doDarken(nodes)
   {
      function darken(color)
      {
         var rgb = rgbSpec.exec(color);
         if (rgb)
         {
            /* Note that this rgb is ["rgb(...)", r, g, b] */
            var hls = RgbToHls(rgb[1], rgb[2], rgb[3]);
            var newLgt = hls[1] / 4 * 3;

            /* This rgb will just be [r, g, b] */
            rgb = HlsToRgb(hls[0], newLgt, hls[2]);
            return "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
         }
         else
            return null;
      }

      function darkenNode(node)
      {
         var style = getComputedStyle(node, "");
         if (! style) return;
         var newColor;
         if (style.color)
         {
            newColor = darken(style.color);
            if (newColor)
               node.style.color = newColor;
         }

         /***
         TODO: Make this a separate feature
         ***/
         if (style.backgroundImage)
         {
            GM_log("doDarken: removing background image");
            node.style.backgroundImage = "none";
         }

         if (style.backgroundColor)
         {
            newColor = darken(style.backgroundColor);
            if (newColor)
               node.style.backgroundColor = newColor;
         }
         else  /* should this be optional? */
         {
            GM_log("doDarken: forcing background to grey");
            style.backgroundColor = "rgb(192, 192, 192)";
         }
      }

      processNodes(darkenNode, nodes? nodes : getNodes("*"));

      /* Also darken the body background */
      GM_log("doDarken: darkening body");
      var nodes = document.getElementsByTagName("body");
      if (! nodes || nodes.length < 1)
      {
         GM_log("doDarken: No body found");
      }
      else
      {
         for (var t = 0; t < nodes.length; t++)
         {
            var node = nodes[t];
            var style = getComputedStyle(node, "");
            if (style)
            {
               var newColor;
               if (style.backgroundColor)
               {
                  newColor = darken(style.backgroundColor);
               }
               else
               {
                  newColor = "rgb(192, 192, 192)";
               }
               GM_log("doDarken: setting body background to " + newColor);
               style.backgroundColor = newColor;
            }
            else
            {
               GM_log("doDarken: body has no style");
            }
         }
      }
   };


   /***
   Process the button click by executing the appropriate subfunction.
   ***/

   switch (action)
   {
      case "ColorList": doColorList(null); break;
      case "RedIsBlue": doRedIsBlue(null); break;
      case "Redden": doRedden(null); break;
      case "Pastel": doPastel(null); break;
      case "Darken": doDarken(null); break;
   }

   return false;
}


function RetrieveText(name)
{
   if (typeof window.colorcoord == "undefined")
   {
      GM_log("No colorcoord attribute ");
      return "";
   }
   else if (typeof window.colorcoord[name] == "undefined")
   {
      GM_log("No colorcoord attribute named " + name);
      return "";
   }

   return window.colorcoord[name];
}

/* Create the div and the controls */
function AddControls()
{
   var visrhetDiv = document.getElementById("visrhetctl");
   if (! visrhetDiv)
   {
      visrhetDiv = document.createElement("div");
      visrhetDiv.id = "visrhetctl";
      visrhetDiv.width = "100%";
      visrhetDiv.style.background = "white";
      visrhetDiv.style.color = "black";
      visrhetDiv.style.marginBottom = "1px";
      visrhetDiv.style.paddingBottom = "2px";

      /***
      Setting overflow establishes a new block-formatting context, so that
      the page proper will start below the floated contents of this div.
      ***/
      visrhetDiv.style.overflow = "auto";

      /* Create the containing div with the show/hide control */
      vrctrDiv = document.createElement("div");
      vrctrDiv.id = "visrhetctr";
      vrctrDiv.style.background = "white";
      vrctrDiv.style.color = "black";
      vrctrDiv.innerHTML =
         "<a href=\"#\" "
       + "onClick=\""
       + "  var visrhetDiv = document.getElementById('visrhetctl');"
       + "  if (visrhetDiv.style.display == 'none')"
       + "     visrhetDiv.style.display = '';"
       + "  else"
       + "     visrhetDiv.style.display = 'none';"
       + "\">"
       + "Visual Rhetoric Controls"
       + "</a>";

      /* Initially the controls are hidden */
      visrhetDiv.style.display = "none";

      /***
      Add the inner container to the outer container, and the outer container
      to the page.
      ***/
      vrctrDiv.appendChild(visrhetDiv);
      document.body.insertBefore(vrctrDiv, document.body.firstChild);
   }

   var colorcoordDiv = document.createElement("div");
   colorcoordDiv.id = "colorcoord";
   colorcoordDiv.style.width = "45%";
   colorcoordDiv.style.cssFloat = "left";
   colorcoordDiv.style.clear = "none";
   colorcoordDiv.style.paddingTop =
   colorcoordDiv.style.paddingLeft =
   colorcoordDiv.style.paddingRight =
   colorcoordDiv.style.paddingBottom = "1px";

   colorcoordDiv.innerHTML =
      '<p style="font-size: large; font-weight: bold; font-family: sansserif;">'
    + '   Color Coordinator'
    + '</p>'
    + '<p style="font-size: small; font-family: serif;">'
    + '   <a href="mailto:wojcikm4@msu.edu">Michael Wojcik</a>,'
    + '   Rhetoric &amp; Writing, Michigan State University'
    + '</p>'
    + '<button id="ColorList">Palette</button>'
    + '<button id="RedIsBlue">RedIsBlue</button>'
    + '<button id="Redden">Redden</button>'
    + '<button id="Pastel">Pastel</button>'
    + '<button id="Darken">Darken</button>'
    ;

   visrhetDiv.appendChild(colorcoordDiv);


   /* Set the buttons' onclick attributes to invoke the handler */
   function SetButtonAction(name)
   {
      document.getElementById(name).addEventListener
      (
         "click"
       , function(){buttonAction(name)}
       , true
      );
   }

   SetButtonAction("ColorList");
   SetButtonAction("RedIsBlue");
   SetButtonAction("Redden");
   SetButtonAction("Pastel");
   SetButtonAction("Darken");


   /* Capture the contents of text fields */
   function SaveText(name, value)
   {
      GM_log("Setting window.colorcoord's " + name + " attribute to " + value);
      if (typeof window.colorcoord == "undefined")
         window.colorcoord = new Object();
      window.colorcoord[name] = value;
   }

   function CaptureTextField(name)
   {
      var elem = document.getElementById(name);
      if (! elem) return;
      elem.addEventListener
      (
         "change"
       , function(){SaveText(name, elem.value)}
       , true
      );
   }
}

window.addEventListener("load", AddControls, true);

