function PatchMenuView(eventUtility)
{
  // Constants (Safari does not allow usage of "const" keyword)
  var USE_BUBBLE_ADD_EVENT_MODEL = false;
  var PX = "px";
  var OFFSCREEN_PX_COORD = "-1000" + PX;
  var SHOW_DISPLAY_STYLE = "block";
  var HIDE_DISPLAY_STYLE = "none";
  var RELATIVELY_POSITIONED = true;
  var ABSOLUTELY_POSITIONED = false;
  var PATCH_ELEMENT_ID_ATTRIBUTE = "patchElementId";
  var PATCH_ID_ATTRIBUTE = "patchId";
	var PATCH_IN_MY_PALETTE_ATTRIBUTE = "patchInMyPalette";
	var PALETTE_GUID_ATTRIBUTE = "paletteId";

  // Private members
  var eventUtility = eventUtility;
  var contentContainer;
  var patchHover;
  var patchMenu;
  var that = this; // Ugly workaround for JavaScript's inability to call public functions from private ones
  //  Working vars for the patch that we're currently working with
  var patch;
  var patchTopLeftPos;
  var patchBottomRightPos;
  var contentContainerTopRightPos; // Has to be updated depending on scroll position, so cannot just read once
  
  // --------------
  // Public Methods
  // --------------
  
  this.addPageEventListeners = function addPageEventListeners(onMouseOverBodyListener,
    onScrollContentContainerListener, onMouseOverHoverListener, onMouseOverPatchMenuListener)
  {
    eventUtility.addEventListener(document, 'mouseover', onMouseOverBodyListener, USE_BUBBLE_ADD_EVENT_MODEL);
    eventUtility.addEventListener(getContentContainer(), 'scroll', onScrollContentContainerListener, USE_BUBBLE_ADD_EVENT_MODEL);
    eventUtility.addEventListener(getPatchHover(), 'mouseover', onMouseOverHoverListener, USE_BUBBLE_ADD_EVENT_MODEL);
    eventUtility.addEventListener(getPatchMenu(), 'mouseover', onMouseOverPatchMenuListener, USE_BUBBLE_ADD_EVENT_MODEL);
  }
  
  this.addPatchEventListener = function addPatchEventListener(patchElementId, onMouseOverPatchListener)
  {
  	var targetPatch = document.getElementById(patchElementId);
  	eventUtility.addEventListener(targetPatch, 'mouseover', onMouseOverPatchListener, USE_BUBBLE_ADD_EVENT_MODEL);
  }
  
  this.showPatchHoverOnPatch = function showPatchHoverOnPatch()
  {
    // The show must happen before we set the coordinates
    showElement(patchHover);
    
    var patchElementId = patch.id;
    var patchId = patch.getAttribute(PATCH_ID_ATTRIBUTE);
    
    patchHover.style.left = (patchBottomRightPos.posX - patchHover.clientWidth - 1) + PX;
  	patchHover.style.top = (patchBottomRightPos.posY - patchHover.clientHeight - 1) + PX;
  	patchHover.setAttribute(PATCH_ELEMENT_ID_ATTRIBUTE, patchElementId);
  	patchHover.setAttribute(PATCH_ID_ATTRIBUTE, patchId);
  }
  
  // Move the elements offscreen and hide them
  this.hideHoverAndPatchMenu = function hideHoverAndPatchMenu()
  {
    hideElement(patchHover);

    this.hidePatchMenu();
  }
  
  this.hidePatchMenu = function hidePatchMenu()
  {
  	hideElement(patchMenu);
  }
  
  // Called by the controller to indicate the patch that we're currently working with
  this.setWorkingPatch = function setWorkingPatch(workingPatch)
  {
    patch = workingPatch;
    patchTopLeftPos = getElementTopLeftPosition(patch, RELATIVELY_POSITIONED);
    patchBottomRightPos = getElementBottomRightPosition(patch, RELATIVELY_POSITIONED);
    
    contentContainerTopRightPos = getElementTopRightPosition(contentContainer, ABSOLUTELY_POSITIONED);
  }
  
  // Read the patch_id attribute of patch hover and set the working patch based on it
  this.setWorkingPatchFromPatchHover = function setWorkingPatchFromPatchHover()
  {
    var patchElementId = patchHover.getAttribute(PATCH_ELEMENT_ID_ATTRIBUTE);
    var workingPatch = document.getElementById(patchElementId);
    this.setWorkingPatch(workingPatch);
  }
  
  this.isPatchMenuAssociatedWithCurrentPatch = function isPatchMenuAssociatedWithCurrentPatch()
  {
    var patchElementId = patchMenu.getAttribute(PATCH_ELEMENT_ID_ATTRIBUTE);
    return (patch.id == patchElementId);
  }
  
  this.getPatchMenuLocationInformation = function getPatchMenuLocationInformation()
  {
    // Must show the patch menu before asking any questions about its position or dimensions
    showPatchMenuIfNotVisible();
     
    var contentContainerHeight = contentContainer.clientHeight;
    var patchWidth = patch.clientWidth;
    var patchMenuWidth = patchMenu.clientWidth;
    var patchMenuHeight = patchMenu.clientHeight;
    
    var returnValue = {
      isTopOfPatchAboveContentContainer : patchTopLeftPos.posY < contentContainerTopRightPos.posY,
      isBottomOfPatchBelowContentContainer : patchBottomRightPos.posY > contentContainerTopRightPos.posY + contentContainerHeight,
      wouldRightOfPatchMenuExceedContentContainer : patchTopLeftPos.posX + patchWidth + patchMenuWidth > contentContainerTopRightPos.posX,
      wouldRightOfBottomAlignedPatchMenuExceedContentContainer : patchTopLeftPos.posX + patchMenuWidth > contentContainerTopRightPos.posX,
      wouldBottomOfPatchMenuExceedContentContainer: patchTopLeftPos.posY + patchMenuHeight > contentContainerTopRightPos.posY + contentContainerHeight
    };
    return returnValue;
  }
  
  this.showPatchMenuAbovePatchAlignedWithTopRight = function showPatchMenuAbovePatchAlignedWithTopRight()
  {
    var patchWidth = patch.clientWidth;
    var patchMenuWidth = patchMenu.clientWidth;
    var patchMenuHeight = patchMenu.clientHeight;
    
    showPatchMenuAtCoords(
      patchTopLeftPos.posX + patchWidth - patchMenuWidth,
  	  patchTopLeftPos.posY - patchMenuHeight);
  }
  
  this.showPatchMenuRightOfPatchAlignedWithBottomRight = function showPatchMenuRightOfPatchAlignedWithBottomRight()
  {
    var patchWidth = patch.clientWidth;
    var patchHeight = patch.clientHeight;
    var patchMenuHeight = patchMenu.clientHeight;
    
    showPatchMenuAtCoords(
      patchTopLeftPos.posX + patchWidth,
      patchTopLeftPos.posY + patchHeight - patchMenuHeight);
  }
  
  this.showPatchMenuBelowPatchAlignedWithBottomRight = function showPatchMenuBelowPatchAlignedWithBottomRight()
  {
    var patchWidth = patch.clientWidth;
    var patchHeight = patch.clientHeight;
    var patchMenuWidth = patchMenu.clientWidth;
    
    showPatchMenuAtCoords(
      patchTopLeftPos.posX + patchWidth - patchMenuWidth,
      patchTopLeftPos.posY + patchHeight);
  }
  
  this.showPatchMenuBelowPatchAlignedWithBottomLeft = function showPatchMenuBelowPatchAlignedWithBottomLeft()
  {
    var patchHeight = patch.clientHeight;
    
    showPatchMenuAtCoords(
      patchTopLeftPos.posX,
      patchTopLeftPos.posY + patchHeight);
  }
  
  this.showPatchMenuRightOfPatchAlignedWithTopRight = function showPatchMenuRightOfPatchAlignedWithTopRight()
  {
    var patchWidth = patch.clientWidth;
    
    showPatchMenuAtCoords(
      patchTopLeftPos.posX + patchWidth,
      patchTopLeftPos.posY);
  }

	this.getCurrentPatchId = function getCurrentPatchId()
	{
		return patchMenu.getAttribute(PATCH_ID_ATTRIBUTE);
	}
	
	this.getCurrentPaletteId = function getCurrentPaletteId()
	{
	  return patch.getAttribute(PALETTE_GUID_ATTRIBUTE);
	}
	
	// Called by Add/Remove link
	this.toggleAddRemoveLinkVisibility = function toggleAddRemoveLinkVisibility()
	{
		var patchInMyPalette = patch.getAttribute(PATCH_IN_MY_PALETTE_ATTRIBUTE) == 'true';
		setIsInMyPalette(! patchInMyPalette);
	}
	
	// Click to Add function
	this.setPatchForRemove = function setPatchForRemove()
	{
		setIsInMyPalette(true);
	}
	
	this.isPatchInMainArea = function isPatchInMainArea()
	{
		// patch id on main area has an id like "patch_505_1_0"
		return patch.id.substring(6).indexOf("_1_") > -1;
	}

  // ---------------
  // Private Methods
  // ---------------
  
  function showElement(el)
  {
  	el.style.display = SHOW_DISPLAY_STYLE;
  }
  
  function hideElement(el)
  {
  	el.style.display = HIDE_DISPLAY_STYLE;
  }

	function setElementVisibility(el, makeVisible)
	{
		if(makeVisible)
		{
			showElement(el);
		}
		else
		{
			hideElement(el);
		}
	}
  
  // Adapted from http://www.webreference.com/dhtml/diner/realpos_old/realpos4.html
  function getElementTopLeftPosition(el, isRelativelyPositioned) 
  {
    posX = el.offsetLeft;
    posY = el.offsetTop;

  	if (isRelativelyPositioned)
  	{
  		tempEl = el.offsetParent;
  		while (tempEl != null) 
  		{
  			posX += tempEl.offsetLeft;
  			posY += tempEl.offsetTop;

  			tempEl = tempEl.offsetParent;
  		}
  	}

  	position = { posX : posX, posY : posY };

  	// If patch is in my palette do not compensate contentContainer scrolling 
		if (isRelativelyPositioned && that.isPatchInMainArea())
  	{
  	  position.posY -= contentContainer.scrollTop;
    }

    return position;
  }

  function getElementTopRightPosition(el, isRelativelyPositioned)
  {
    var position = getElementTopLeftPosition(el, isRelativelyPositioned);
  	position.posX += el.clientWidth;
  	return position;
  }

  function getElementBottomRightPosition(el, isRelativelyPositioned)
  {
  	var position = getElementTopLeftPosition(el, isRelativelyPositioned);
  	position.posX += el.clientWidth;
  	position.posY += el.clientHeight;
  	return position;
  }
  
  // Must show the patch menu before asking any questions about its position or dimensions
  function showPatchMenuIfNotVisible()
  {
    if (patchMenu.style.display == HIDE_DISPLAY_STYLE)
    {
			// Display the correct add/remove link before we show the menu
			var patchInMyPalette = patch.getAttribute(PATCH_IN_MY_PALETTE_ATTRIBUTE) == 'true';
			setAddRemoveLinkVisibility(patchInMyPalette);
			
      patchMenu.style.left = OFFSCREEN_PX_COORD;
      patchMenu.style.top = OFFSCREEN_PX_COORD;
      showElement(patchMenu);
    }
  }

	function setAddRemoveLinkVisibility(patchInMyPalette)
	{	
		// If portal is called from picker then we hide add_to_palette and remove_from_palette items menu
		var patchAddToPalette = document.getElementById('add_to_palette');
		if(patchAddToPalette) setElementVisibility(patchAddToPalette, ! patchInMyPalette);
		
		var patchRemoveFromPalette = document.getElementById('remove_from_palette');
		if(patchRemoveFromPalette) setElementVisibility(patchRemoveFromPalette, patchInMyPalette);
	}
	
	function setIsInMyPalette(patchInMyPalette)
	{
	  // Don't iterate through page patches unnecessarily
		var patchInMyPaletteValue = patchInMyPalette ? 'true' : 'false';
		if (patch.getAttribute(PATCH_IN_MY_PALETTE_ATTRIBUTE) == patchInMyPaletteValue)
		{
		  return;
		}
				
		// Apply the attribute swap to every patch div on the page with the same patch ID
		var patchId = patch.getAttribute(PATCH_ID_ATTRIBUTE);
		var patchIndex = 0;
		var currentPatch = document.getElementById("patch_" + patchId + "_1_" + patchIndex++);
		while (currentPatch)
		{
			currentPatch.setAttribute(PATCH_IN_MY_PALETTE_ATTRIBUTE, patchInMyPaletteValue);
			
			currentPatch = document.getElementById("patch_" + patchId + "_1_" + patchIndex++);
		}

		setAddRemoveLinkVisibility(patchInMyPalette);
	}
  
  function showPatchMenuAtCoords(leftCoord, topCoord)
  {
    patchMenu.style.left = leftCoord + PX;
    patchMenu.style.top = topCoord + PX;
    
    var patchElementId = patch.id;
    var patchId = patchHover.getAttribute(PATCH_ID_ATTRIBUTE);
    patchMenu.setAttribute(PATCH_ELEMENT_ID_ATTRIBUTE, patchElementId);
    patchMenu.setAttribute(PATCH_ID_ATTRIBUTE, patchId);
  }

  // These getters are for vars that cannot be initialized in the constructor
  // because these elements do not yet exist when the script is initially
  // processed in the <head> of the HTML document
  function getContentContainer()
  {
    if (contentContainer == null)
    {
      contentContainer = document.getElementById('content_container');
    }
    
    return contentContainer;
  }
  
  function getPatchHover()
  {
    if (patchHover == null)
    {
      patchHover = document.getElementById('patch_hover');
    }
    
    return patchHover;
  }
  
  function getPatchMenu()
  {
    if (patchMenu == null)
    {
      patchMenu = document.getElementById('patch_menu');
    }
    
    return patchMenu;
  }
}

function EventUtility()
{
  // --------------
  // Public Methods
  // --------------
  
  this.addEventListener = function addEventListener(element, eventName, handler, useCaptureAddEventModel)
  {
    if (element.addEventListener)
    {
      element.addEventListener(eventName, handler, useCaptureAddEventModel);
    }
    else
    {
      // IE does not support the addEventListener function
      element.attachEvent('on' + eventName, handler);
    }
  }
  
  this.getEventTarget = function getEventTarget(e)
  {
  	return (e.target ? e.target : e.srcElement);
  }
  
  this.cancelEventBubble = function cancelEventBubble(e)
  {
  	if (!e) var e = window.event;
    // Need to always set this property, even though it's IE-specific
    // Firefox, etc. will ignore it, and there is no way to check if it already exists with an if statement
    e.cancelBubble = true;
  	if (e.stopPropagation) e.stopPropagation();
  }
}

function PatchMenuController(patchMenuView, eventUtility)
{
  // Private members
	var patchMenuView = patchMenuView;
	var eventUtility = eventUtility;
	var eventListeningActive = false;

  // --------------
  // Public Methods
  // --------------
  
  // Should be called at the very end of the page, once all of the objects are loaded
	this.addPageEventListeners = function addPageEventListeners()
  {
    patchMenuView.addPageEventListeners(onMouseOverBody, onScrollContentContainer,
      onMouseOverHover, onMouseOverPatchMenu);
      
    eventListeningActive = true;
  }
  
  // Should be called for all of the patches on the page
  this.addPatchEventListener = function addPatchEventListener(patchElementId)
  {
    patchMenuView.addPatchEventListener(patchElementId, onMouseOverPatch);
  }
  
  // Allow the event handling to be paused and resumed for functions like Similar, Harmony
	// Main Area: area below My Palette
  // doesActionAffectMainArea: false for Add/Remove, true for everything else
  this.pauseEventListeners = function pauseEventListeners(doesActionAffectMainArea)
  {
    var isCurrentPatchInMainArea = patchMenuView.isPatchInMainArea();
	  if (isCurrentPatchInMainArea == doesActionAffectMainArea)
	  {
	    eventListeningActive = false;

      patchMenuView.hideHoverAndPatchMenu();
	  }
  }
  
  this.resumeEventListeners = function resumeEventListeners()
  {
    eventListeningActive = true;
  }

	this.getCurrentPatchId = function getCurrentPatchId()
	{
		return patchMenuView.getCurrentPatchId();
	}
	
	this.getCurrentPaletteId = function getCurrentPaletteId()
	{
	  return patchMenuView.getCurrentPaletteId();
	}
	
	this.toggleAddRemoveLinkVisibility = function toggleAddRemoveLinkVisibility()
	{
	  patchMenuView.toggleAddRemoveLinkVisibility();
	}
	
	// Click to Add function
	this.setPatchForRemove = function setPatchForRemove()
	{
		patchMenuView.setPatchForRemove();
	}
  
  // --------------
  // Event Handlers
  // --------------
  
  function onMouseOverBody(e)
  {
    // Only necessary to check eventListeningActive in event handlers where something is being shown
    
  	patchMenuView.hideHoverAndPatchMenu();
  }
  
  function onScrollContentContainer(e)
  {
    // Only necessary to check eventListeningActive in event handlers where something is being shown
        
    // If we do not hide the elements for this event, they will appear on top of other divs,
    // because Safari does not process other events like onMouseOver for body
    // when scrolling is done by the user
    patchMenuView.hideHoverAndPatchMenu();
  }
  
  function onMouseOverPatch(e)
  {
    eventUtility.cancelEventBubble(e);
    if (!eventListeningActive)
    {
      return;
    }
    
  	var patch = eventUtility.getEventTarget(e);
  	patchMenuView.setWorkingPatch(patch);
  	
  	// Hide any previously visible menu if it was displayed for a different patch
  	if (! patchMenuView.isPatchMenuAssociatedWithCurrentPatch())
  	{
  	  patchMenuView.hidePatchMenu();
  	}
  	
  	// Do not show the hover if bottom of the patch is below the content container
  	var info = patchMenuView.getPatchMenuLocationInformation();
  	if (info.isBottomOfPatchBelowContentContainer)
  	{
  	  return;
  	}

  	patchMenuView.showPatchHoverOnPatch();
  }
  
  function onMouseOverHover(e)
  {
    eventUtility.cancelEventBubble(e);
    if (!eventListeningActive)
    {
      return;
    }
    
    patchMenuView.setWorkingPatchFromPatchHover();

    // Perform different alignment logic depending on how we might exceed the container
    var info = patchMenuView.getPatchMenuLocationInformation();
    if (!patchMenuView.isPatchInMainArea())
    {
      // My Palette area of the page
      if (info.wouldRightOfPatchMenuExceedContentContainer)
      {
        patchMenuView.showPatchMenuBelowPatchAlignedWithBottomRight();
      }
      else
      {
        patchMenuView.showPatchMenuRightOfPatchAlignedWithTopRight();
      }
    }
    else if (info.isTopOfPatchAboveContentContainer)
    {
      // We know we can't show the menu to the right or above the patch
      if (info.wouldRightOfBottomAlignedPatchMenuExceedContentContainer)
      {
        patchMenuView.showPatchMenuBelowPatchAlignedWithBottomRight();
      }
      else
      {
        patchMenuView.showPatchMenuBelowPatchAlignedWithBottomLeft();
      }
    }
    else
    {
      if (info.wouldRightOfPatchMenuExceedContentContainer)
      {
        // Our last choice is to display it above the patch
        patchMenuView.showPatchMenuAbovePatchAlignedWithTopRight();
      }
      else
      {
        if (info.wouldBottomOfPatchMenuExceedContentContainer)
        {
          patchMenuView.showPatchMenuRightOfPatchAlignedWithBottomRight();
        }
        else
        {
          // We prefer to display the menu to the right of the patch
          patchMenuView.showPatchMenuRightOfPatchAlignedWithTopRight();
        }
      }
    }
  }
  
  // Needs to happen just to cancel the event, so that it doesn't bubble up to the body
  function onMouseOverPatchMenu(e)
  {
    // Only necessary to check eventListeningActive in event handlers where something is being shown
    
  	eventUtility.cancelEventBubble(e);
  }
}

// Declare a controller that can be called by script further down the page
var eventUtility = new EventUtility();
var patchMenuController = new PatchMenuController(new PatchMenuView(eventUtility), eventUtility);
