// Global variables related to PickChow
PICKCHOW = {
  completeMealRule: '1 meat, bean or dairy, 1 grain or starchy vegetable, and 1 vegetable',
  sections: {
    'meat': [[85, 206], [214, 328], [50, 358]],
    'dairy': [[172, 142], [271, 126], [248, 248]],
    'grains': [[70, 423], [242, 386], [218, 547]],
    'fruit': [[302, 326], [341, 161], [464, 301]],
    'vegetables': [[317, 376], [310, 535], [462, 373]],
    'dessert': [472, 90] // special, no centroid calculation necessary
  },
  deleteCodes: $A([Event.KEY_BACKSPACE, Event.KEY_DELETE, Event.KEY_ESC]),

  /** A few helpers **/

  // Take coordinates of points p, a and b and determines which side
  // of line ab p is on (positive or negative number).
  ss: function(p, a, b){
    if (b) {
      var ss = (p[1] - a[1]) * (b[0] - a[0]) - (p[0] - a[0]) * (b[1] - a[1]);
      return ss;
    } else {
      return -1;
    }
  },

  inside_triangle: function(p, a, b, c){
    // Inside triangle if all positive or negative "side."
    if (this.ss(p, a, b) * this.ss(p, b, c) > 0 && this.ss(p, b, c) * this.ss(p, c, a) > 0) {
      return true;
    } else {
      return false;
    }
  },

  centroid: function(a, b, c){
    return [((a[0] + b[0] + c[0]) / 3.0), ((a[1] + b[1] + c[1]) / 3.0)];
  },

  // Is the specified position inside the specified element
  inside_element: function(position, element) {
    var offset = element.cumulativeOffset();
    var dims = element.getDimensions();
    return position.x > offset[0] && position.x < (offset[0] + dims.width) &&
      position.y > offset[1] && position.y < (offset[1] + dims.height)
  },

  centered_offset: function(element) {
    return {x: element.getDimensions().width / 2, y: element.getDimensions().height / 2}
  }
};

FoodDb = {
  foodGroups: $H(), // by id (must preload when the page loads)
  foodGroupsByName: $H(),
  foods: $H(), // by id
  foodsByGroupName: $H(),

  addFoodGroup: function(foodGroup) {
    if (!this.foodGroups.get(foodGroup.id)) {
      this.foodGroups.set(foodGroup.id, foodGroup);
      this.foodGroupsByName.set(foodGroup.name, foodGroup);
      this.foodsByGroupName.set(foodGroup.name, $A());
      return foodGroup;
    } else {
      return false;
    }
  },

  addFood: function(food){
    if (!this.foods.get(food.id)) {
      this.foods.set(food.id, food);
      var fg = food.getFoodGroup()
      if (fg) {this.foodsByGroupName.get(fg.name).push(food);}
      return food;
    } else {
      return false;
    }
  },

  // Like AR model... no view code
  FoodGroup: Class.create({
    initialize: function(data) {
      Object.extend(this, data); // copy properties to this object
      if (data.parent_id) {
        // Map the existing parent to this child, OR if the parent is created after
        // some of the children, find the children and set their parent to this
        if (!(this.parent = FoodDb.foodGroups.get(data.parent_id))) {
          FoodDb.foodGroups.each(function(fg){if (fg.parent_id == this.id) {fg.parent = this}}.bind(this));
        }
      }
    },

    // Is this the meat group or one of it's subgroups?'
    isMeat: function() {
      return this.name == 'meat' || (this.parent && this.parent.name == 'meat')
    },

    foods: function() {
      FoodDb.getFoods(this);
    },

    hasSubGroups: function() {
      return this.subGroups().size() > 0;
    },

    subGroups: function() {
      return FoodDb.foodGroups.values().select(function(fg){
        return fg.parent == this
      }.bind(this))
    }
  }),

  // Like AR model... no view code
  Food: Class.create({
    initialize: function(data) {
      Object.extend(this, data); // copy properties to this object
      this.food_group = FoodDb.foodGroups.get(data.food_group_id);
    },

    // For nutrition
    hasFoodGroup: function(name) {
      return $A(this.getMainFoodGroups()).any(function(fg){return fg.name == name});
    },

    // For chowbox tabs
    getFoodGroup: function() {
      return this.food_group;
    },

    // For chowbox tabs
    getMainFoodGroup: function() {
      var foodGroup = this.getFoodGroup();
      return foodGroup.parent || foodGroup;
    },

    // For nutrition
    getFoodGroups: function(){
      return this.food_group_ids.collect(function(id){
        return FoodDb.foodGroups.get(id)});
    },

    // For nutrition (e.g. meal completeness)
    getMainFoodGroups: function() {
      var foodGroups = this.getFoodGroups();
      return foodGroups.collect(function(fg){return fg.parent || fg});
    }
  }),

  // Like AR model... no view code
  Meal: Class.create({
    initialize: function(foods){
      this.foods = foods || $A();
    },

    empty: function() {
      return !this.foods || this.foods.size() == 0;
    },

    isComplete: function() {
      return (this.hasFoodGroup('meat') || this.hasFoodGroup('dairy')) &&
              this.hasFoodGroup('vegetables') &&
              (this.hasFoodGroup('grains') || this.hasFoodGroup('starchy_vegetables'));
    },

    getFoodsForGroup: function(foodGroup) {
      return this.foods.select(function(f){return f.getFoodGroup() == foodGroup})
    },

    hasFoodGroup: function(name) {
      return this.foods.any(function(food){
        return food.hasFoodGroup(name)
      }.bind(this));
    },

    addFood: function(food) {
      this.foods.push(food);
      return food;
    },

    removeFood: function(food) {
      var index = this.foods.indexOf(food);
      return index >= 0 ? this.foods.splice(index, 1) : false;
    }
  })
};


// This is not a class (only one per page) and it directly modifies the DOM
// If it were a class, it should get a reference to a parent DOM element and modify
// only that element. I don't see ever using more than one PickChow on a page.
PickChow = {
  lastQstring: null,
  activeFoodGroup: null,
  meterManager: null,
  downloadedFoods: $H(), // We need to remember which (complete) food groups
                        // we've downloaded (FoodDB shouldn't need to know this).
  foodClass: 'food',
  showedMoreBox: false,
  user: null,
  idleTime: 60000,
  idleTimeout: null,
  setIdle: function(){
    this.idleTimeout = window.setTimeout(function(){
      PickChow.playSound('idle');
    }, this.idleTime );
  },
  moreInfoTimeouts: [],
  clearMoreTimeouts: function() {
    while (this.moreInfoTimeouts.length > 0) {
      var to = this.moreInfoTimeouts.pop();
      window.clearTimeout(to);
    }
  },

  // Methods to get display elements
  getPlate: function() {return $('plate');},
  getFoodGroup: function(foodGroup) {return $('foodgroup-' + foodGroup.name);},
  getFoodGroupTab: function(foodGroup) {return $$('#chowbox ul#tabs li a.' + foodGroup.name).first();},
  getSubGroups: function(foodGroup) {return $('subgroups-' + foodGroup.name);},
  getChowbox: function(){return $('foods-container');},
  getFood: function(food) {return $('food-' + food.id);},
  getSearchInput: function() {return $('q');},
  getSearchGo: function() {return $('go');},
  getFoodChoicesHeader: function() {return $('aux-header');},
  getLoading: function() {return $('loading');},

  init: function(meterManager, user, maxFoods, sounds) {
    this.meterManager = meterManager;
    if (this.user = user.user) {
      this.showedMoreBox = this.user.saw_more_box ? true : false;
      this.user.update_account_url = user.update_account_url;
    }
    this.initFoodGroups();
    this.Plate.init(this.getPlate());
    this.switchFoodGroup('vegetables');
    this.initSearch();
    this.Meal.maxFoods = maxFoods;
    this.sounds = sounds;
    this.showMoreTimeout = null;

    // Set up sound effects
    // DISABLED: A single jPlayer is used due to IE issue with multiple players
//    $H(sounds).each(function(pair){
//      var div = this.getSoundDiv(pair.key);
//      jQuery(div).jPlayer({
//        swfPath: '/javascripts',
//        ready: function(url) {this.element.jPlayer("setFile", url)}.curry(pair.value)
//      });
//    }, this);
    jQuery(this.getSoundDiv()).jPlayer({
      swfPath: '/javascripts'
    });

    // idle sound
    Event.observe(document.body, 'mousemove', function(){
      window.clearTimeout(this.idleTimeout);
      this.stopSound('idle');
      this.setIdle();
    }.bind(this));

    // When the user clicks anywhere on PickChow, stop the automatic "more info" popup
    $('pickchow').observe('mousedown', this.clearMoreTimeouts.bind(this));
  },

  restart: function() {
    var result = false;
    if (PickChow.getMeal().foods.size() > 0) {
      jConfirm('Are you sure you want to clear the plate?', 'Start Over?', function(r) {
        if (r) {
          result = true;
          this.Plate.clear(this.getPlate());
        }
      }.bind(this));
    } else {
      jAlert("You don't have any food on your plate.");
    }
    return result;
  },

  // Create DOM elements for all the food group "chow boxes" (and any subgroups)
  // TODO: If the FoodDb group data was not preloaded, get data via AJAX.
  initFoodGroups: function() {
    var chowbox = this.getChowbox();
    FoodDb.foodGroups.values().each(function(foodGroup){
      chowbox.insert(
        new Element('div', {
          'id': 'foodgroup-' + foodGroup.name,
          'style': 'display: none;',
          'class': 'foodgroup'
        })
      );

      if (foodGroup.hasSubGroups()) {
        var subGroupsDiv = new Element('ul', {'id': 'subgroups-' + foodGroup.name, 'class': 'subgroups'});
        chowbox.insert(subGroupsDiv);
        foodGroup.subGroups().sortBy(function(fg){return fg.name}).each(function(subGroup){
          // Draw the subgroup
          var groupItem = new Element('li', {'id': 'subgroup-' + subGroup.name, 'class': 'subgroup'}).insert(
            new Element('a', {'href': '#'}).insert(subGroup.title)
          );

          subGroupsDiv.insert(groupItem); // Insert new element into the DOM before adding 'onclick' observer

          // Before switching to the specific subgroup, hide (w/animation) the subgroups list
          groupItem.observe('click', function() {
            new Effect.Move(subGroupsDiv, {
              x: -100, y: 0,
              mode: 'relative',
              duration: 0.25,
              fps: 99,
              afterFinish: function(){
                PickChow.switchFoodGroup(subGroup.name);
                subGroupsDiv.writeAttribute("style", "");
                subGroupsDiv.hide();
              }.bind(this)
            });
            return false;
          }.bind(this));
        }.bind(this));
      }
    }.bind(this));
  },

  initSearch: function() {
    var ieVersion = getInternetExplorerVersion();
    if (ieVersion >= 8) {
      $("q").style.margin = 0;
      $("go").style.margin = "5px 0 0 -25px";
      $("go").style.position = "absolute";
    }

    var searchInput = this.getSearchInput();
    searchInput.observe('click', function(){
      if (searchInput.classNames().include('watermark')) {
        this.lastQstring = searchInput.value;
        searchInput.value = '';
        searchInput.removeClassName('watermark');
      }
    }.bind(this));

    searchInput.observe('blur', function(){
      if (searchInput.value.length == 0) {
        searchInput.addClassName('watermark');
        searchInput.value = this.lastQstring;
      }
    }.bind(this));
  },

  // When starting with an existing meal...
  loadMeal: function(meal){ // FoodDb.Meal
    // For each food in the meal...
    var elements = [];
    var po = this.getPlate().cumulativeOffset();
    var co = PICKCHOW.centered_offset(this.getPlate());
    var plate_center = {x: po[0] + co.x, y: po[1] + co.y};
    meal.foods.each(function(food){
      FoodDb.addFood(food); // ...the food group may not have been downloaded yet
      var element = this.Food.createForPlate(food, plate_center); // create element

      elements.push(element);
    }.bind(this));

    // Now move all the foods to their right place
    elements.each(function(e){
      this.Plate.moveToZone(e);
      this.updateMeters([this.Plate.getFood(e)], 1);
    }.bind(this));
  },

  getMeal: function() {
    return new FoodDb.Meal(this.Plate.getFoods(this.getPlate()).collect(
      function(e){return this.Plate.getFood(e);}.bind(this)
    ));
  },

  downloadFoods: function(foodGroup){
    var loading = this.getLoading();
    var groupDiv = this.getFoodGroup(foodGroup);
    new Ajax.Request('/foodgroups/' + foodGroup.id + '/foods.json', {
      method: 'get',
      onLoading: function(){
        loading.show();
      }.bind(this),
      onSuccess: function(transport){
        transport.responseJSON.each(function(item){
          var food = FoodDb.foods.get(item.food.id);
          if (!food) {food = FoodDb.addFood(new FoodDb.Food(item.food));}
          if (!this.getFood(food)) {
            this.Food.createForChowbox(food, groupDiv);
          }
        }.bind(this));
        this.downloadedFoods.set(foodGroup.name, true);
      }.bind(this),
      onFailure: function(){
        jAlert('There was a problem getting foods for the ' + foodGroup.name + ' group. Please try again.');
      }.bind(this),
      onComplete: function(){
        loading.hide();
      }.bind(this)
    });
  },

  performSearch: function(query) {
    var foodGroup = this.activeFoodGroup;
    query = query.strip(); // leading and trailing whitespace

    // Ignore bad requests
    if ((query == 'search ' + foodGroup.title) || (query.length == 0)) {return;}

    new Ajax.Request('/foodgroups/' + foodGroup.id + '/foods/search.json?q=' + query, {
      method: 'get',
      onLoading: function(){
        $('loading').show();
      }.bind(this),
      onSuccess: function(transport){
        var groupDiv = this.getFoodGroup(foodGroup);
        // using a class name ensures that it will only be unhidden when the search filter is removed
        groupDiv.childElements().each(function(e){e.addClassName('search-hidden')});
        transport.responseJSON.each(function(id){
          var food, foodDiv;
          if (food = FoodDb.foods.get(id.toString()))
            if (foodDiv = groupDiv.select('#food-' + food.id)[0])
              foodDiv.removeClassName('search-hidden');
        }.bind(this));
        this.getFoodChoicesHeader().update(this.createSwitchGroupButton(foodGroup).update('Clear Search')).show();
        this.getChowbox().addClassName('short');
      }.bind(this),
      onFailure: function(){
        jAlert('There was a problem getting search results. Please try again.');
      }.bind(this),
      onComplete: function(){
        $('loading').hide();
      }.bind(this)
    });
  },

  // Accepts a FoodGroup object or group name
  switchFoodGroup: function(group_or_name) {
    var foodGroup = typeof(group_or_name) == 'string' ? FoodDb.foodGroupsByName.get(group_or_name) : group_or_name;
    var parent = foodGroup.parent;
    var isParent = foodGroup.hasSubGroups();
    var chowbox = this.getChowbox();

    // Play a sound for certain groups
    if ($A(['dairy', 'chicken']).include(foodGroup.name)) {
      PickChow.playSound('click_' + foodGroup.name);
    }

    // Hide everything in the foods-container (which also holds subgroups)
    chowbox.childElements().each(Element.hide);

    // Reset the selected tab if this isn't a subgroup
    if (!parent) {
      $$('#pickchow .current').each(function(tab){tab.removeClassName("current")});
    }

    // Update "Back" button
    var header = this.getFoodChoicesHeader();
    if (parent) {
      header.update(
        this.createSwitchGroupButton(parent).update('< Back')
      ).show();
      this.getChowbox().addClassName('short');
    } else {
      header.hide();
      this.getChowbox().removeClassName('short');
    }

    // Load entire food group's foods
    if (!this.downloadedFoods.get(foodGroup.name)) {
      this.downloadFoods(foodGroup);
    }

    // Show the foods or subgroups
    (isParent ? this.getSubGroups : this.getFoodGroup).call(this, foodGroup).show();

    // Clear any past search results
    $$('.search-hidden').each(function (item) {
      item.removeClassName('search-hidden');
    }.bind(this));

    // Update currently selected tab (first clear any selected tabs)
    $$('#pickchow .current').each(function(tab){tab.removeClassName("current")});
    this.getFoodGroupTab(parent || foodGroup).addClassName("current");

    // Update the "search box" for the new group (searchable when no subgroups)
    var searchInput = this.getSearchInput();
    searchInput.writeAttribute('class', parent ? parent.name : foodGroup.name); // sets color to match tab
    this.setSearchWatermark(isParent ? 'select category first' : 'search ' + foodGroup.title);
    [searchInput, this.getSearchGo()].each(function(e){e[isParent ? 'disable' : 'enable'].call(e)});
    searchInput[(isParent ? 'add' : 'remove') + 'ClassName'].call(searchInput, 'disabled');

    // Finally, remember which group is currently active
    this.activeFoodGroup = foodGroup;
  },

  createSwitchGroupButton: function(foodGroup) {
    return new Element('a', {'href': '#', 'class': foodGroup.name + "-back-button"}).observe(
      'click', function(){
        PickChow.switchFoodGroup(foodGroup);
        return false;
      }.bind(this)
    )
  },

  setSearchWatermark: function(watermark) {
    this.getSearchInput().addClassName("watermark").value = watermark;
  },

  imDone: function() {
    if (!this.Meal.validate(this.getMeal())) {return;}
    PickChow.playSound('click_done');
    PickChow.showSaveAndShare('step-1');
  },

  cancelImDone: function() {
    PickChow.hideSaveAndShare();
  },

  updateMeters: function(foods, multiplier) {
    this.meterManager.updateMeters(foods, multiplier);
  },

  showSaveAndShare: function(step, options) {
    jQuery.fn.colorbox(jQuery.extend({},
      ZisBoomBah.colorboxOptions,
      {inline: true, href:'#save-and-share-' + step},
      options));
  },
  showSaveAndShareLogin: function() {
    PickChow.showSaveAndShare('login', {height:'556px'});
  },
  showSaveAndShareStep2: function() {
    PickChow.showSaveAndShare('step-2', {onClosed: PickChow.showSaveAndShareStep3});
  },
  showSaveAndShareStep3: function() {
    PickChow.showSaveAndShare('step-3', {height:'556px'});
  },
  hideSaveAndShare: function() {
    jQuery.fn.colorbox.close();
  },

	// Bring the animation forward, play it, and hide it again.
  // Allow passing a callback to do something after the animation is done.
	playAnimation: function(name, funcAfterPlay) {
    return true ; // disable animation because it's broken in Safari and it's getting in the way of testing

		// Get movie object
		var movie = $(name + '-cheer');

    var playMovieFunc = function(){
      $('pickchow').setOpacity(0.5); // make plate area opaque so animation is more visible
      $('skip-animation').show();
      this.show(); // Bring it forward
      this.Play();
    }.bind(movie);

    var hideMovieFunc = function(){
      this.hide();
      $('pickchow').setOpacity(1); // make plate area opaque so animation is more visible
      $('skip-animation').hide();
      if (funcAfterPlay) {
        funcAfterPlay();
      }
    }.bind(movie);

    playMovieFunc();

		// When the animation is done, hide it.
		var	intervalId = window.setInterval(function(){
      try {
        if (!movie.IsPlaying()) {
          hideMovieFunc();
          window.clearInterval(intervalId);
        }
      } catch (e) {
        window.clearInterval(intervalId);
      }
		}.bind(this), 100);
	},

  stopAnimation: function() {
    $$('#animations .animation').each(function(e){
      // Get movie object
      var movie = e.down('object');
      try {
        if (movie.IsPlaying()) {
          movie.Rewind();
        }
      } catch (e) {
        // prevent error when calling IsPlaying() on movie that's not playing
      };
    });
  },

  // Get the sound to play for the given event.
  // For 'drop', the food must be passed as an option.
  // For others, no options are needed.
  getDropFoodSound: function(food) {
    var fg = food.getMainFoodGroup();
    if (food.is_liquid) {
      return 'drop_food_liquid'
    } else if ($A(['fruit', 'vegetables']).include(fg.name)) {
      return 'drop_food_fruit_or_veggie';
    } else {
      return 'drop_food_other';
    }
  },
  
  // Show the food's "more" info in a few seconds unless the user clicks the mouse
  initAutoMoreInfo: function(food) {
    var foo_show = function() {
      this.Food.showMoreInfo(food);
      if (this.user) {
        // Update the user's account so this doesn't show up again automatically
        new Ajax.Request(this.user.update_account_url, {
          parameters: {'user[saw_more_box]': 1, '_method': 'put'}
        });
      }
    }.bind(this);
    this.moreInfoTimeouts.push(window.setTimeout(foo_show, 3000));
  },

  // NOTE: using a single div due to IE problems with multiple players
  getSoundDiv: function() {
//  getSoundDiv: function(name) {
//    var id = 'pickchow-sound-' + name;
    var id = 'pickchow-sound';
    var e = $(id);
    if (!e) {
      e = new Element('div', {'id': id});
      $$('body')[0].insert(e);
    }
    return e;
  },

  // Play the file at the specified url (local or remote)
  playSound: function(name) {
//    var div = jQuery(this.getSoundDiv(name));
    var div = jQuery(this.getSoundDiv());
    div.jPlayer('setFile', this.sounds[name]);
    div.jPlayer('play');
  },

  // If a sound is specified, only stop if the specified sound is playing
  stopSound: function(name) {
//    jQuery(this.getSoundDiv(name)).jPlayer('stop');
    var div = jQuery(this.getSoundDiv());
    if (div.jPlayer('getData', 'diag.src') == this.sounds[name]) {
      div.jPlayer('stop');
    }
  },

  //
  // A food DOM element.
  // Used as a food choice in the chowbox and as an item on the plate.
  Food: {
    // Create a food element (options for 'more' handle and name)
    create: function(food, id, target, photo_style, options) {
      if (!options) options = {};
      var foodItem = new Element('div', {id: id, title: food.name}).addClassName('food');
      foodItem.style.position = options['position'] || 'relative';

      // create handle, if specified
      if (options['handle']) {
        var handle = this.createMoreInfoLink(food);
        foodItem.insert(handle);
        foodItem.observe('mouseover', function(){this.show();}.bind(handle));
        foodItem.observe('mouseout', function(){this.hide();}.bind(handle));
      }

      // Add photo
      var food_and_name = new Element('div', {'class': 'food-and-name'});
      food_and_name.insert(new Element('img', {src: food.photo_urls[photo_style], alt: food.name}));

      // Add name, if specified
      if (options['name']) {
        food_and_name.insert(new Element('div', {'class': 'food-name'}).update(food.name));
      }

      foodItem.insert(food_and_name);
      target.insert(foodItem);

      return foodItem;
    },

    createForChowbox: function(food, target) {
      var element = this.create(food, ['food', food.id].join('-'), target, 'default', {handle: true, name: true});

      // Make draggable that is destroyed if not dragged onto the plate
      element.down('.food-and-name').observe('mousedown', function(event){
        // Remove all existing drag clones, just in case of some browser error
        $$('.food.clone').each(function(e){e.remove();});

        var draggable = this.createForDragging(food, event.pointer());

        // Start the drag and register with Draggables
        draggable.initDrag(event);
        Draggables.updateDrag(event);
      }.bind(this));

      return element;
    },

    createForPlate: function(food, position) {
      var plate = PickChow.getPlate();
      var element = this.create(food, PickChow.Plate.getFoodId(PickChow.getPlate(), food),
        plate, 'more', {position: 'absolute', handle:true});

      element.observe('click', function(event) {
          // Prevent event from bubbling to plate (see http://www.quirksmode.org/js/events_order.html)
          event.cancelBubble = true;
          if (event.stopPropagation) {event.stopPropagation()};
        }.bind(this)
      );

      var plate_size = food.plate_size;
      if (plate_size && plate_size != 1) {
        var img = element.down('img');

        var func = function() {
          img.style.width = (img.getWidth() * plate_size) + 'px';
//        img.style.height =  (dims.height * plate_size) + 'px'; //  this causes problems for some reason, and we don't need it anyway since the height is auto
        }.bind(this);

        img.complete ? func() : img.observe('load', func); // must wait until image has loaded

        if (plate_size > 1.25) {
          img.stopObserving('load', func); // we don't want to resize again
          img.src = img.src.replace(/\/more_/, '/original_'); // use original image for higher resolution
          element.style.zIndex = -1; // show beneath others
        } else if (plate_size < 1) {
          element.style.zIndex = 1; // show above others
        }
      }

      // Set position (AFTER insert)
      var eo = PICKCHOW.centered_offset(element);
      var po = plate.cumulativeOffset();
      element.style.left = (position.x - po[0] - eo.x)+ 'px';
      element.style.top = (position.y - po[1] - eo.y) + 'px';

      // Highlight plate zones on click
      element.observe('mousedown', PickChow.Plate.highlightZones.curry(plate, true, food));
      element.observe('mouseup', PickChow.Plate.highlightZones.curry(plate, false, food));

      // Make draggable that is destroyed if dragged off the plate
      new Draggable(element, {
        onEnd: function(draggable, event) {
          // if the element is dragged off the plate, remove it and update the meters
          if (!PICKCHOW.inside_element(event.pointer(), plate)) {
            var food = PickChow.Plate.getFood(draggable.element);
            draggable.element.remove();
            draggable.destroy();
            PickChow.updateMeters([food], -1);
            var stars = PickChow.meterManager.currentStars();
            // If meal is not 5 stars and removed food is not dessert, remove all desserts.
            if (stars < 5 && food.getMainFoodGroup().name != 'dessert') {
              var dessert_els = PickChow.Plate.getDessertElements(plate);
              if (dessert_els.any()) {
                jAlert("Sorry! We had to remove your dessert because your meal no longer has 5 stars.");
                dessert_els.each(function(el){el.remove();});
                PickChow.updateMeters(dessert_els.collect(function(el){return PickChow.Plate.getFood(el)}), -1);
              }
            }
          }
        }.bind(this)
      });

      return element;
    },

    createForDragging: function(food, position) {
      PickChow.clearMoreTimeouts(); // I think the draggable creation stops the click handler at the higher level
      var plate = PickChow.getPlate();
      var element = this.create(food, ['clone', 'food', food.id].join('-'),
        $('pickchow'), 'default', {position: 'absolute'}).addClassName('clone');

      // Set position (AFTER insert)
      var eo = PICKCHOW.centered_offset(element);
      element.style.left = (position.x - eo.x) + 'px';
      element.style.top = (position.y - eo.y) + 'px';

      // Make draggable that is destroyed if not dropped on the plate
      var d = new Draggable(element, {
        onStart: PickChow.Plate.highlightZones.curry(plate, true, food),
        onEnd: function(draggable, event) {
          PickChow.Plate.highlightZones(plate, false, food);
          // If dropped on plate, create a clone for the plate and update the meters and stars.
          // Also, the first time a food is dragged, show the more-box for a few seconds.
          if (PICKCHOW.inside_element(event.pointer(), plate) &&
              PickChow.Plate.allowFood(plate, food)) {
            PickChow.playSound(PickChow.getDropFoodSound(food));
            var plate_food = this.createForPlate(food, event.pointer());
            PickChow.Plate.moveToZone(plate_food);
            var starsBefore = PickChow.meterManager.currentStars();
            PickChow.updateMeters([food], 1);

            // Play animation and/or auto-show "more" box
            var stars = PickChow.meterManager.currentStars();
            var animation = PickChow.getMeal().foods.size() == 1 ? 'zis' : stars == 3 ? 'boom' : stars == 4 ? 'bah' : stars == 5 ? 'final' : null;
            var showMoreBoxFunc = function(){
              if (!PickChow.showedMoreBox) {
                PickChow.initAutoMoreInfo(food);
              }
            }.bind(this);

            if (animation && starsBefore < stars) { // only show if stars increased
              PickChow.playAnimation(animation, showMoreBoxFunc);
            } else {
              showMoreBoxFunc();
            }

          }
          draggable.element.remove();
          draggable.destroy();
        }.bind(this)
      });


      return d;
    },

    // Create element used to show "more info" for a food (hidden by default)
    createMoreInfoLink: function(food) {
      return new Element('a', {'class':'handle',
          'style': 'display: none', 'title': 'Fun facts about ' + food.name + '!'}
        ).observe('click', function(event) {
          PickChow.Food.showMoreInfo(food);
          // Prevent click from initiating food drag
          event.cancelBubble = true;
          if (event.stopPropagation) {event.stopPropagation()};
        }.bind(this));
    },

    // Where should it go on the plate?
    getZone: function(food) {
      return food.getMainFoodGroup().name;
    },

    showMoreInfo: function(food) {
      jQuery.fn.colorbox(jQuery.extend({href: "/foods/" + food.id + ".html"}, ZisBoomBah.colorboxOptions));
      PickChow.showedMoreBox = true;
    }
  },

  // The collection of PickChow.Food DOM elements (i.e. foods on the plate)
  Meal: {
    paths: {}, // paths to menu, submit motw
      maxFoods: null, // should be set from Meal::MAX_FOODS on page init
    form: function() {
      return $('meal_form');
    },

    // Can the meal be saved?
    validate: function() {
      var meal = PickChow.getMeal();

      if (meal.empty()) {
        jAlert("There aren't any foods in your meal. Please add some foods first.");
        return false;
      }

      if (!meal.isComplete()) {
        jAlert('Sorry, you cannot save your meal until it is complete.\n\n' +
          'A complete meal consists of at least ' + PICKCHOW.completeMealRule + '.');
        return false;
      }

      return true;
    },

    // Prepare form with meal data and submit
    // Accepts inputs, not input values
    save: function(intent, name_input, msg_input, default_msg) {
      var form = PickChow.Meal.form();
      var name = name_input.value;
      var msg = msg_input.value;

      // Require meal name
      if ($(name_input).hasClassName('watermark')) { // wrap in $() for IE7
        jAlert("Please enter a name for your meal first.");
        return false;
      } else {
        form['meal[name]'].value = name;
        if (intent == 'share') {
          form['meal[share_message]'].value = msg != '' ? msg : default_msg;
        }
      }

      // Prepare meal form data

      // Rating
      form['meal[rating]'].value = PickChow.meterManager.currentStars();
      // Food ids
      form['meal[food_ids]'].value = PickChow.getMeal().foods.collect(function(food){return food.id});
      // Save intent
      form.intent.value = intent;

      // Submit form
      return form.commit.click(); // not a simple submit because of remote form tag?
    },

    saved: function(id, shared, paths) {
      PickChow.Meal.form()['meal[id]'].value = id;
      $('share_meal_form').action = paths.share;
      PickChow.Meal.paths = paths;
      shared ? PickChow.showSaveAndShareStep2() : PickChow.showSaveAndShareStep3();
    },

    share: function(form) {
      var msg_input = form['message'];
      var msg = msg_input.value;
      var rec_input = form['recipient_ids[]']
      var recipient_ids = rec_input.length
        ? $A(rec_input).collect(function(checkbox){
            return checkbox.checked ? checkbox.value : null
          }).compact()
        : rec_input.checked ? [rec_input.value] : [];

      if (recipient_ids.length == 0) {
        jAlert('Please select at least one friend to share your meal with or click "' +
          $('share_meal_form').down('a.no_thanks').innerHTML +  '".');
      } else if ($(msg_input).hasClassName('watermark') || msg == '') { // wrap in $() for IE7
        jAlert('Please enter a message to your friends.');
      } else {
        new Ajax.Request(form.action, {
          parameters: $H(form.serialize(true)).merge({id: PickChow.Meal.form()['meal[id]'].value}),
          onLoading: function(){jQuery('#save-and-share-step-2 .loading-indicator').show();},
          onComplete: function(){jQuery('#save-and-share-step-2 .loading-indicator').hide();}
        });
      }

      return false;
    },

    shared: function(error) {
      if (error == null) {
        PickChow.showSaveAndShareStep3();
      } else {
        jAlert('There was an error. We have been notified and will fix it as soon as possible.');
      }
    },

    goToMenu: function() {
      window.location.href = PickChow.Meal.paths.menu;
    },
    submitAsMotw: function() {
      window.location.href = PickChow.Meal.paths.submit_motw;
    }
  },

  Plate: {
    init: function(plate) {
      plate.observe('click', this.handleClick.bind(plate));
    },

    handleClick: function(event) {
      var pointX = event.pointerX() - this.offsetLeft;
      var pointY = event.pointerY() - this.offsetTop;
      var point = [pointX, pointY];

      //console.log(point);

      $H(PICKCHOW.sections).each(function(section) {
        var inside = PICKCHOW.inside_triangle(point, section.value[0], section.value[1], section.value[2]);
        if (inside) {
          PickChow.switchFoodGroup(section.key)
          return;
        }
      });
    },

    clear: function(plate) {
      PickChow.playSound('clear_plate');
      var elements = this.getFoods(plate);
      var foods = elements.collect(function(element){return this.getFood(element);}.bind(this));
      PickChow.updateMeters(foods, -1);
      elements.each(function(e){e.remove();});
    },

    allowFood: function(plate, food) {
      var mainGroup = food.getMainFoodGroup()
      var meal = PickChow.getMeal();

      if ((meal.foods.size() + 1) > PickChow.Meal.maxFoods) {
        jAlert('Sorry! You cannot have more than ' + PickChow.Meal.maxFoods + ' foods on your plate.');
        return false;
      }

      if (mainGroup.name == 'dessert' && PickChow.meterManager.currentStars() != 5) {
        // Desserts have special logic which requires that the "meal" have 5 stars before adding it.
        var more = this.hasDessert(plate) ? 'more ' : ''
        jAlert("Sorry! Your meal must have 5 stars before you can add " + more + "dessert.");
        return false;
      }
      return true;
    },


    // Since we can add the same food more than once, we need to give it a unique id
    getFoodId: function(plate, food) {
      return ['plateFood', food.id, this.getFoods(plate).size() + 1].join('-');
    },

    moveToZone: function(element) {
      var zone = PickChow.Food.getZone(this.getFood(element));
      if (zone == 'starchy_vegetables') { zone = 'grains'; } // starchy vegs. and grains share a zone

      // Addons and Quick Picks can go anywhere on the plate.
      if ($A(['addons', 'quickpicks']).include(zone)) {return true;}

      // NOTE: make you sure .clone() from sections or else you will be
      // passing by-reference and will be modifying the original sections each time
      // (which will very quickly start to get out of whack).
      var target;
      if (zone == 'dessert') {
        // dessert only goes in one spot and is not randomized
        target = PICKCHOW.sections[zone].clone();
      } else {
        target = PICKCHOW.centroid(PICKCHOW.sections[zone][0], PICKCHOW.sections[zone][1],
          PICKCHOW.sections[zone][2]).clone();

        // Now randomize the position a bit (+/- 50 pixels)
        target[0] += Math.floor(Math.random() * -99) + 50;
        target[1] += Math.floor(Math.random() * -99) + 50;
      }

      // Adjust target by half of the width/height of the plate food
      target[0] -= element.getWidth() / 2;
      target[1] -= element.getHeight() / 2;

      new Effect.Move(element, {
        x: target[0],
        y: target[1],
        mode: 'absolute'
      });

      return true;
    },

    // Return all the foods on the plate
    getFoods: function(plate) {
      return plate.select('.' + PickChow.foodClass);
    },

    getFood: function(element) {
      return FoodDb.foods.get(element.id.split('-')[1])
    },

    highlightZones: function(plate, enable, food) {
      if (enable) {
        food.getMainFoodGroups().each(function(fg){
          if (fg.name == 'starchy_vegetables'){fg = FoodDb.foodGroupsByName.get('grains');}
          var hl = plate.select(['#', fg.name, '-highlight'].join(''))[0];
          if (hl) {
            hl.fade({duration: 0.25, from: 0, to: 0.33, beforeStart: function(){
              hl.setOpacity(0);
              hl.show();
            }.bind(this)});
          }
        });
      } else {
        plate.select('.plate-highlight').each(function(e){
          if (e.visible) {
            e.fade({duration: 0.25, from: e.getOpacity(), to: 0, afterFinish: function(){
              e.hide();
            }.bind(this)});
          }
        });
      }
    },

    getDessertElements: function(plate) {
      return this.getFoods(plate).select(function(el){return this.getFood(el).getMainFoodGroup().name == 'dessert'}.bind(this));
    },

    hasDessert: function(plate) {
      return this.getDessertElements(plate).any();
    }
  }
};

MeterRenderer = Class.create({
  initialize: function(data) {
    this.isSmall = $$('#' + data.name + ".small").first();
    if (Prototype.Browser.IE) {
      this.needleImg = new Image();
      if (this.isSmall)
        this.needleImg.src = "/images/pickchow/additup_needle_sm.png";
      else
        this.needleImg.src = "/images/pickchow/additup_needle.png";
    } else {
      if (this.isSmall)
        this.needleImg = $('needle-img-small')
      else
        this.needleImg = $('needle-img')
    }

    this.data = data;
    this.canvas = $$('#' + this.data.name + ' .needle-canvas').first();
    this.position = 0;    // this holds the angle rotated counter-clockwise by 90 degrees (e.g. -90 becomes 0)
    this.running = false;
    this.queue = Array();

    this.setNeedlePosition(0);
  },

  draw: function() {
    var angleValue;
    var v = this.data.value; // for brevity
    var th = this.data.thresholds; // for brevity
    var nudge = 10; // keep the needle at least 5 degrees from a boundary
    var angles = [45, 90, 135, 180] // the four "boundaries"

    if (this.data.meterType == 'tri') {
      // TODO: 
      if (v < th[0]) { // below accepted min
        angleValue = (v / th[0]) * angles[0];
      } else if ((v >= th[0]) && (v < th[1])) { // between acceptable min/max
        angleValue = ((angles[1] / (th[1] - th[0])) * (v - th[0])) + angles[0];
      } else if ((v >= th[1]) && (v < th[2])) { // more than accepted max but less than meter max
        angleValue = ((angles[0] / (th[2] - th[1])) * (v - th[1])) + angles[2];
      } else { // more than meter max
        angleValue = angles[3];
      }

      // needle "nudge"
      var fromTh1 = angleValue-angles[0];
      var fromTh2 = angles[2]-angleValue;
      if (Math.abs(fromTh1) < nudge) {
        var dir = fromTh1 > 0 ? 1 : -1;
        angleValue = angles[0]+(dir * nudge)
        if (dir > 0)
          angleValue += 5; // increase 5 more degrees because it looks better
      } else if (Math.abs(fromTh2) > 0 && fromTh2 < nudge) {
        var dir = fromTh2 > 0 ? -1 : 1;
        angleValue = angles[2]-((fromTh2 > 0 ? 1 : -1) * nudge)
        if (dir < 0)
          angleValue -= 5; // decrease 5 more degrees because it looks better
      }

    } else { // "bi" meter
      if (v < th[0]) {
        angleValue = (v / th[0]) * angles[1];
      } else if ((v >= th[0]) && (v < th[1])) {
        angleValue = ((90 / (th[1] - th[0])) * (v - th[0])) + 90;
      } else {
        angleValue = 180;
      }

      // needle "nudge"
      var fromTh = angleValue-angles[1];
      if (Math.abs(fromTh) < nudge) {
        angleValue = angles[1]+((fromTh > 0 ? 1 : -1) * nudge)
      }
    }

    this.rotateNeedle(Math.floor(angleValue));
  },

  setNeedlePosition: function(position) {
    var context = this.canvas.getContext('2d');
    var newAngle = position - 90;
    var rad = (newAngle % 360) * Math.PI / 180;

    // Clear it out
    context.restore();context.clearRect(0, 0, this.canvas.width, this.canvas.height);context.save();

    // Put the origin at somewhere needle the center (it's actually a
    // little lower)
    if (this.isSmall)
      context.translate(this.canvas.width / 2, this.canvas.height / 2 + 6);
    else
      context.translate(this.canvas.width / 2, this.canvas.height / 2 + 10);

    context.save();
    context.rotate(rad);
    if (this.isSmall)
      // There is a 5x4 offset from the needle's fixed center to the corner of the image
      context.drawImage(this.needleImg, -(this.needleImg.width) + 7, -(this.needleImg.height) + 8);
    else
      // There is a 14x13 offset from the needle's fixed center to the corner of the image
      context.drawImage(this.needleImg, -(this.needleImg.width) + 14, -(this.needleImg.height) + 13);
    context.restore();

    this.position = position;
  },

  rotateNeedle: function(position) {
    this.queue.push(position);
    if (!this.running)
      this.processQueue();
  },

  processQueue: function() {
    if (this.queue.length == 0)
      return;

    this.running = true;
    this.processQueueEntry(this.queue[0]);
  },

  processQueueEntry: function(targetPosition){
    var currentPosition = this.position;

    if (targetPosition == currentPosition) {
      // We're done with this job. Process the rest of the queue if necessary.
      this.queue.splice(0, 1);
      this.running = false;
      clearTimeout(this.timer);
      this.timer = null;
      this.processQueue();
      return;
    }
    // Determine if we need to rotate forward or backward, then do it!
    var newPosition = currentPosition + (currentPosition < targetPosition ? 1 : -1);
    this.setNeedlePosition(newPosition);

    if (Prototype.Browser.IE) {
      this.processQueueEntry(targetPosition);
    } else {
      this.timer = setTimeout(function(){this.processQueueEntry(targetPosition);}.bind(this), 10);
    }
  }
});

Meter = Class.create({
  initialize: function(name, opts) {
    this.name = name;
    this.renderer = new MeterRenderer(this);

    // The current accumulated value
    this.value = 0;

    // the amount the value is outside of the acceptable range
    this.rangeOffsetPercent = 1;

    var options = Object.extend({
      thresholds: [],
      reversed: false
    }, opts);

    this.reversed = options.reversed; // MUST BE SET BEFORE THRESHOLD!
    this.setThresholds(options.thresholds);
  },
  change: function(delta) {
    this.value += delta
    this.updateRangeOffsetPercent();
    this.renderer.draw();
  },
  setThresholds: function(thresholds) {
    this.thresholds = thresholds;
    switch(thresholds.length) {
    case 2:
      this.meterType = 'bi';
      break;
    case 3:
      this.meterType = 'tri'
      break;
    }
    this.calculateRangeSize();
  },
  calculateRangeSize: function() {
    if (this.meterType == 'tri') {
      this.rangeSize = this.thresholds[1] - this.thresholds[0];
    } else {
      if(this.reversed) {
        this.rangeSize = this.thresholds[0] - 0;
      } else {
        this.rangeSize = this.thresholds[1] - this.thresholds[0];
      }
    }
  },
  updateRangeOffsetPercent: function() {
    var amount = 0;
    if (this.meterType == 'tri') {
      if (this.value < this.thresholds[0]) {
        amount = this.value - this.thresholds[0];
      } else if ((this.value >= this.thresholds[0]) && (this.value < this.thresholds[1])) {
        amount = 0;
      } else {
        amount = this.value - this.thresholds[1];
      }
    } else {
      if (this.value >= 0 && this.value < this.thresholds[0]) {
        if(this.reversed) {
          amount = 0
        } else {
          amount = this.value - this.thresholds[0];
        }
      } else {
        if(this.reversed) {
          amount = this.value - this.thresholds[0];
        } else {
          amount = 0;
        }
      }
    }
    this.rangeOffsetPercent = amount / this.rangeSize * 100;
  },
  isEmpty: function() {
    return ((Math.round(this.value * 100) / 100)== 0);
  }
});

MeterManager = Class.create({
  measures: $w('protein carbs fat fiber sugar saturated_fat sodium'),
  initialize: function(profile, meter_thresholds) {
    this.profile = profile;
    this.meters = new Hash();
    this.stars =  $$('#stars .star');
    this.foods = $A(); // current foods (FoodDb.Food)

    this.measures.each(function(x) { // omit 'three_s' (not used right now)
      this.addMeter(x, {
        thresholds: meter_thresholds[x],
        reversed: $w('sugar saturated_fat sodium').include(x) // omit 'three_s' (not used right now)
      });
    }.bind(this));
  },

  addMeter: function(name, options) {
    this.meters.set(name, new Meter(name, options));
  },

  updateMeters: function(foods, multiplier) {
    $A(foods).each(function(food){
      this.meters.keys().each(function(key){
        var meter = this.meters.get(key);
        if (food[key] != null) {
          var value = multiplier * food[key];
          meter.change(value);
        }
      }.bind(this));

      // We need to know which foods we are rating at any given time
      if (multiplier > 0) {
        this.foods.push(food);
      } else {
        this.foods.splice(this.foods.indexOf(food), 1);
      }
    }.bind(this));

    // Highlight (or unhighlight) the meter gauge text
    // Collect the status of each meter
    var status = $H();
    $w('protein carbs fat fiber sugar saturated_fat sodium').each(function(m) {
      status.set(m, this.within.call(this, 0, $A([m])));
    }.bind(this));

    // For the non-threeS meters, add/remove class "green" depending on status
    $w('protein carbs fat fiber').each(function(m){
      var el = $$('#' + m + '.meter')[0];
      var func = (status.get(m) ? 'add' : 'remove') + 'ClassName';
      eval('Element.' + func + '(el, \'green\')');
    });

    // For three-S meters, build a 3 bit binary string to determine CSS sprite bg position
    var bits = '';
    $w('sugar saturated_fat sodium').each(function(m){ // NOTE: the order matters!
      bits += status.get(m) ? '1' : '0';
    });
    // Remove any class name matching /green\d\d\d/ and add updated one
    var threeS_text = $('threeS-text');
    threeS_text.classNames().each(function(c){
      if (/^green\d{3}$/.test(c)) {threeS_text.removeClassName(c)};
    });
    threeS_text.addClassName('green' + bits);

    this.updateStars();
  },

  updateStars: function(stars) {
    this.clearStars();
    this.setStars(stars || this.currentStars());
  },

  // Do not show any stars unless it's a complete meal
  currentStars: function() {
    // We are ignoring the "big" 3S meter for now (and not showing it)
    var all = this.meters.keys().reject(function(e){return e == 'three_s'});
//    var threess = ['sugar', 'saturated_fat', 'sodium'];
    var meal = new FoodDb.Meal(this.foods);
    var complete = meal.isComplete();

    // If meal is empty...
    if(this.allEmpty())
      return 0;
    // For a complete meal...
    if(complete) {
      // ...if all meters are OK
      if (this.within(0, all)) {return 5;}
      // ...if all meters are within 15%
      if(this.within(15, all)) {return 4;}
      // ...if all meters are within 25%
      if(this.within(25, all)) {return 3;}
      // ... meters can be anything
      return 2;
    }
    // For an incomplete meal...
    return 1;
  },

  allEmpty: function() {
    var result = this.meters.values().all(function(meter) {
      return meter.isEmpty();
    });
    return result;
  },

  clearStars: function() {
    this.stars.each(function(star) {
      star.removeClassName('on');
      star.addClassName('off');
    });
  },

  setStars: function(number) {
    var matching = this.stars.slice(0, number);
    matching.each(function(star) {
      star.removeClassName('off');
      star.addClassName('on');
    });
  },

  within: function(range_or_amount, names) {
    if (Object.isNumber(range_or_amount)) {
      range_or_amount = $R(-range_or_amount, range_or_amount);
    }
    var result = $A(names).all(function(name) {
      return range_or_amount.include(this.meters.get(name).rangeOffsetPercent);
    }.bind(this));
    return result;
  }
});
