// Flomaton - JavaScript library for zooming HTML.



var SVGNS = "http://www.w3.org/2000/svg";
var HTMLNS = "http://www.w3.org/1999/xhtml";
var SCALE_BASE = 1.1; // Scale = SCALE_BASE ^ myScale

var debugOutput = null;

/*
 * Orginal: http://adomas.org/javascript-mouse-wheel/
 * prototype extension by "Frank Monnerjahn" themonnie @gmail.com
 */
 
Object.extend(Event, {
  wheel:function (event) {
    var delta = 0;
    if (!event) 
      event = window.event;
    if (event.wheelDelta) {
      delta = event.wheelDelta/120; 
      if (window.opera) 
        delta = -delta;
    } else if (event.detail) { 
      delta = -event.detail/3;     
    }
    return Math.round(delta); //Safari Round
  }
  
});

var Superabound = {

  viewers: []

};

function handleResize(event) {
  Superabound.viewers.each( function (item) {
    item.resize(event);
  });
}

// 
Event.observe(window, "resize", handleResize);

var Mapset = Class.create({

  initialize: function(base, firstTile) {
  
    this.base = base;
    this.firstTile = firstTile;
  
    this.viewers = []
    
    // selection?
    
    // ideally i would just instance the tiles into the viewer...
  
  },

});

var MapLocation = Class.create({

  initialize: function() {
    this.scale = 1.0;
    this.centerX = 0;
    this.centerY = 0;
  },
  
  applyTransform: function(svgElement) {
  
    // s = Math.pow(this.scale, SCALE_BASE);
    s = this.scale;
    
    /*
    svgElement.setAttribute("transform",
      "translate(" + this.centerX + "," + this.centerY + ") " +
      "scale(" + s + "," + s + ")"
      );
    */
  }
  
});


var MapViewer = Class.create({

  initialize: function(mapset, replaces) {
  
    this.mapset = mapset;
    this.replaces = $(replaces);
    
    this.debugWindow = document.createElementNS(HTMLNS, "p");
    this.debugWindow.innerHTML = mapset.base;
    
    // Enable or disable the debug window by uncommenting or commenting out this line.
    // this.replaces.appendChild(this.debugWindow);
    
    // How many rects have been clipped in the last frame.
    this.clipCounter = document.createElementNS(HTMLNS, "p");
    this.clipCounter.innerHTML = "clip counter: ";
    // this.replaces.appendChild(this.clipCounter);
    
    this.lastClipCount = 0;
    
    // Copy it to the global.
    debugOutput = this.debugWindow;
    
    // TODO: whenever the document gets resized, center the viewport. (?)
    // And resize the replaces element's height (again, ??).
    this.svg = createSVG();
    this.replaces.appendChild(svg);

    // Create the x and y axis lines.
    appendLine(this.svg, 0, 0, 10, 0, "blue");
    appendLine(this.svg, 0, 0, 0, 10, "green");
    
    this.group = document.createElementNS(SVGNS, "g");
    this.group.setAttribute("class", "mapviewer");
    this.svg.appendChild(this.group);

    var rootTileData = {
      x: 0,
      y: 0,
      top: 0,
      left: 0,
      bottom: 101,
      right: 101,
      url: mapset.firstTile
    };
    
    this.rootTile = new MapTile(this, this, rootTileData);
    this.group.appendChild(this.rootTile.getContentElement());
    
    // this.replaces.innerHTML = "hello, this is a map viewer";

    // LeftDown is true while the left mouse button is down.  
    this.leftDown = false;
    
    this.lastEvent = null;

    this.eventResize = this.resize.bindAsEventListener(this);
    this.eventKeyPress = this.keyPress.bindAsEventListener(this);
    this.eventMouseDown = this.mouseDown.bindAsEventListener(this);
    this.eventMouseUp = this.mouseUp.bindAsEventListener(this);
    this.eventMouseMove = this.mouseMove.bindAsEventListener(this);
    this.eventMouseOut = this.mouseOut.bindAsEventListener(this);
    this.eventMouseOver = this.mouseOver.bindAsEventListener(this);
    this.eventMouseWheel = this.mouseWheel.bindAsEventListener(this);
  
    Event.observe(document, "resize", this.eventResize);
    Event.observe(document, "keypress", this.eventKeyPress);
    Event.observe(document, "keydown", this.eventKeyPress);
    Event.observe(this.replaces, "mousedown", this.eventMouseDown);
    Event.observe(this.replaces, "mouseup", this.eventMouseUp);
    Event.observe(this.replaces, "mousemove", this.eventMouseMove);
    // Event.observe(this.replaces, "mouseout", this.eventMouseOut);
    // Event.observe(this.replaces, "mouseover", this.eventMouseOver);
    Event.observe(document, "mousewheel", this.eventMouseWheel, false);
    Event.observe(document, "DOMMouseScroll", this.eventMouseWheel, false); // Firefox
    

    // The viewport rect is a rectangle that is the same size as the viewport.
    // It is used to do intersection tests with the tiles.
    this.vizViewportRect = document.createElementNS(SVGNS, "rect");
    this.vizViewportRect.setAttribute("x", "-100");
    this.vizViewportRect.setAttribute("y", "-100");
    this.vizViewportRect.setAttribute("width", "100");
    this.vizViewportRect.setAttribute("height", "100");
    this.vizViewportRect.setAttribute("class", "viewport");
    this.svg.appendChild(this.vizViewportRect);
    this.vizViewportRect.setAttribute("style", "fill: none; stroke: orange; stroke-width: 1.0");
    this.vizViewportRect.setAttribute("display", "none");
  
    this.location = new MapLocation();
    this.location.scale = 1.0;
    this.zoomLevel = 0.0;
    
    // Maybe load a cookie to the last (x, y, scale)?
    // Based on the element's id?
    
    // Send a resize event and start loading the root tile.
    this.setViewBox();
    this.rootTile.markVisible();
    
    // Add this to the array.
    this.mapset.viewers.push(this);
    Superabound.viewers.push(this);
    
  
  },
  
  destroy: function() {
    this.mapset.viewers = this.mapset.viewers.without(this);
    Superabound.viewers = Superabound.viewers.without(this);
    this.mapset = null;
  
    Event.stopObserving(this.replaces, "mousedown", this.eventMouseDown);
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
  },
  
  zoomToFit: function() {
    var dim = Element.getDimensions(this.replaces);
    var desiredScale = dim.height / this.rootTile.height;
    this.zoomLevel = Math.log(desiredScale) / Math.log(SCALE_BASE);
    this.location.scale = this.calculateScale();
    this.location.centerX = this.rootTile.width / 2;
    this.location.centerY = this.rootTile.height / 2;
    this.setViewBox();
  },
  
  setInitialZoom: function() {
    if (this.zoomLevel == 0)
        this.zoomToFit();
  },
  
  setViewBox: function() {
  
    var dim = Element.getDimensions(this.replaces);
    
    this.viewWidth = dim.width / this.location.scale;
    this.viewHeight = dim.height / this.location.scale;
    this.viewTop = this.location.centerY - this.viewHeight / 2;
    this.viewLeft = this.location.centerX - this.viewWidth / 2;
    
    var viewportMargin = 100 / this.location.scale;
    
    this.viewportRect = new MapRect();
    this.viewportRect.x = this.viewLeft + viewportMargin;
    this.viewportRect.y = this.viewTop + viewportMargin;
    this.viewportRect.width = this.viewWidth - viewportMargin * 2;
    this.viewportRect.height = this.viewHeight - viewportMargin * 2;
    
    this.vizViewportRect.setAttribute("x", this.viewLeft + viewportMargin);
    this.vizViewportRect.setAttribute("y", this.viewTop + viewportMargin);
    this.vizViewportRect.setAttribute("width", this.viewWidth - viewportMargin * 2);
    this.vizViewportRect.setAttribute("height", this.viewHeight - viewportMargin * 2);
    this.vizViewportRect.setAttribute("style", "fill: none; stroke: orange; stroke-width: " + (1.0 / this.location.scale));
    
    this.svg.setAttribute("viewBox", "" + this.viewLeft + " " + this.viewTop +
      " " + this.viewWidth + " " + this.viewHeight);
  },
  
  resize: function(event) {
    var clientRect = this.svg.getBoundingClientRect();
    // this.debugWindow.innerHTML = "new size: " + dim.width + ", " + dim.height;

    this.setViewBox();    
    this.syncVisibleTiles(this.viewportRect);
    // this.location.applyTransform(this.svg);
  },
  
  keyPress: function(event) {
  
    var rval;
      
    if (event.keyCode == Event.KEY_PAGEUP) {
      this.debugWindow.innerHTML = "zoom in";
      event.stopPropagation();
      rval = false;
    } else if (event.keyCode == Event.KEY_PAGEDOWN) {
      this.debugWindow.innerHTML = "zoom out";
      event.stopPropagation();
      rval = false;
    } else {
      // this.debugWindow.innerHTML = "keypress: " + event;
      rval = true;
    }
    
    return rval;
  },

  mouseDown: function(event) {
    if (event.isLeftClick()) {
      this.debugWindow.innerHTML = "left down";
      this.leftDown = true;
    }
  },

  mouseUp: function(event) {
    if (event.isLeftClick()) {
      this.debugWindow.innerHTML = "left up";
      this.leftDown = false;
    }
  },
  
  calculateScale: function() {
    return Math.pow(SCALE_BASE, this.zoomLevel);
  },

  mouseMove: function(event) {
  
    if (this.leftDown && this.lastEvent != null) {
    
      var dx = this.lastEvent.screenX - event.screenX;
      var dy = this.lastEvent.screenY - event.screenY;
      
      // this.debugWindow.innerHTML = "mouse pan: (" + dx + ", " + dy + 
      //   "); lastEvent: " + this.lastEvent.screenX + "; event: " + event.screenX;
    
      if (!event.ctrlKey) {
        
        this.location.centerX += dx / this.location.scale;
        this.location.centerY += dy / this.location.scale;
        
        // this.location.applyTransform(this.group);
        this.setViewBox();
        this.syncVisibleTiles(this.viewportRect);
      } else if (event.ctrlKey) {
        
        this.zoomLevel -= dy * 0.1;
        this.zoomLevel = Math.max(-1000.0, this.zoomLevel);
        this.zoomLevel = Math.min(1000.0, this.zoomLevel);
        this.location.scale = this.calculateScale();
        // this.location.scale -= (dy * 0.1);
        
        // this.debugWindow.innerHTML = "mouse zoom: dy: " + dy + 
        //   "; zoomLevel: " + this.zoomLevel + "; scale: " + this.location.scale;
        
        // this.location.applyTransform(this.group);
        this.setViewBox();
        this.syncVisibleTiles(this.viewportRect);
      }
      
    }
    
    this.lastEvent = event;
  },

  mouseOut: function(event) {
    this.debugWindow.innerHTML = "mouse out";
    this.leftDown = false;
  },

  mouseOver: function(event) {
    this.debugWindow.innerHTML = "mouse over";
  },
  
  mouseWheel: function(event) {
    
    var dwheel = Event.wheel(event);
    var element = Event.element(event);
    if (element == this.svg) {
      this.zoomLevel -= dwheel * 1.0;
      this.zoomLevel = Math.max(-1000.0, this.zoomLevel);
      this.zoomLevel = Math.min(1000.0, this.zoomLevel);
      this.location.scale = this.calculateScale();
      // this.location.scale -= (dy * 0.1);
    
      this.debugWindow.innerHTML = "mouse wheel: " + Event.wheel(event) + 
        "; zoomLevel: " + this.zoomLevel;
        
      // this.location.applyTransform(this.group);
      this.setViewBox();
      this.syncVisibleTiles(this.viewportRect);
    
      // Stop propogation so it doesn't scroll.
      event.stop();
    }
  },

  setScale: function(newScale) {
    this.location.scale = newScale;
    
    // apply the xform
  },

  setPosition: function(newx, newy) {
    this.location.centerX = newx;
    this.location.centerY = newy;
    
    // apply the xform
  },

  // Gets called every frame to cull any invisible tiles and load
  // any newly visible tiles.
  syncVisibleTiles: function(rect) {
  
    var clipCount = this.rootTile.syncVisibleTiles(rect);
    this.lastClipCount = clipCount;
    this.clipCounter.innerHTML = "clip count: " + clipCount;
  },
  
  getScale: function() {
    return this.location.scale;
  },
  
  getChildrenElement: function() {
    return this.group;
  },
  
  createURL: function() {
    return this.mapset.base;
  }
  
  
});

// A region in a map viewer.
var MapRegion = Class.create({

  initialize: function(parentObject, regionData) {
    this.parentObject = parentObject;
    // this.tileElement = tileElement;
    this.visible = false;
    
    // An array of MapTile objects.
    this.children = [];
    
    // Copy the attributes.
    this.g = regionData.g;
    this.minimumScale = regionData.minimumScale;

    // Default to invisible.    
    this.g.setAttribute("display", "none");
    
    // TODO: Recursively add any child regions.    
    // var regionTags = Element.select(this.g, ".region");
    // for (var i = 0; i < regionTags.length; i++) {
    //   var item = regionTags[i];
    //   var region = new MapRegion(this, new RegionTag(item));
    //   this.children.push(region);
    // }

  },

  // This is passed a MapRect object.
  syncVisibleTiles: function(rect) {
  
    // NOTE: rect is not used.

    var scale = this.getScale();
    
    /*
    this.top < xformed.y + xformed.height &&
            this.bottom > xformed.y &&
            this.left > xformed.x + xformed.width &&
        this.right < xformed.x
    */
    
    // Test intersection with this object's top, left, bottom and right.
    // If the object is visible, then make sure the scale >= minimum scale.
    if (scale >= this.minimumScale) {
      this.markVisible();
      this.children.each(function(item) {
        item.syncVisibleTiles(rect);
      });
      
    } else {
      this.markInvisible();
    }
    
    return 0; // TODO: return child rect count.
  
  },
  
  markVisible: function() {
  
    // Already visible.
    if (this.visible)
      return;
  
    this.visible = true;
    
    // this.elem.setAttribute("style", "fill: none; stroke: black; stroke-width: " + this.strokeWidth);
    
    this.g.setAttribute("display", "inline");
    
    
  },
  
  markInvisible: function() {
    this.visible = false;
    // this.elem.setAttribute("style", "fill: none; stroke: red; stroke-width: " + this.strokeWidth);
    this.g.setAttribute("display", "none");
    
    // Mark all children as invisible.
    
  },
  
  getScale: function() {
    return this.parentObject.getScale();
  },
  
  getContentElement: function() {
    return this.g;
  }
  
});

// A tile in a map viewer. The SVG structure is:
//   g content element
//     rect outline
//     g child element
//       child regions[]
var MapTile = Class.create({

  isRootTile: function() {
    return this.mapViewer == this.parentObject;
  },

  initialize: function(mapViewer, parentObject, tileData) {
    this.mapViewer = mapViewer;
    this.parentObject = parentObject;
    // this.tileElement = tileElement;
    this.visible = false;
    
    // An array of MapTile objects.
    this.children = [];
    
    // Copy the attributes.
    this.x = tileData.x;
    this.y = tileData.y;
    this.top = tileData.top;
    this.left = tileData.left;
    this.bottom = tileData.bottom;
    this.right = tileData.right;
    this.url = tileData.url;
    
    this.width = this.right - this.left;
    this.height = this.bottom - this.top;
    this.strokeWidth = this.width / 200.0;
    // this.strokeWidth = Math.min(this.strokeWidth, 2); // Hack...
    
    this.mainGroup = document.createElementNS(SVGNS, "g");
    this.mainGroup.setAttribute("class", this.url);
    this.mainGroup.setAttribute("transform", "translate(" + this.x + " " + this.y + ")");
    
    this.childGroup = document.createElementNS(SVGNS, "g");
    this.childGroup.setAttribute("class", "children");
    this.mainGroup.appendChild(this.childGroup);
    
    // Create a rect object.
    this.elem = document.createElementNS(SVGNS, "rect");
    this.elem.setAttribute("class", "outline");
    this.elem.setAttribute("x", "" + this.left);
    this.elem.setAttribute("y", "" + this.top);
    this.elem.setAttribute("width", "" + this.width);
    this.elem.setAttribute("height", "" + this.height);
    this.elem.setAttribute("style", "fill: gray; stroke: none; stroke-width: " + this.strokeWidth);
    this.elem.setAttribute("display", "none");
    this.mainGroup.appendChild(this.elem);    
    
    // this.tileScale = 1.0;
  },

  resize: function(newTop, newLeft, newBottom, newRight) {
    // TODO: change the g.
  },
  
  // This is passed a MapRect object.
  syncVisibleTiles: function(rect) {

    var xformed = new MapRect();
    xformed.x = rect.x - this.x;
    xformed.y = rect.y - this.y;
    xformed.width = rect.width;
    xformed.height = rect.height;
    
    var scale = this.getScale();
    
    // TODO: test scale against this.minimumScale
    
    var scaledWidth = this.width * scale;
      
    /*
    this.top < xformed.y + xformed.height &&
            this.bottom > xformed.y &&
            this.left > xformed.x + xformed.width &&
        this.right < xformed.x
    */
    
    
      var clipCount = 1;
      
    // Test intersection with this object's top, left, bottom and right.
    // If the object is visible, then make sure screen width is > 10.
    if (this.top < xformed.y + xformed.height &&
        this.bottom > xformed.y &&
        this.left < xformed.x + xformed.width &&
        this.right > xformed.x &&
        scaledWidth > 60) {
        
      this.markVisible();
      
      var len = this.children.length;
      for (var i = 0; i < len; i++) {
        clipCount += this.children[i].syncVisibleTiles(xformed);
      }
      
      // this.children.each(function(item) {
      //   item.syncVisibleTiles(xformed);
      // });
      
    } else {
      this.markInvisible();
    }
    
    return clipCount;
  
  },
  
  unload: function() {
    // Do nothing.
  },

  // Handles the successful result of an Ajax request for the tile's contents.
  loadContents: function(transport) {
  
    try {
      // this.childGroup.innerHTML = transport.responseText;
    
      debugOutput.innerHTML = "hello loadContents: " + transport.responseText;

      // Turn off bounding rect when contents arrive.
      this.elem.setAttribute("display", "none");
      
      var tileFragment = transport.responseXML.documentElement;
    
    
      // TODO: check for parsererror.
    
      tileFragment = document.importNode(tileFragment, true);
      Element.extend(tileFragment);    
    
      // FIXME: this info is in the documentURI and baseURI fields, so there is no need
      // to put it into an attribute.
      this.urlBase = tileFragment.getAttribute("urlbase");

      // Copy the data from the tile.    
      this.width = parseFloat(tileFragment.getAttribute("width"));
      this.height = parseFloat(tileFragment.getAttribute("height"));
      // this.headerScale = parseFloat(tileFragment.getAttribute("headerscale"));
    
      // Change the width, height, top, left, bottom and right
      // to reflect the data in the tile.
      this.bottom = this.top + this.height;
      this.right = this.left + this.width;
      this.elem.setAttribute("width", "" + this.width);
      this.elem.setAttribute("height", "" + this.height);

      debugOutput.innerHTML = "hello loadContents: tileFragment: " + tileFragment;

      // Since Element.select() isn't working in Chrome, this is a workaround.    
      var child = tileFragment.firstChild;
      while (child != null) {
        if (child.nodeName == "tile") {
          var tile = new MapTile(this.mapViewer, this, new TileTag(child)); // False means not the root. Hack.
          this.children.push(tile);
          var x = child;
          child = child.nextSibling;
          tileFragment.replaceChild(tile.getContentElement(), x);
        } else if (child.nodeName == "g") {
          if (child.getAttribute("class") == "region") {
            var region = new MapRegion(this, new RegionTag(child));
            this.children.push(region);
            var x = child;
            child = child.nextSibling;
            tileFragment.replaceChild(region.getContentElement(), x);
          } else {
            child = child.nextSibling;
          }
        } else {
          child = child.nextSibling;
        }
      }
    
      /*
      var tileTags = Element.select(tileFragment, "tile"); // tileFragment.select() doesn't work for some reason...
      debugOutput.innerHTML = "hello loadContents: tileTags.length: " + tileTags.length;
    
      for (var i = 0; i < tileTags.length; i++) {
        var item = tileTags[i];
        var tile = new MapTile(this, new TileTag(item));
        this.children.push(tile);
        tileFragment.replaceChild(tile.getContentElement(), item);
      }
    
      var regionTags = Element.select(tileFragment, ".region");
      for (var i = 0; i < regionTags.length; i++) {
        var item = regionTags[i];
        var region = new MapRegion(this, new RegionTag(item));
        this.children.push(region);
      }
      */

      this.childGroup.appendChild(tileFragment);
      
      // This zooms the viewport to fit the root tile's extents.
      if (this.isRootTile())
        this.mapViewer.setInitialZoom();
        
      // After loading, sync the visible tiles.
      // TODO: would be better to just calculate the viewport for this tile
      // and call this.syncVisibleTiles(tileRect).
      this.mapViewer.syncVisibleTiles(this.mapViewer.viewportRect);
      
    } catch (err) {
      debugOutput.innerHTML = "hello loadContents: err: " + err;
    }
    
  },
  
  markVisible: function() {
  
    // Already visible.
    if (this.visible)
      return;
  
    // if (this.children.length != 0)
    //   alert("wha huh?")
      
    this.visible = true;
    
    this.elem.setAttribute("style", "fill: none; stroke: green; stroke-width: " + this.strokeWidth);
    
    var onSuccessCallback = this.loadContents.bind(this);
    
    var fullpath = this.parentObject.createURL() + "/" + this.url + ".xml";
    
    debugOutput.innerHTML = "markVisible: fullpath: " + fullpath;
    
    new Ajax.Request(fullpath, {
      method: 'get',
      onSuccess: onSuccessCallback,
      onFailure: function(transport) {
        alert("didn't work");
      }
    });

  },
  
  createURL: function() {
    return this.parentObject.createURL() + "/" + this.urlBase;
  },
  
  markInvisible: function() {
    this.visible = false;
    this.elem.setAttribute("display", "");
    this.elem.setAttribute("style", "fill: gray; stroke: none; stroke-width: " + this.strokeWidth);
    
    // Remove all children.
    while (this.childGroup.firstChild != null)
      this.childGroup.removeChild(this.childGroup.firstChild);

    // Hopefully this removes all references?    
    this.children = [];
    this.tileData = null;
  },
  
  getScale: function() {
    return this.parentObject.getScale();
  },
  
  // Returns the element that contains the children for this tile.
  getChildrenElement: function() {
    return this.childGroup;
  },

  // Returns the main content element for this tile.  
  getContentElement: function() {
    return this.mainGroup;
  }
  
});

var MapRect = Class.create({

  initialize: function() {
    this.x = 0;
    this.y = 0;
    this.width = 0;
    this.height = 0;
  }

});

// Parses the attributes of a tile tag.
var TileTag = Class.create({

  initialize: function(tileTag) {
    this.x = parseFloat(tileTag.getAttribute("x"));
    this.y = parseFloat(tileTag.getAttribute("y"));
    this.top = parseFloat(tileTag.getAttribute("top"));
    this.left = parseFloat(tileTag.getAttribute("left"));
    this.bottom = parseFloat(tileTag.getAttribute("bottom"));
    this.right = parseFloat(tileTag.getAttribute("right"));
    this.url = tileTag.getAttribute("url");
  }

});

// Parses the attributes of a region tag.
var RegionTag = Class.create({

  initialize: function(regionTag) {
    this.g = regionTag; // Copy the g tag.
    this.minimumScale = parseFloat(regionTag.getAttribute("min-scale"));
  }

});

// Creates the top-level svg tag.
function createSVG() {
  svg = createSVGElement("svg");
  svg.setAttribute("version", "1.1");
  svg.setAttribute("baseProfile", "full");
  // svg.setAttribute("viewBox", "0 0 100 100");
  // svg.setAttribute("width", "100%");
  // svg.setAttribute("height", "100%");
  
  return svg;
}

// Creates an element with the SVG namespace.
function createSVGElement(elementType) {
  return document.createElementNS(SVGNS, elementType);
}

function appendLine(element, x1, y1, x2, y2, color) {
    line = document.createElementNS(SVGNS, "line");
    line.setAttribute("x1", x1);
    line.setAttribute("y1", y1);
    line.setAttribute("x2", x2);
    line.setAttribute("y2", y2);
    line.setAttribute("style", "fill: none; stroke: " + color);
    element.appendChild(line);
}
