<?php
#########################################################################
# Class definitions for all classes used by alc_calc.php, v 0.41
# Note that these classes are not fully encapsulated
#  In particular, they tend to make use of global variables that are
#  supposed to be initialized by alc_calc.php
#
# Note: public/private/protected are NOT being used, because they
#  are PHP5-specific features
# Variables however have all been implemented as if they were private
#  variables unless specified otherwise in comments
#  (although since this hasn't been tested, there are possibly some
#   glitches)
#
# Written by Nephele (nephele@skyhighway.com), last modified 05/03/2008
########################################################################

define('VERSION', 0.42);
# These parameters control how the program runs; tweaking them may improve
# program runtime, but probably at the expense of providing good information
# about what potions are possible
# MAXPOTION is an approximate cap on the number of potions to create
# If the projected number of potions exceeds MAXPOTION, the code will begin
#  cautiously skipping less useful potions
# If the actual number of potions reaches MAXPOTION, more aggressive measures
#  will be taken to reduce the number of additional potions
# If the actual number of potions reaches MAXPOTION*1.5, no more potions
#  will be saved
define('MAXPOTION', 500);
# FREQDIFF is the difference in frequency values at which to start discarding
#  potions
# Decreasing FREQDIFF will reduce program runtime, but means potions using
#  rare ingredients will hardly ever be shown
define('FREQDIFF', 10);

########################################################################
# Ingredient Class
########################################################################
class Ingredient {
# non-arrays
  var $name, $id, $cost, $weight, $freq, $src;
  var $avail;
  var $score_pro, $score_con;
# arrays
  var $effs;

# Note that this initialization is currently all being done ahead of time
# So this class just needs to ingest the preassigned information, without
# doing any real processing
  function Ingredient($name=NULL, $id=NULL, $cost=NULL, $weight=NULL, $freq=NULL, $src=NULL, $effs=NULL) {

    $this->name = $name;
    $this->id = $id;
    $this->cost = $cost;
    $this->weight = $weight;
    $this->freq = $freq;
    $this->src = $src;
    $this->effs = $effs;
    $this->score_pro = $this->score_con = NULL;
  }  

  function is_score_set() {
    return isset($this->score_pro);
  }
  function is_avail() {
    return $this->avail;
  }
  function get_freq() {
    global $custom_freq;
    return $custom_freq[$this->id];
  }
  function get_name() {
    return $this->name;
  }
  function get_weight() {
    return $this->weight;
  }
  function get_cost() {
    return $this->cost;
  }
  function get_score_pro() {
    return $this->score_pro;
  }
  function get_score_con() {
    return $this->score_con;
  }

  function get_linkname() {
    $outstr = "";
    $outstr .= '<a href="http://www.uesp.net/wiki/';
    if ($this->src == "SI") {
      $outstr .= "Shivering";
    }
    else {
      $outstr .= "Oblivion";
    }
    $outstr .= ":";
    $linkname = preg_replace('/\s+/', '_', $this->name);
    $linkname = preg_replace('/_\(.*\)$/', '', $linkname);
    $outstr .= $linkname;
    $outstr .= '">';
    $outstr .= $this->name;
    $outstr .= "</a>";
    if ($this->src) {
      $outstr .= '<sup><a href="http://www.uesp.net/wiki/';
      if ($this->src == "SI") {
	$outstr .= 'Shivering:Shivering_Isles">SI';
      }
      else {
	$outstr .= 'Oblivion:Vile_Lair">mod';
      }
      $outstr .= '</a></sup>';
    }
    return $outstr;
  }

  function use_ing($e) {
    global $alchemy;
    if ($this->effs[$e] > $alchemy ||
        !$this->avail)
      return FALSE;
    else 
      return TRUE;
  }

  function add_effs(&$curr_ptr) {
    global $alchemy;
    foreach ($this->effs as $e => $alc) {
      if ($alc<=$alchemy) {
        increment_array($curr_ptr,1,$e);
      }
    }
  }

  function master_eff() {
    global $alchemy;
    if ($alchemy<100)
      return -1;
    foreach ($this->effs as $e => $alc) {
      if ($alc==0)
        return $e;
    }
    return -1;
  }

  function reset_custom(&$cust_use, &$cust_freq) {
    if ($this->freq<0) {
      $cust_use = 0;
      $cust_freq = 0;
    }
    else {
      $cust_use = 1;
      $cust_freq = $this->freq;
    }
  }

  function get_name_and_title() {
    global $alchemy, $alleffs;
    $outstr = '<span title="';
    $outstr .= 'Wt='.$this->weight;
    $outstr .= ', Cost='.$this->cost;
    $outstr .= ', ';
    $first = 1;
    foreach ($this->effs as $et => $v) {
      if ($v > $alchemy)
        continue;
      if (!$first)
        $outstr .= '+';
      $first = 0;
      $outstr .= $alleffs[$et]->get_abbrev();
    }
    $outstr .= '">';
#    $outstr .= $this->name;
    $outstr .= $this->get_linkname();
    $outstr .= '</span>';
    return $outstr;
  }

  function set_avail() {
# could pass these in instead of globalizing...
    global $do_SI, $do_quest, $do_rare, $custom_use, $custom_freq;
# custom_use=2 overrides all other settings
    if ($custom_use[$this->id]==2) {
      $this->avail = TRUE;
# signal that this is a required ingredient
      return TRUE;
    }
    if (!$custom_use[$this->id] ||
        (!$do_SI && !is_null($this->src)) ||
        (!$do_quest && $custom_freq[$this->id]==0) ||
        (!$do_rare && $custom_freq[$this->id]==1)) {
      $this->avail = FALSE;
    }
    else {
      $this->avail = TRUE;
    }
    return FALSE;
  }

  function set_score_orig() {
    global $finde, $alleffs, $find_poison;
    global $custom_use, $custom_freq;

# Calculate a score for this ingredients based upon how useful its
# list of effects is
# Every ingredient's starting score is basically 100
#  (i.e., it must match one requested effect)
    $this->score_pro = $this->score_con = 0;
    foreach ($this->effs as $et => $v2) {
# Max score for ingredients that provide the effects I'm after
      if (isset($finde[$et]))
        $this->score_pro += 100;

# Additional bonus if this effect is in the same group as an effect I'm after
      foreach ($finde as $ft => $v3)
	$this->score_pro += 10*$alleffs[$et]->get_group($ft);

# Pro/con addition depending on whether the effect is the same sign as those I'm after
      if ($alleffs[$et]->is_poison()==$find_poison)
        $this->score_pro++;
      else
        $this->score_con += 10*max(1, $alleffs[$et]->get_poison());

# Add frequency as a positive factor (0 to 5)
      $this->score_pro += $custom_freq[$this->id];

# Add weight as a negative factor (weighted less strongly than
# side-effects)
      $this->score_con += $this->weight;
    }
  }

  function set_score_mod() {
    global $alchemy;
    global $my_eff_to_get, $extraeffs_lim;
    global $custom_freq, $custom_use;

#    $this->effid = "";
    $this->score_pro = 0;
    foreach ($my_eff_to_get as $eff) {
      if (!isset($this->effs[$eff]) ||
          $this->effs[$eff] > $alchemy)
        continue;
#      $this->effid .= ".$eff";
      $this->score_pro += 100;
    }
    foreach ($extraeffs_lim as $eff => $v) {
      if (!isset($this->effs[$eff]) ||
          $this->effs[$eff] > $alchemy)
        continue;
#      $this->effid .= ".$eff";
      $this->score_pro += 10;
    }
# for now I'm not using these effids... but keep the code in place
# in case a simple sort doesn't work
#    $this->effid = substr($this->effid, 10);
    $this->score_pro += $custom_freq[$this->id];
  }

  function print_custom($opt) {
    global $alleffs;
    global $alchemy;
    global $finde, $find_poison;
    global $badinput;
    global $custom_freq, $custom_use;
    global $do_SI, $do_quest, $do_rare;

    $outstr = "";

    $outstr .= "<tr>\n";
    $outstr .= "<td";
    if ((isset($badinput['custom_use']) && $custom_use[$this->id]==2) ||
        (isset($badinput['custom_freq']) && isset($badinput['custom_freq'][$this->id])))
      $outstr .= " class=\"error\"";
    $outstr .= " title=\"";
    if ($custom_use[$this->id]==2) {
      $outstr .= "Will be used in ALL potions";
    }
    else if ($this->is_avail()) {
      if ($opt) 
        $outstr .= "Currently available for use in potions";
      else
        $outstr .= "Could be used in potions, but does not currently provide any requested effects";
    }
    else {
      $outstr .= "Will NOT be used in any potions because ";
      if (!$custom_use[$this->id])
        $outstr .= "N (never use) has been selected";
      else if (!$do_SI && !is_null($this->src)) {
	if ($this->src == "SI") {
	  $outstr .= "it is a SI ingredient";
	}
	else {
	  $outstr .= "it is a mod ingredient";
	}
      }
      else if (!$do_quest && $custom_freq[$this->id]==0)
        $outstr .= "it is a quest-specific ingredient";
      else if (!$do_rare && $custom_freq[$this->id]==1)
        $outstr .= "it is a rare ingredient";
    }
    $outstr .= '">';
    $outstr .= $this->get_linkname();
    $outstr .= "</td>\n";
    for ($j=2; $j>=0; $j--) {
      $outstr .= '<td><input type="radio" name="custom_use_'. $this->id .'"';
      $outstr .= " value=\"$j\"";
      if ($j == $custom_use[$this->id])
        $outstr .= ' checked="1"';
      $outstr .= ' title="';
      if ($j==2)
        $outstr .= 'YES: Force this ingredient to be used in all potions';
      else if ($j==1)
        $outstr .= 'MAYBE: This ingredient could be used in a potion';
      else
        $outstr .= 'NEVER: Prevent this ingredient from ever being used in a potion';
      $outstr .= "\"></input></td>\n";
    }
    $outstr .= '<td><input type="text" name="custom_freq_'. $this->id . '"';
    $outstr .= ' maxlength="1" size="1" value="';
    if (isset($badinput['custom_freq']) && isset($badinput['custom_freq'][$this->id]))
      $outstr .= NULL;
    else
      $outstr .= $custom_freq[$this->id];
    $outstr .= "\" title=\"Frequency: 0=quest-specific, 1=rare to 5=very common\"";
    $outstr .= "></input></td>\n";
    foreach ($this->effs as $e => $v) {
      $outstr .= "<td class=\"ingeff_";
      if ($v>$alchemy)
        $outstr .= 'unavail';
      elseif ($alleffs[$e]->is_poison())
        $outstr .= 'poison';
      else
        $outstr .= 'potion';
      if (!isset($finde[$e]))
        $outstr .= '_other';
      $outstr .= '">'. $alleffs[$e]->get_linkname(). "</td>\n";
    }
    for ($j=count($this->effs); $j<4; $j++) {
      $outstr .= "<td></td>\n";
    }
    $outstr .= "<td>". $this->weight. "</td>\n";
    $outstr .= "<td>";
    $outstr .= '<input type="hidden" name="ingr_src_'. $this->id . '" value="';
    if ($this->src) {
	$outstr .= $this->src;
    }
    $outstr .= '"></input>';
    $outstr .= $this->cost;
    $outstr .= "</td>\n";
    $outstr .= "</tr>\n";

    return $outstr;
  }
}

define('NOTYPE', 0);
define('STDTYPE', 1);
define('MAGTYPE', 2);
define('DURTYPE', 3);

########################################################################
# Effect Class
########################################################################
class Effect {
# non-arrays
  var $name, $abbrev, $id, $type, $cost, $pct, $poison, $anti;
  var $avail;
  var $mag, $dur, $basemag, $basedur, $sidemag, $sidedur;
# arrays
  var $ings, $group_effs;

# constructor
  function Effect($name=NULL, $id=NULL, $abbrev=NULL, $type=NULL, $cost=NULL, $pct=NULL, $poison=NULL, $anti=NULL, $ings=NULL, $group_effs=NULL) {
    $this->name = $name;
    $this->id = $id;
    $this->abbrev = $abbrev;
    switch ($type) {
      case 'none':
        $this->type = NOTYPE;
        break;
      case 'magonly':
        $this->type = MAGTYPE;
        break;
      case 'duronly':
        $this->type = DURTYPE;
        break;
      case 'std':
      default:
        $this->type = STDTYPE;
        break;
    }
    $this->cost = $cost;
    $this->pct = $pct;
    $this->poison = $poison;
    $this->anti = $anti;
    $this->ings = $ings;
    $this->group_effs = $group_effs;
  }

# simple get/set functions
  function is_poison() {
    return (bool)$this->poison;
  }
  function get_poison() {
    return $this->poison;
  }
  function get_name() {
    return $this->name;
  }
  function get_abbrev() {
    return $this->abbrev;
  }
  function get_avail() {
    return $this->avail;
  }
  function is_ing($i) {
    return isset($this->ings[$i]);
  }

  function get_mag($doside) {
    if ($doside)
      return $this->sidemag;
    else
      return $this->mag;
  }

  function get_dur($doside) {
    if ($doside)
      return $this->sidedur;
    else
      return $this->dur;
  }

  function get_group($eff) {
    if (isset($this->group_effs[$eff]))
      return $this->group_effs[$eff];
    else
      return 0;
  }

  function add_groups(&$extraeffs, $finde) {
    foreach ($this->group_effs as $eff => $ngroup) {
      if (!isset($finde[$eff])) {
        increment_array($extraeffs,$ngroup,$eff);
      }
    }
  }

  function get_anti() {
    if (!isset($this->anti))
      return -1;
    return $this->anti;
  }

  function is_anti($effs, &$antiused) {
    if (!isset($this->anti))
      return 0;
    if (isset($antiused[$this->anti]))
      return -1;
    foreach ($effs as $eff) {
      if ($eff==$this->anti) {
        $antiused[$eff] = 1;
        return 0;
      }
    }
    return 1;
  }

  function get_linkname() {
    $outstr = "";
    $outstr .= '<a href="http://www.uesp.net/wiki/';
    $outstr .= "Oblivion:";
    $linkname = preg_replace('/\s+/', '_', $this->name);
    $linkname = preg_replace('/_\(.*\)$/', '', $linkname);
    $outstr .= $linkname;
    $outstr .= '">';
    $outstr .= $this->name;
    $outstr .= "</a>";
    return $outstr;
  }

# effect strengths and availability
  function init_avail() {
    global $allings;

    $this->avail = 0;
    foreach ($this->ings as $i => $v) {
      if ($allings[$i]->use_ing($this->id))
         $this->avail++;
    }
# must have at least two available ingredients
    if ($this->avail<2)
      $this->avail = FALSE;
    else
      $this->avail = TRUE;
  }

  function init_str() {
    global $allings;
    global $equip, $equip_str;
    global $mod_alc;

    $equip_facs[STDTYPE] = array('Calc' => 0.35,
                                 'CalcDur' => 0.35,
			         'CalcMag' => 1.4,
			         'RetDur' => 1,
			         'RetMag' => 0.5,
			         'Alem' => 2);
    $equip_facs[MAGTYPE] = array('Calc' => 0.3,
			         'RetMag' => 0.5,
			         'Alem' => 2);
    $equip_facs[DURTYPE] = array('Calc' => 0.25,
			         'RetDur' => 0.35,
			         'Alem' => 2);

# NB only strength-specific calculations can come after this point!
    if ($this->type == NOTYPE) {
      $this->mag = $this->dur = $this->basemag = $this->basedur = 1;
      return;
    }

    $str = ($mod_alc + $equip_str[$equip['MP']]*25.)/($this->cost/10.);
    if ($this->type == STDTYPE) {
      $mag = pow($str/4., 1/2.28);
      $dur = 4.*$mag;
    }
    elseif ($this->type == MAGTYPE) {
      $mag = pow($str, 1/1.28);
      $dur = 1;
    }
    else {
      $dur = $str;
      $mag = 1;
    }
    $this->basemag = $mag;
    $this->basedur = $dur;

    if ($this->type==STDTYPE) {
      if (!$this->poison) {
        $mag = $this->basemag;
	if ($equip['Ret']) {
	  $mag += $this->basemag*$equip_facs[$this->type]['CalcMag']*$equip_str[$equip['Calc']];
	}
	else {
	  $mag += $this->basemag*$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']];
	}
        $mag += $this->basemag*$equip_facs[$this->type]['RetMag']*$equip_str[$equip['Ret']];
	
        $dur = $this->basedur;
        $dur += $this->basedur*$equip_facs[$this->type]['CalcDur']*$equip_str[$equip['Calc']];
        $dur += $this->basedur*$equip_facs[$this->type]['RetDur']*$equip_str[$equip['Ret']];
      }
      else {
        $mag = $this->basemag*pow(1.+$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']],2);
        $dur = $this->basedur*pow(1.+$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']],2);
      }
    }
    elseif ($this->type == DURTYPE) {
      $dur = $this->basedur;
      $dur += $this->basedur*$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']];
      if (!$this->poison)
        $dur += $this->basedur*$equip_facs[$this->type]['RetDur']*$equip_str[$equip['Ret']];
    }
    else {
      $mag = $this->basemag;
      if ($this->poison) {
        $mag += $this->basemag*$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']];
      }
      elseif ($equip_str[$equip['Ret']] && $equip_str[$equip['Calc']]) {
        $mag += $this->basemag*$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']] *
	        $equip_facs[$this->type]['RetMag']*$equip_str[$equip['Ret']];
      }
      else {
        $mag += $this->basemag*$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']];
        $mag += $this->basemag*$equip_facs[$this->type]['RetMag']*$equip_str[$equip['Ret']];
      }
    }

# round values up or down
# (really should just need to add 0.5, but there a couple of cases
#  where I've come up with x.4999, and it needs to be rounded up to x+1)
    $this->mag = max(1, floor($mag+0.5001));
    $this->dur = max(1, floor($dur+0.5001));

    if (!$this->poison) {
      $this->sidemag = $this->mag;
      $this->sidedur = $this->dur;
      return;
    }

# calculate strength of negative sideeffects in potions
    if ($this->type == MAGTYPE) {
      $mag -= $this->basemag*$equip_facs[$this->type]['Alem']*$equip_str[$equip['Alem']];
      }
    elseif ($this->type == DURTYPE) {
      $dur -= $this->basedur*$equip_facs[$this->type]['Alem']*$equip_str[$equip['Alem']];
      }
    else {
      $mag = $this->basemag*
             (1.+$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']])*
             (1.+$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']]
                -$equip_facs[$this->type]['Alem']*$equip_str[$equip['Alem']]);
      $dur = $this->basedur*
             (1.+$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']])*
             (1.+$equip_facs[$this->type]['Calc']*$equip_str[$equip['Calc']]
              -$equip_facs[$this->type]['Alem']*$equip_str[$equip['Alem']]);
    }
    $this->sidemag = max(1, floor($mag+0.5001));
    $this->sidedur = max(1, floor($dur+0.5001));
  }

# allow $mag and $dur to be provided, overwriting calc'd values,
# for cases like printing recipes that use old settings
  function get_htmlname($mag=NULL, $dur=NULL, $other=NULL) {
    global $find_poison;
    $out = "";
    $out .= "<span class=\"effect";
    if ($this->poison)
      $out .= "_poison";
    else
      $out .= "_potion";
    if (isset($other) && $other)
      $out .= "_other";
    $out .= "\"><span class=\"effectname\">";
#    $out .= $this->name;
    $out .= $this->get_linkname();
    $out .= "</span>";
    $out .= "<span class=\"effectmag";
    if ((bool)$this->poison!=$find_poison)
      $out .= "_side";
    $out .= "\">";
    $out .= $this->get_textstrength(1, $mag, $dur);
    $out .= "</span></span>\n";
    return $out;
  }

  function get_textstrength($doside=0, $mag=NULL, $dur=NULL) {
    global $find_poison;
    $out = "";

    if ($this->type == NOTYPE)
      return $out;
    
    if (!$doside || ($doside==1 && (bool)$this->poison==$find_poison)) {
      if (!isset($mag))
        $mag = $this->mag;
      if (!isset($dur))
        $dur = $this->dur;
    }
    else {
      if (!isset($mag))
        $mag = $this->sidemag;
      if (!isset($dur))
        $dur = $this->sidedur;
    }

    $out .= " (";
    if ($this->type == STDTYPE || $this->type == MAGTYPE) {
      $out .= $mag;
      if ($this->pct)
        $out .= '%';
      else
        $out .= 'pt';
    }
    if ($this->type == STDTYPE)
      $out .= ', ';
    if ($this->type == STDTYPE || $this->type == DURTYPE)
      $out .= $dur. 's';
    $out .= ")";
    return $out;
  }

  function set_possible_ings($dupok=0) {
    global $allings, $finde, $nskip;

    $out_is = array();
    foreach ($this->ings as $ing => $v) {
      if (!$allings[$ing]->use_ing($this->id))
        continue;
      if ($allings[$ing]->is_score_set()) {
# If a score has already been calculated, then this must be a duplicate
# ingredient... don't bother to add it to the arrays again
        if (!$dupok)
           continue;
      }
      else {
        $allings[$ing]->set_score_orig();
      }
      $out_is[] = $ing;
    }
    if (count($out_is))
      usort($out_is, 'sort_by_score');
    return $out_is;
  }

  function get_unshown($shown) {
    $currlist = array();
    foreach ($this->ings as $i => $v) {
      if (isset($shown[$i]))
        continue;
      $currlist[] = $i;
    }
    return $currlist;
  }

  function get_htmlselect($j) {
    global $eff_to_get, $neff;
    $out = "";

# only print effects that are available with current settings
# ALSO print effects that were previously selected, even if changes in
# available ingredients or alchemy have made them unavailable
# (avoids problems with user requesting to edit the ingredients, but then
#  having the ingredients disappear)
    if (!$this->avail && ($j>=$neff || $eff_to_get[$j]!=$this->id))
      return $out;
    $out .= '<option value="'. $this->id . '"';
    if ($this->is_poison())
      $out .= ' class="effect_poison"';
    else
      $out .= ' class="effect_potion"';
    
    if ($j<$neff && $eff_to_get[$j]===$this->id)
      $out .= ' selected';
    $out .= '>';
    $out .= $this->name;
    $out .= $this->get_textstrength(0);
    $out .= "\n";
    return $out;
  }
}

########################################################################
# Recipe Class
########################################################################
class Recipe {
# non-arrays
  var $cost = NULL;
  var $ispoison, $alchemy, $mod_alc, $luck;
  var $initkey;
# arrays
  var $ings, $effs, $eff_mags, $eff_durs;
  var $equip;

  function Recipe($inglist="", $key="") {
    global $allings;
    $this->initkey = $key;
    $this->ings = array();
    $nsets = explode('x', $inglist);
# explicitly count here to make sure no more than 4 input slots
    for ($ns=0; $ns<count($nsets) && $ns<4; $ns++) {
      $this->ings[$ns] = explode('_', $nsets[$ns]);
# make sure that there isn't any out-of-bounds data here
# if there is, this truncating is probably going to yield bizarre results,
# but at least the code won't crash
# and any out-of-bounds data is from users tampering with the form, so its
# their own fault if they end up with screwy results
      for ($is=0; $is<count($this->ings[$ns]); $is++) {
        $this->ings[$ns][$is] = max(-1, min(count($allings), $this->ings[$ns][$is]));
      }
    }
    $this->cost = NULL;
    $this->name = 'No name';
  }

  function get_initkey() {
    return $this->initkey;
  }

  function is_valid() {
    if (!count($this->ings))
      return 0;
    return 1; 
  }

  function clear() {
    $this->ings = $this->effs = $this->eff_mags = $this->eff_durs = array();
    $this->equip = array();
    $nset = 0;
  }

  function set_name($name) {
    $ok = 1;
# convert string into HTML-safe version
# not sure whether any extra checking is needed... if I do add any
#  just need to return 0 if the checks fail
    $this->name = htmlentities($name);
    return $ok;
  }

  function check_update() {
    if (!isset($this->cost))
      $this->set_stats();
  }

  function set_stats() {
    global $alleffs, $allings;
    global $alchemy, $luck, $mod_alc, $equip, $potion_cost;

    $curr_effs = array();
    foreach ($this->ings as $islot => $v) {
      if ($this->ings[$islot][0]<0)
# negative means any 2 of following list, so add in second one from next list
        $allings[$this->ings[$islot+1][1]]->add_effs($curr_effs);
      else 
        $allings[$this->ings[$islot][0]]->add_effs($curr_effs);
    }
# master-level single-ingredient potions
    if ($alchemy>=100 && count($this->ings)==1) {
      $e = $allings[$this->ings[0][0]]->master_eff();
      increment_array($curr_effs,1,$e);
    }
    $this->effs = array();
    foreach ($curr_effs as $e => $v) {
      if ($curr_effs[$e]>=2 && !$alleffs[$e]->is_poison())
        $this->effs[] = $e;
    }
    sort($this->effs);
    if (count($this->effs))
      $this->ispoison = 0;
    else
      $this->ispoison = 1;
    $teffs = array();
    foreach ($curr_effs as $e => $v) {
      if ($curr_effs[$e]>=2 && $alleffs[$e]->is_poison())
        $teffs[] = $e;
    }
    sort($teffs);
    $this->effs = array_merge($this->effs, $teffs);

    $this->cost = $potion_cost;
    foreach ($this->effs as $e) {
      if ($this->ispoison == $alleffs[$e]->is_poison()) {
        $this->eff_mags[$e] = $alleffs[$e]->get_mag(0);
        $this->eff_durs[$e] = $alleffs[$e]->get_dur(0);
      }
      else {
        $this->eff_mags[$e] = $alleffs[$e]->get_mag(1);
        $this->eff_durs[$e] = $alleffs[$e]->get_dur(1);
      }
    }

# store inputs under which these stats were calculated
    $this->alchemy = $alchemy;
    $this->luck = $luck;
    $this->mod_alc = $mod_alc;
    $this->equip = $equip;
  }

  function print_recipe($narr, $nprt, $doonly=NULL) {
    global $alleffs, $allings;
    global $equip_types, $equip_names, $equip_opts;

    $outstr = "";

    if (!isset($this->cost))
      $this->set_stats();

    $outstr .= '<div class="recipe" id="recipe_num_'.$narr."\">\n";
    $outstr .= '<a name="recipe_num_'.$narr.'"></a>'."\n";
    $outstr .= '<h3 class="potion-header">';
    $outstr .= "Recipe ".$nprt. ": ";
    if (isset($doonly) && $doonly) {
       $outstr .= $this->name;
    }
    else {
      $outstr .= "<span class=\"noemph\"><input type=\"text\" name=\"recipe_name_$narr\" value=\"". $this->name. "\"></input>\n";
      $outstr .= "<span class=\"rightalign_small\"><input type=\"checkbox\" name=\"recipe_del_$narr\">Delete this recipe</input></span>\n";
      $outstr .= "</span>\n";
    }
    $outstr .= "</h3>\n";

    $first = 1;
    foreach ($this->effs as $e) {
      if (!$first)
        $outstr .= " + ";
      $first = 0;
      $outstr .= $alleffs[$e]->get_htmlname($this->eff_mags[$e], $this->eff_durs[$e]);
      $outstr .= "\n";
    }

    $do2col = 0;
    $outstr .= "<table class=\"potiontable\"><tr>\n";
    for ($ns=0; $ns<count($this->ings); $ns++) {
      if ($this->ings[$ns][0]<0) {
        $do2col = 1;
        continue;
      }
      $outstr .= "<td class=\"potioncolumn";
      if ($do2col)
        $outstr .= "_double\" colspan=\"2\"";
      else
        $outstr .= '"';
      $outstr .= ">\n";
      if ($do2col)
        $outstr .= "<strong>Any TWO of the following:</strong><br />\n";
      for ($is=0; $is<count($this->ings[$ns]); $is++) {
        if ($is && $is==count($this->ings[$ns])-1)
          $outstr .= "OR ";
        $outstr .= $allings[$this->ings[$ns][$is]]->get_name_and_title();
        if ($is==count($this->ings[$ns])-2)
          $outstr .= "<br />";
        elseif ($is!=count($this->ings[$ns])-1)
          $outstr .= ",<br />";
      }
      $outstr .= "</td>\n";
      $do2col = 0;
    }
    for ($ns=count($this->ings); $ns<4; $ns++) {
      $outstr .= "<td class=\"potioncolumn_unused\"></td>\n";
    }
    $outstr .= "</tr></table>\n";

    $outstr .= "<div class=\"recipeblock\">\n";
    $outstr .= "Recipe availability and statistics calculated for:\n";
    $outstr .= "<ul>\n";
    $outstr .= "<li><em>";
    $outstr .= $equip_opts[floor($this->alchemy/25)+1];
    $outstr .= "</em> alchemist (skill=$this->alchemy, luck=$this->luck)</li>\n";
    $outstr .= "<li>Equipment quality: ";
    $first = 1;
    foreach ($equip_types as $eq) {
      if (!$first)
        $outstr .= " + ";
      $first = 0;
      $outstr .= $equip_opts[$this->equip[$eq]];
    }
    $outstr .= "</li>\n</ul></div>\n";

    calc_weight($this->ings, $totwtmin, $totwtmax);
    calc_ingcost($this->ings, $totcostmin, $totcostmax);
    $outstr .= "<div class=\"recipestats\">\n";
    $outstr .= "<ul>\n";
    $outstr .= "<li>Potion weight = $totwtmin";
    if ($totwtmax>$totwtmin)
      $outstr .= " - $totwtmax";
    $outstr .= "</li>\n";
    $outstr .= "<li>Potion value = $this->cost</li>\n";
    $outstr .= "<li>Ingredient cost = $totcostmin";
    if ($totcostmax>$totcostmin)
      $outstr .= " - $totcostmax";
    $outstr .= "</li>\n</ul>\n</div>\n";
    $outstr .= "</div>\n";
    return $outstr;
  }
}

########################################################################
# Potion class
#########################################################################
class Potion {
# non-arrays
  var $id, $freqtot, $effid, $keyid, $ning;
# arrays
  var $ings;

  function Potion($id=NULL, $ings=NULL, $freqtot=NULL, $effid=NULL, $keyid=NULL, $sc_pro=NULL, $sc_con=NULL, $sc_key=NULL) {
    global $poteffs, $keyeffs, $eff_score_pro, $eff_score_con;

    $this->id = $id;
    $this->ings = $ings;
    $this->freqtot = $freqtot;
    $this->effid = $effid;
    $this->keyid = $keyid;
    $this->ning = count($this->ings);

    $poteffs[$this->effid][$this->freqtot][$this->id] = 1;
    $eff_score_pro[$this->effid] = $sc_pro;
    $eff_score_con[$this->effid] = $sc_con;

    $keyeffs[$this->keyid][$this->effid] = 1;
    $eff_score_pro[$this->keyid] = $sc_key;
    $eff_score_con[$this->keyid] = 0;
  }

  function delete() {
    global $poteffs;
    unset($poteffs[$this->effid][$this->freqtot][$this->id]);
  }

  function get_ning() {
    return $this->ning;
  }
  function get_effid() {
    return $this->effid;
  }
  function get_ings() {
    return $this->ings;
  }

  function add_to_npot(&$npot, &$npot_anti_b) {
    global $eff_score_con;
    increment_array($npot,1,'freq',$this->freqtot);
    increment_array($npot,1,'ning',$this->ning);
    increment_array($npot,1,'effid',$this->effid);
    increment_array($npot,1,'keyid',$this->keyid);
    increment_array($npot,1,'fq-id',$this->freqtot,$this->effid);
    increment_array($npot,1,'id-fq',$this->effid,$this->freqtot);
    increment_array($npot,1,'key-fq-id',$this->keyid,$this->freqtot,$this->effid);
    increment_array($npot,1,'key-id-fq',$this->keyid,$this->effid,$this->freqtot);
    if ($eff_score_con[$this->effid])
      $npot_anti_b++;
  }
}

# it would be nice to use a variable parameter list here, but it is
# not possible because the first argument has to be by-ref
function increment_array(&$a, $inc, $key1, $key2=null, $key3=null, $key4=null) {
  if (is_null($key2)) {
    $ref =& $a[$key1];
  }
  elseif (is_null($key3)) {
    $ref =& $a[$key1][$key2];
  }
  elseif (is_null($key4)) {
    $ref =& $a[$key1][$key2][$key3];
  }
  else {
    $ref =& $a[$key1][$key2][$key3][$key4];
  }  
  if (!isset($ref))
    $ref = 0;
  $ref+=$inc;
}

# all initialization related to effect strengths
function setup_effects() {
  global $new_sess;
  global $luck, $alchemy, $equip, $do_SI, $do_quest, $do_rare, $use_direnni, $custom_freq, $custom_use;
  global $equip_types, $allings, $badinput;

  if ($new_sess) {
# new session: provide default values for variables
    $luck = 50;
# alchemy set to 75 instead of 100 by default so master-level (i.e,
# one ingredient) potions don't appear automatically
    $alchemy = 75;
    foreach ($equip_types as $eq)
      $equip[$eq] = 1;
    $use_direnni = 0;
    $do_SI = 1;
    $do_quest = 0;
    $do_rare = 0;
  }

###
### Get input data
### All effect-related input data is read, validated, and stored in this section.
###

  if (isset($_SERVER['REQUEST_METHOD']) &&
      $_SERVER['REQUEST_METHOD'] == 'POST') {
# luck: must be integer, allowed range 0-299 (allowing to go past 100
# in case it's been boosted)
    if (isset($_POST['luck']) &&
        ctype_digit($_POST['luck']) &&
        $_POST['luck']<300) {
# NB only overwriting existing value if it has changed
# mainly just a holdover from early versions of code, which kept track of
# which variables had been changed
      if (intval($_POST['luck'])!=$luck)
        $luck = intval($_POST['luck']);
    }
    elseif (isset($_POST['luck']))  {
      $badinput['luck'] = 1;
# default value for luck, just so calculations can be done
      $luck = 50;
    }

# alchemy: must be integer, allowed range 0-299
    if (isset($_POST['alchemy']) &&
        ctype_digit($_POST['alchemy']) &&
        $_POST['alchemy']<300) {
      if (intval($_POST['alchemy'])!=$alchemy)
        $alchemy = intval($_POST['alchemy']);
    }
    elseif (isset($_POST['alchemy'])) {
      $badinput['alchemy'] = 1;
# default value for alchemy, just so calculations can be done
      $alchemy = 75;
    }

# equipment types: must be integer, allowed range 0-6
# (6 used only by Direnni; allowed even if use_direnni hasn't been set
#  but should perhaps make that trigger use_direnni=true?)
# ignore bad data: only possible if user is not using form
# (also, not checking that mortar+pestle>0, again only possible if user
#  is not using form)
    foreach ($equip_types as $eq) {
      if (isset($_POST[$eq]) &&
          ctype_digit($_POST[$eq]) &&
          $_POST[$eq]<=6) {
        if (intval($_POST[$eq])!=$equip[$eq])
	  $equip[$eq] = intval($_POST[$eq]);
      }
    }

# flags: boolean
# don't bother to do much error checking here, just cast them into yes/no values
# this is done fairly early in processing, so values can be overridden
# (i.e., by set_eff_, exact_eff_)
    $use_direnni = isset($_POST['use_direnni']);
    $do_SI = isset($_POST['do_SI']);
    $do_quest = isset($_POST['do_quest']);
    $do_rare = isset($_POST['do_rare']);
  }
  else if (isset($_SERVER['REQUEST_METHOD']) &&
	 $_SERVER['REQUEST_METHOD'] == 'GET') {
# only a limited number of variables are expected to be set using GET
    if (isset($_GET['alchemy']) &&
        ctype_digit($_GET['alchemy']) &&
        $_GET['alchemy']<300) {
      if (intval($_GET['alchemy'])!=$alchemy)
        $alchemy = intval($_GET['alchemy']);
    }
    if (isset($_GET['do_SI'])) {
      if ($_GET['do_SI']) {
        $do_SI = TRUE;
      }
      else {
	$do_SI = FALSE;
      }
    }	    
    if (isset($_GET['do_quest'])) {
      if ($_GET['do_quest']) {
	$do_quest = TRUE;
      }
      else {
	$do_quest = FALSE;
      }
    }	    
    if (isset($_GET['do_rare'])) {
      if ($_GET['do_rare']) {
	$do_rare = TRUE;
      }
      else {
	$do_rare = FALSE;
      }
    }	    
  }
}

# Processing of effect-related variables
function process_effects() {
  global $mod_alc, $potion_cost, $allings, $alleffs, $req_ings;
  global $luck, $alchemy, $equip, $equip_str;
  global $badinput;

# modified alchemy for potion strengths
  $mod_alc = max(0, min(100, max(0, min(100, $alchemy))+($luck-50)*2/5));
  $potion_cost = floor(($mod_alc+$equip_str[$equip['MP']]*25)*0.45);

# available ingredients
  $req_ings = array();
  for ($i=0; $i<count($allings); $i++) {
    if ($allings[$i]->set_avail()) {
# return value of TRUE signals that this ingredient is must-use
      $req_ings[] = $i;
    }
  }
# check the number of ingredients with custom_use=2
# (maximum 4 possible)
  if (count($req_ings)>4) {
    $badinput['custom_use'] = 1;
  }

# effect strengths and availability
  for ($e=0; $e<count($alleffs); $e++) {
    $alleffs[$e]->init_avail();
    $alleffs[$e]->init_str();
  }
}

function setup_potions() {
  global $new_sess, $do_potion;
  global $allow_neg, $exact_match, $prev_request, $curr_request;
  global $maxprint, $maxprset;
  global $allings, $alleffs, $badinput, $custom_use, $custom_freq;
  global $neff_in_max, $neff_in, $neff, $eff_to_get, $finde;
  global $curr_link;

  if ($new_sess) {
# new session: provide default values for variables
    $allow_neg = 1;
    $exact_match = 0;
    $prev_request = $curr_request = NULL;

# This is the target number of potions to print out
# The actual number printed may exceed $maxprint, but probably only by 50%
# Decreasing $maxprint will not have too much effect on runtime, but will
# reduce the size of the output
    $maxprint = 50;
# This is the maximum number of effect combinations to print out
    $maxprset = 10;
  }

  $neff_in_max = 6;
  $neff_in = 5;
  $neff = 0;
  $eff_to_get = array();
  $finde = array();
  $do_potion = 1;
###
### Get input data
### All input data is read, validated, and stored in this section.
###
  if (isset($_SERVER['REQUEST_METHOD']) &&
      $_SERVER['REQUEST_METHOD'] == 'POST') {
# first determine whether potion data is even included with this post
    if (isset($_POST['do_potion'])) {
      $do_potion = (bool)$_POST['do_potion'];
    }
    if (!$do_potion) {
# even if do_potion set to zero, check to see if there was a list of
# potion effects provided (needed to get the correct ingredient list)
      setup_potion_effects();
      return;
    }

# flags: boolean
# don't bother to do much error checking here, just cast them into yes/no values
# this is done fairly early in processing, so values can be overridden
# (i.e., by set_eff_, exact_eff_)
    $allow_neg = isset($_POST['allow_neg']);
    $exact_match = isset($_POST['exact_match']);

# see if there was a request to look up a set of effects
# - check for a variable named "set_eff_" or "exact_eff_"
#   (only difference between two is the setting for exact_match)
    $eff_done = 0;
    foreach ($_POST as $key => $val) {
      if (substr($key,0,8)=="set_eff_") {
        $elist = explode('_', substr($key,8));
        $exact_match = 0;
      }
      elseif (substr($key,0,10)=="exact_eff_") {
        $elist = explode('_', substr($key,10));
        $exact_match = 1;
      }
      else
        continue;
      $nt = 0;
      $eff_to_get = array();
# check that elist values are all legit
      foreach ($elist as $e) {
        if (ctype_digit($e) &&
            $e<count($alleffs) &&
            !isset($finde[$e])) {
          $e = intval($e);
          $eff_to_get[$nt] = $e;
          $finde[$e] = $nt;
          $nt++;
        }
      }
      if (count($eff_to_get)) {
        $eff_done = 1;
        $neff = count($eff_to_get);
        $curr_link = "Potions";
        break;
      }
    }
# standard requested effects: must be integers, between 0 and count($alleffs)
# ignore bad data: only possible if user is not using form
    if (!$eff_done) {
      setup_potion_effects();
    }

# number of entries to print: must be integers
# arbitrarily allow values up to 1000 for maxprint, 100 for maxprset
#  (actually, these are also being enforced by size of input field)
    if (isset($_POST['maxprint']) &&
        ctype_digit($_POST['maxprint']) &&
        $_POST['maxprint']<1000) {
      $maxprint = intval($_POST['maxprint']);
    }
    elseif (isset($_POST['maxprint'])) {
      $badinput['maxprint'] = 1;
      $maxprint = 50;
    }
    if (isset($_POST['maxprset']) &&
        ctype_digit($_POST['maxprset']) &&
        $_POST['maxprset']<100) {
      $maxprset = intval($_POST['maxprset']);
    }
    elseif (isset($_POST['maxprset'])) {
      $badinput['maxprset'] = 1;
      $maxprset = 50;
    }

# see whether user requested to return to a previous set of settings
#  'submit-prev' request is processed last:
#  overrides any other settings that may have been changed
    if (isset($_POST['submit-prev'])) {
      $curr_link = "Potions";
# assume that format of any info saved in prev_request is fine
# (comes from session data, not posted data)
      if (isset($prev_request)) {
        $opts = explode('+', $prev_request);
        $eff_to_get = explode('_', $opts[0]);
        $neff = count($eff_to_get);
        $finde = array();
        for ($nt=0; $nt<count($eff_to_get); $nt++) {
          $eff_to_get[$nt] = intval($eff_to_get[$nt]);
          $finde[$eff_to_get[$nt]] = $nt;
        }
        array_shift($opts);
        foreach ($opts as $optstring) {
          preg_match('/(.*)=(.*)/', $optstring, $optsplit);
          $$optsplit[1] = (bool)$optsplit[2];
        }
      }
    }
  }
  else {
# For potions specifically, check get arguments to allow URL to
# request a specific set of effects (allow links from other places)
# Should allow strings like
# "alc_calc.php?effect1=Restore_Health&effect2=Restore_Magicka"

# First clear out any previous info, if this is a returning user
    if (!$new_sess) {
# clear out requested effects
      $neff = 0;
      $eff_to_get = array();

      if (isset($curr_request)) {
        $prev_request = $curr_request;
        $curr_request = NULL;
      }
    }

    for ($j=0; $j<$neff_in_max; $j++) {
      $jb = $j+1;
      if (!isset($_GET["effect$jb"]) || $_GET["effect$jb"]==-1)
        continue;
      if (ctype_digit($_GET["effect$jb"])) {
        $e = intval($_GET["effect$jb"]);
        if ($e<count($alleffs) && !isset($finde[$e])) {
          if (!isset($eff_to_get[$neff]) || $e!=$eff_to_get[$neff]) {
            $eff_to_get[$neff] = $e;
          }
          $finde[$e] = $neff;
          $neff++;
        }
      }
      else {
        $testa = $_GET["effect$jb"];
# clean up input string: change _/+ into a space; delete non-alpha
# numeric characters; eliminate extra spaces; apply ucwords
        $testa = preg_replace('/[_\+]/', ' ', $testa);
        $testa = preg_replace('/[^\s\w]/', '', $testa);
        $testa = preg_replace('/^\s*/', '', $testa);
        $testa = preg_replace('/\s*$/', '', $testa);
        $testa = preg_replace('/\s+/', ' ', $testa);
        $testa = ucwords(strtolower($testa));
        for ($e=0; $e<count($alleffs); $e++) {
          if ($testa===$alleffs[$e]->get_name()) {
            if (!isset($eff_to_get[$neff]) || $e!=$eff_to_get[$neff]) {
              $eff_to_get[$neff] = $e;
            }
            $finde[$e] = $neff;
            $neff++;
            
            break;
          }
        }
      }
    }
    if (count($eff_to_get)) {
	$curr_link = "Potions";
    }

    if (isset($_GET['exact_match'])) {
      if ($_GET['exact_match']) {
	$exact_match = TRUE;
      }
      else {
	$exact_match = FALSE;
      }
    }
    else {
      $exact_match = FALSE;
    }
  }
}

function setup_potion_effects() {
  global $finde, $eff_to_get, $neff, $neff_in;
  global $alleffs;

  $nt = 0;
  for ($j=0; $j<$neff_in; $j++) {
    if (!isset($_POST["effect$j"]) || $_POST["effect$j"]==-1)
      continue;
    if (ctype_digit($_POST["effect$j"]) && $_POST["effect$j"]<count($alleffs)) {
      $e = intval($_POST["effect$j"]);
      if (!isset($finde[$e])) {
        if (!isset($eff_to_get[$nt]) || $e!=$eff_to_get[$nt]) {
          $eff_to_get[$nt] = $e;
        }
        $finde[$e] = $nt;
        $nt++;
      }
    }
  }
  if ($neff!=$nt) {
    $neff = $nt;
  }
# make sure eff_to_get is empty past neff
# (otherwise some sections that rely on eff_to_get alone get messed up)
  array_splice($eff_to_get, $neff);
}

function process_potions() {
  global $do_potion;
  global $badinput, $neff, $curr_request, $prev_request;
  global $allow_neg, $show_section, $do_SI, $do_quest, $do_rare, $exact_match;
  global $maxprint, $maxprset;
  global $allings, $alleffs, $req_ings;
  global $allpotions, $poteffs, $keyefffs, $freqmax, $eff_score_con, $eff_score_pro;
  global $npot_orig, $npot_prob, $npot_anti, $npot_anti_b, $npot_ex, $npot;
  global $antis, $antiused;
  global $nings, $skip_set, $keydone;
  global $freqonly, $toprint, $nprint, $poss_extra;
  global $prgroups, $altid;
  global $extraeffs, $extraeffs_lim, $missing_eff, $reduce_level;
  global $eff_to_get, $my_eff_to_get, $finde, $curr_effs, $baseid;
  global $possible_ings, $discrep, $find_poison;

  if (!$do_potion && !$neff)
    return;

  $do_find = 0;
  $new_request = NULL;
# find effects 
  if (!count($badinput) && $neff) {
    $do_find = 1;
    $possible_ings = array();

    $discrep = 0;
    $new_request = "";
    for ($f=0; $f<$neff; $f++) {
      if ($f)
        $new_request .= "_";
      $new_request .= $eff_to_get[$f];
      if ($f==0) {
        $find_poison = $alleffs[$eff_to_get[$f]]->is_poison();
      }
      elseif ($find_poison!=$alleffs[$eff_to_get[$f]]->is_poison()) {
# DISCREPANCY
# If there is a discrepancy, this is going to be a potion, so set find_poison=0
# For discrepancies, a few changes are implemented (since it's impossible to
# know how user wants to prioritize effects)
#  - extraeffs set to be an empty array (don't prioritize any additional effects)
#  - make no attempt to add extra ingredients, or counteract negative effects
# Things that haven't been tweaked
#  - scores are still calculated by treating the final product as a potion,
#    i.e., positive effects are sorted higher than negative effects
#  - deciding potions to print does not explicitly get changed when discrep set
#    But previous tweaks imply that all potions will be in the same $kset (same
#    list of key effects).  Therefore printing will basically be decided on
#    frequency alone.  Printing groups will be determined based on positive
#    side effects still
        $discrep = 1;
        $find_poison = 0;
      }
    }
    if ($discrep && !$allow_neg) {
      $do_find = 0;
      $show_section['ing'] = 0;
    }
  }

  if ($do_potion &&
      isset($curr_request) &&
      (!isset($new_request) ||
       substr($curr_request,0,strlen($new_request)+1)!=$new_request.'+')) {
# If the effects do not match up with the last request, then treat this as new
# move current request (i.e., one from last call) into previous slot
      $prev_request = $curr_request;
    }

# add in option settings (do not influence whether this request matches previous one)
  if ($do_potion) {
    if (isset($new_request)) {
      foreach (array("do_SI", "do_quest", "do_rare", "allow_neg", "exact_match") as $opt) {
        $new_request .= "+$opt=".$$opt;
      }
    }
    $curr_request = $new_request;
  }

  $extraeffs = $extraeffs_lim = array();
  $missing_eff = array();
  $reduce_level = 0;
  if ($do_find) {
# If a combined poison/potion requested, don't bother to set up a list of
# extra effects
    if (!$discrep) {
      for ($f=0; $f<$neff; $f++)
        $alleffs[$eff_to_get[$f]]->add_groups($extraeffs, $finde);
      uksort($extraeffs, 'sort_extraeff');
    }

    for ($f=0; $f<$neff; $f++) {
      $e = $eff_to_get[$f];
      $possible_ings[$e] = $alleffs[$e]->set_possible_ings(1);
      if (count($possible_ings[$e])<2) {
        $missing_eff[] = $e;
        $do_find = 0;
      }
    }
# re-sort eff_to_get so that the effects with the least ingredients get
# processed first
# this can be done no matter how many potions I expect to get
# note that this order is only used internally... eff_to_get is still
# used for outputting data, so the user doesn't have to deal with his
# effect list being reordered all the time
    $my_eff_to_get = $eff_to_get;
    usort($my_eff_to_get, 'sort_by_ingcount');

# check any required ingredients... want to know if any requested effects
# are already taken care of by required ingredients
    $curr_effs = array();
    foreach ($req_ings as $ing)
      $allings[$ing]->add_effs($curr_effs);

# move any effects that have already been taken care of by required ingredients
# to the end of the list
    $n = $nmove = 0;
    while ($n<count($my_eff_to_get)-$nmove) {
      $e = $my_eff_to_get[$n];
      if (isset($curr_effs[$e]) && $curr_effs[$e]>=2) {
        array_splice($my_eff_to_get, $n, 1);
        array_push($my_eff_to_get, $e);
        $nmove++;
      }
      else {
        $n++;
      }
    }

# figure out which effects do and don't overlap
#  implement extreme measures if more than 6 in list
# any same-sign effects WITHOUT any overlaps are useless
# pair up any ingredients with all same same-sign effects
#  set up so only one gets fully processed by find_potion
#     (on second, only pair with third, then skip)
# prioritize ingredients by fewest anti-sign effects, frequency
    $matche = array();
    $done = array();
    foreach ($my_eff_to_get as $e) {
      foreach ($possible_ings[$e] as $i) {
        if (isset($done[$i]))
          continue;
        $done[$i] = 1;
        $allings[$i]->add_effs($matche);
      }
    }

# Take any unpairable effects out of extraeffs_lim list
    $extraeffs_lim = $extraeffs;
    foreach ($extraeffs_lim as $et => $v) {
      if (!isset($matche[$et]) || $matche[$et]<2)
        unset($extraeffs_lim[$et]);
    }

# Completely redo scores here
# This score takes into account which sideeffects are actually possible
# from current ingredients list, and can't be calculated until all
# ingredients are known
    $done = array();
    for ($t=0; $t<count($my_eff_to_get); $t++) {
# for some reason a "foreach ($my_eff_to_get as $e)" statement gets
# short-circuited by creating a new tlist array.  I'm guessing it's a PHP
# glitch, but to get rid of it for now, count through the loop
      $e = $my_eff_to_get[$t];
      $tlist = array();
      foreach ($possible_ings[$e] as $i) {
# remove duplicate ingredients at this point
        if (isset($done[$i]))
          continue;
        $done[$i] = 1;
        $allings[$i]->set_score_mod();
        $tlist[] = $i;
      }
      usort($tlist, 'sort_by_score');
      $possible_ings[$e] = $tlist;
    }

# Now see whether or not I have a problem with the number of potions
# (wait until now so ingredients with multiple effects have been
#  cleaned up)
    if ($find_poison || !$allow_neg || $exact_match)
# effectively I'm assuming that half of the potions will not
# be possible in these cases
      $np = 0.5;
    else
      $np = 1;
    for ($n=0; $n<count($my_eff_to_get); $n++) {
      $e = $my_eff_to_get[$n];
      if ($n<2) {
# Multiply in maximum possible number of combinations using two ingredients
# from this effect's list
        $np *= count($possible_ings[$e])*(count($possible_ings[$e])+1)/2;
      }
      elseif ($n<4) {
# Past first two effects, unlikely to ever use more than one ingredient
        $np *= count($possible_ings[$e]);
      }
      if ($np > MAXPOTION) {
        $reduce_level = 1;
        break;
      }
    }

# If current measures don't work, it might be necessary to actually
# remove some ingredients from the list at this point... but hopefully
# I won't have to go that far
  
# assign $baseid using my_eff_to_get
# needs to be done before calling find_potion
    $baseid = join($my_eff_to_get, '.');
  }

  if (!$do_potion)
    return;

  $allpotions = array();
  if ($do_find) {
    $poteffs = array();
    $keyeffs = array();
    $freqmax = array();
    $eff_score_con = $eff_score_pro = array();

# This one call actually does most of the work finding potions
    $npot_orig = find_potions($my_eff_to_get, 0, $req_ings);
  }

  $freqonly = $toprint = array();
  $nprint = 0;

  if (count($allpotions)) {
# This count is done before trying to counteract negative side effects
# just to avoid complications from having 4 ingredients even when there is
# only one effect
# Also, in cases where all potions have negative side effects, allows a list
# of alternatives to still be generated
    $poss_extra = array();
    if (count($my_eff_to_get)==1 &&
        !$discrep &&
        !$exact_match) {
      foreach ($poteffs as $effid => $v) {
        $effs = explode('.', $effid);
        foreach ($effs as $e) {
          if (isset($extraeffs[$e]))
            $poss_extra[$e] = 1;
        }
      }

      foreach ($allpotions as $ingid => $v) {
        if (count($poss_extra)==count($extraeffs))
          break;
# don't check for any single-ingredient potions (no ingredients can be added!)
        if ($allpotions[$ingid]->get_ning()==1)
          continue;

        $ings = $allpotions[$ingid]->get_ings();
        $curr_effs = array();
        foreach ($ings as $i)
          $allings[$i]->add_effs($curr_effs);
        foreach ($extraeffs as $e => $v1) {
          if (isset($poss_extra[$e]))
            continue;
          if ((isset($curr_effs[$e]) && $curr_effs[$e]>=2) ||
              find_potions($e, 3, $ings)) {
            $poss_extra[$e] = 1;
          }
        }
      }
    }

    $npot_prob = 0;
    if (!$find_poison &&
        !$discrep &&
        !$exact_match) {
# try to counteract any negative side effects
      foreach ($allpotions as $ingid => $v) {
# if con score is 0, there are no negative side effects
        if (!$eff_score_con[$allpotions[$ingid]->get_effid()])
          continue;
# skip any single-ingredient potions (they should have already been
# skipped since their con score should be 0, but just to be safe
# explicitly check ning too)
        if ($allpotions[$ingid]->get_ning()==1)
          continue;

        $effs = explode('.', $allpotions[$ingid]->get_effid());
        $antis = array();
        $antiused = array();
        foreach ($effs as $e) {
          $status = $alleffs[$e]->is_anti($effs, $antiused);
          if ($status<0) {
# no point in even searching if status<0
# (means that multiple negative effects have same counteragent)
            $antis = array();
            break;
          }
# add effects that have not been counteracted, but could possibly be
          if ($status>0)
	    $antis[] = $alleffs[$e]->get_anti();
        }
        if (count($antis)<1)
          continue;

        $adone = 0;
        if ($allpotions[$ingid]->get_ning()<4)
          $adone = find_potions($antis, 1, $allpotions[$ingid]->get_ings());

        if (!$allow_neg) {
# if allow_neg=0 remove from list now that I've tried to counteract all
# negative effects (the potions with counteracting effects are saved separately)
	  $allpotions[$ingid]->delete();
          unset($allpotions[$ingid]);
          $npot_orig--;
        }
        else {
          $npot_prob++;
        }
      }
    }

    $npot_anti = count($allpotions) - $npot_orig;

# try to add extra ingredients to add useful additional effects
    if (count($allpotions)<MAXPOTION &&
        count($my_eff_to_get)>1 &&
        !$discrep &&
        !$exact_match) {
      foreach ($allpotions as $ingid => $v) {
        if ($allpotions[$ingid]->get_ning()>=4)
          continue;
        if ($allpotions[$ingid]->get_ning()==1)
          continue;

        $ings = $allpotions[$ingid]->get_ings();
        $curr_effs = array();
        foreach ($ings as $i)
          $allings[$i]->add_effs($curr_effs);
# This search looks to match existing unpaired effects
        foreach ($curr_effs as $e => $v) {
          if (!isset($extraeffs[$e]) || $curr_effs[$e]>=2)
            continue;
          find_potions($e, 2, $ings);
        }

# If potion only has 2 ingredients, more effects should also be possible
# Note that this check is only done when more than one effect was requested
# (one-effect cases are all treated separately in earlier loop)
        if ($allpotions[$ingid]->get_ning()>2)
          continue;
        foreach ($extraeffs as $e => $v) {
          if (isset($curr_effs[$e]))
            continue;
          find_potions($e, 2, $ings);
        }
      }
    }

    $npot_ex = count($allpotions) - $npot_anti - $npot_orig;

# statistics on potions, to help with deciding which ones to print
    $npot = array();
    $npot_anti_b = 0;
    foreach ($allpotions as $ingid => $v)
      $allpotions[$ingid]->add_to_npot($npot, $npot_anti_b);

    if (count($allpotions)) {
      $nings = array_keys($npot['ning']);
      sort($nings);

# if there are few enough options, print them all
# allow a few extra, since selection routine allows nprint to exceed $maxprint
      if (count($allpotions)<$maxprint*1.3) {
        foreach ($npot['id-fq'] as $effid => $v) {
          $freqonly[$effid] = 0;
          foreach ($npot['id-fq'][$effid] as $freq => $v1) {
            $nprint += $npot['id-fq'][$effid][$freq];
            $toprint[$effid][$freq] = 1;
          }
        }
      }
      else {
        $skip_set = array();

# if I'm not going to print all potions, immediately prune out any
# potions with side-effects that do not improve on other available potions
# (i.e., no additional good effects, no significant improvement in availability)
        if (!$find_poison && $allow_neg && !$discrep) {
          foreach ($npot['id-fq'] as $effid => $v) {
            $ids = explode('.', $effid);
            $poison = 0;
            while ($alleffs[$ids[count($ids)-1]]->is_poison()) {
              $poison = 1;
              array_pop($ids);
            }
            if (!$poison)
              continue;
            $idnew = join($ids, '.');
            if (isset($npot['id-fq'][$idnew])) {
              $fqmax = max(array_keys($npot['id-fq'][$idnew]));
              $fqs = array_keys($npot['id-fq'][$effid]);
              sort($fqs);
              foreach ($fqs as $freq) {
                if ($freq < $fqmax+FREQDIFF);
                  $skip_set[$effid][$freq] = 1;
              }
            }
          }
        }

# first add particularly easy to make potions
        $fqs = array_keys($npot['freq']);
        rsort($fqs);
        foreach ($fqs as $freq) {
          if ($freq<$fqs[0]-FREQDIFF)
            break;
          $ids = array_keys($npot['fq-id'][$freq]);
          usort($ids, 'sort_by_eff_score');
          array_unshift($ids, $baseid);
          foreach ($ids as $effid) {
            if (!isset($npot['fq-id'][$freq][$effid]) ||
                isset($toprint[$effid][$freq]) ||
                isset($skip_set[$effid][$freq]))
            continue;
            $toprint[$effid][$freq] = 1;
            $nprint += $npot['fq-id'][$freq][$effid];
            $freqonly[$effid] = 1;

            if ($nprint>$maxprint/3 || count($toprint)>$maxprset/3)
              break 2;
          }
        }

# then add most useful effects
# add at least one potion from each of the key effects
      $keydone = array();
        for ($r=0; ;$r++) {
          if ($r<1 && count($toprint)<$maxprset)
            $addset = 1;
          else
            $addset = 0;
          $alldone = 1;
          foreach ($npot['key-id-fq'] as $kset => $v) {
            if (isset($keydone[$kset]))
              continue;
            $alldone = 0;
            $keydone[$kset] = 1;

            $ids = array_keys($npot['key-id-fq'][$kset]);
            if (!$r)
# very first time through, want to specifically find useful effects without negatives
              usort($ids, 'sort_by_eff_score_noneg');
            else
              usort($ids, 'sort_by_eff_score');
            foreach ($ids as $effid) {
              $fqs = array_keys($npot['key-id-fq'][$kset][$effid]);
              rsort($fqs);
              foreach ($fqs as $freq) {
                if (isset($toprint[$effid]) && isset($toprint[$effid][$freq]))
                  continue;
                if (isset($skip_set[$effid][$freq]))
                  continue;
                if (!$addset && count($toprint)>$maxprset && !isset($toprint[$effid]))
                  continue;
                $keydone[$kset] = 0;
                $nprint += $npot['id-fq'][$effid][$freq];
                $toprint[$effid][$freq] = 1;
                $freqonly[$effid] = 0;
                if ($r && $nprint>$maxprint)
                  break 4;
                break 2;
              }
            }
          }
          if ($nprint>$maxprint || $alldone)
            break;
        }
      }
    }

# when I go to print, don't pay attention to whether the effect is in
# the desired list (since user doesn't know anything about it, so it
# will just be confusing to have bonus effects treated differently)
    $prgroups = array();
    $altid = array();
    foreach ($toprint as $effid => $v) {
      $es = explode('.', $effid);
      $good = $bad = array();
      foreach ($es as $e) {
        if (isset($finde[$e]))
          continue;
        if ($find_poison==$alleffs[$e]->is_poison()) {
          $prgroups[$e][] = $effid;
          $good[] = $e;
        }
        else {
          $bad[] = $e;
        }
      }
      if ((string)$baseid===(string)$effid) {
        $prgroups['base'][] = $effid;
      }
      elseif (count($good)>2) {
        $prgroups['multi'][] = $effid;
      }
      elseif(!count($good)) {
        $prgroups['other'][] = $effid;
      }
      usort($good, 'sort_by_eff_str');
      usort($bad, 'sort_by_eff_str');
      array_reverse($bad);
      $altid[$effid] = "";
      foreach ($good as $e)
        $altid[$effid] .= "+$e";
      $altid[$effid] .= ".";
      foreach ($bad as $e)
        $altid[$effid] .= "-$e";
    }

    uksort($prgroups, 'sort_by_eff_str');
  }
}

function output_potions(&$nsec, $docurr=NULL) {
  global $allow_neg, $find_poison, $exact_match, $potion_cost, $discrep;
  global $eff_to_get, $missing_eff;
  global $alleffs, $allpotions;
  global $nprint, $toprint, $prgroups, $poss_extra;
  global $npot_orig, $npot_prob, $npot_anti, $npot_anti_b, $npot;
  global $reduce_level;
  global $nings;

  $outstr = "";
  if (isset($docurr) && $docurr)
    $outstr .= '<a name="Curr"></a>'."\n";
  $outstr .= '<a name="Potions"></a><h2>('.$nsec.') Possible Potions';

  $first = 1;
  foreach ($eff_to_get as $e) {
    if (!$first)
      $outstr .= "+ ";
    else 
      $outstr .= ' for ';
    $first = 0;
    $outstr .= $alleffs[$e]->get_htmlname();
  }    
  $outstr .= "</h2>\n";

  if ($discrep && !$allow_neg) {
# No potions: potion+poison combination requested
    $outstr .= "<p>\n<strong class=\"error\">No potions are possible:</strong>\n";
    $outstr .= "You requested both a potion effect (";
    $first = 1;
    foreach ($eff_to_get as $e) {
      if (!$alleffs[$e]->is_poison()) {
        if (!$first)
          $outstr .= ", ";
	$first = 0;
	$outstr .= $alleffs[$e]->get_name();
      }
    }
    $outstr .= ") and a poison effect (";
    $first = 1;
    foreach ($eff_to_get as $e) {
      if ($alleffs[$e]->is_poison()) {
        if (!$first)
          $outstr .= ", ";
	$first = 0;
	$outstr .= $alleffs[$e]->get_name();
      }
    }
    $outstr .= ").\n";
    $outstr .= "However, you also selected that no negative side-effects are allowed in potions.\n";
    $outstr .= "These two selections are not compatible; please change one and try again.\n";
    $outstr .= "</p>\n";
  }
  elseif (!count($allpotions)) {
# No potions: any other reason
    $outstr .= "<p>\n<strong class=\"error\">No potions are possible:</strong>\n";
    $outstr .= "With your current alchemy level and current selection of ingredients,\n";
    if (!count($missing_eff)) {
      $outstr .= "no potion is possible that simultaneously provides all requested effects.\n";
    }
    else {
      $outstr .= "two ingredients are not available to provide ";
      if (count($missing_eff)>1) {
        $outstr .= "any of the following requested effects: ";
        for ($et=0; $et<count($missing_eff); $et++) {
          if ($et && count($missing_eff)>2)
            $outstr .= ", ";
	  if ($et+1==count($missing_eff))
          $outstr .= "or ";
	  $outstr .= $alleffs[$missing_eff[$et]]->get_name();
	}
        $outstr .= ".\n";
      }
      else {
        $outstr .= "the requested effect ". $alleffs[$missing_eff[0]]->get_name(). ".\n";
      }
    }
    $outstr .= "</p><p>\n";
    $outstr .= "You may want to modify your alchemy level, change the requested effects,\n";
    if (!count($missing_eff) && !$allow_neg && !$find_poison) {
# in case only possibility was eliminated because of negative side-effects
      $outstr .= "allow negative side-effects for this potion,\n";
    }
    if ($exact_match) {
      $outstr .= "allow extra effects (unset 'Exact matches only'),\n";
    }
    $outstr .= 'or <input type="submit" value="edit the ingredient list" name="show_ing" title="Open list of all ingredients that provide requested effects"></input>'. "\n";
    $outstr .= "</p>\n";
  }
  else {
# Potions found
# statistics... need to decide how/where to position this info box still, but
# for now at least get the contents printed out
    $outstr .= "<div class=\"infoblock\">\n";
    $outstr .= "<h4>Statistics:</h4>\n";
    $outstr .= "<ul>\n";
    $outstr .= "<li title=\"Total number of potions found that match criteria\">Potions found: ". count($allpotions). "</li>\n";
    $outstr .= "<li title=\"Number of potions that have been printed out\">Potions printed: $nprint</li>\n";
    $outstr .= "<li title=\"Default value in gold of any of these potions\">Potion value: <span id=\"potion_cost_b\">$potion_cost</span></li>\n";
    if (count($nings)==1) {
      $outstr .= "<li title=\"Number of ingredients used to make any of the potions found\">Required ingredients: $nings[0]</li>\n";
    }
    else {
      $outstr .= "<li title=\"Minimum number of ingredients needed to make the requested potion\">Minimum ingredients: $nings[0]</li>\n";
      $outstr .= "<li title=\"Maximum number of ingredients needed to make the requested potion\">Maximum ingredients: ". $nings[count($nings)-1]. "</li>\n";
    }
    $outstr .= "</ul>\n";
    $outstr .= "</div>\n";

    $outstr .= "<p>\n";
    if ($reduce_level==0) {
      $outstr .= count($allpotions). " potions total are possible that match your criteria.\n";    }
    else {
      $outstr .= count($allpotions). " potions were found that match your criteria.\n";
      if ($reduce_level==1) {
        $outstr .= "There are probably additional possible ingredient combinations that were\n";
        $outstr .= "skipped because they include less common ingredients or do not have\n";
        $outstr .= "useful extra effects.\n";
      }
      elseif ($reduce_level==2) {
        $outstr .= "Other ingredient combinations are possible but were\n";
        $outstr .= "skipped because so many matches were found.\n";
        $outstr .= "The potions that were skipped include less common ingredients or do not have\n";
        $outstr .= "useful extra effects.\n";
      }
      elseif ($reduce_level==3) {
        $outstr .= "Many more ingredient combinations are possible but the search was\n";
        $outstr .= "truncated because so many matches were found.\n";
        $outstr .= "It is possible that there are other useful combinations among those\n";
        $outstr .= "that were skipped.\n";
      }
    }
    if (count($allpotions)<=$nprint) {
      $outstr .= "All of these potions are listed below.\n";
    }
    else {
      $outstr .= "Only a selection ($nprint total) of these potions are listed below.\n";
      $outstr .= "The printed potions are those that use the most common ingredients and/or have\n";
      $outstr .= "the most useful additional effects.\n";
    }
    if ($reduce_level || count($allpotions)>$nprint) {
      $outstr .= "To refine your search, you may want to\n";
      $outstr .= '<input type="submit" value="edit the ingredient list" name="show_ing" title="Open list of all ingredients that provide requested effects"></input>'. "\n";
    }
    $outstr .= "</p>\n";

    if (!$npot_orig || $npot_prob==$npot_orig) {
      $outstr .= "<p>\n";
      $outstr .= "It is impossible to create this potion without negative side-effects.\n";
      if ($npot_anti) {
        $outstr .= "However, the side-effects can be counteracted in the listed potions.\n";
        if (!$allow_neg) {
          $outstr .= "These potions are listed even though you requested no negative side-effects\n";
          $outstr .= "because the side-effects have been counteracted.\n";
        }
      }
      $outstr .= "</p>\n";
    }
    elseif ($npot_anti) {
      $outstr .= "<p>\n";
      $outstr .= "Some of the potions that were found had negative side-effects.\n";
      $outstr .= "It is possible to counteract some of those side-effects, as shown\n";
      $outstr .= "in some of the listed potions.\n";
      if (!$allow_neg) {
        $outstr .= "These potions are listed even though you requested no negative side-effects\n";
        $outstr .= "because the side-effects have been counteracted.\n";
      }
      $outstr .= "</p>\n";
    }
    elseif (!$allow_neg && $npot_anti_b) {
      $outstr .= "<p>\n";
      $outstr .= "Note that some potions with negative side-effects have actually been included\n";
      $outstr .= "in the following lists, although you requested otherwise.\n";
      $outstr .= "In all cases, the negative side-effects are counteracted by other\n";
      $outstr .= "effects in the potion, and therefore were considered to be acceptable.\n";
      $outstr .= "</p>\n";
    }

    foreach ($prgroups as $prid => $v) {
      $prlabel[$prid] = "potions_";
      if (is_numeric($prid)) {
        $prlabel[$prid] .= str_replace(' ', '_', $alleffs[$prid]->get_name());
      }
      else {
        $prlabel[$prid] .= $prid;
      }
      if (!$find_poison)
        $prtitle[$prid] = "Potions";
      else 
        $prtitle[$prid] = "Poisons";
      if ($prid === 'base')
        $prtitle[$prid] .= " with Requested Effects Only";
      elseif ($prid === 'multi')
        $prtitle[$prid] .= " with Many Effects";
      elseif ($prid === 'other')
        $prtitle[$prid] = "Other ".$prtitle[$prid];
      else
        $prtitle[$prid] .= " with ". $alleffs[$prid]->get_name();
    }

    $printindex = 0;
    if (count($prgroups)>2 || (count($prgroups>1) && count($toprint)>5)) {
      $printindex = 1;
      $outstr .= "<p>The potions have been organized into the following sections:\n";
      $outstr .= "<ul>\n";
      foreach ($prgroups as $prid => $v) {
        $outstr .= "<li><a href=\"#$prlabel[$prid]\">";
        $outstr .= $prtitle[$prid];
        $outstr .= "</a></li>\n";
      }
      $outstr .= "</ul></p>\n";
    }

# remove any entries from poss_extra that are already being
# printed out
    foreach ($poss_extra as $e => $v) {
      if (isset($prgroups[$e])) {
        unset($poss_extra[$e]);
      }
    }
    if (count($poss_extra)) {
      $outstr .= "<p>\n";
      if (count($poss_extra)==1) {
        if ($printindex)
          $outstr .= "One more useful effect that could be added to this potion is\n";
        else
          $outstr .= "One useful effect that could be added to this potion is\n";
      }
      else {
        if ($printindex)
          $outstr .= "Other useful effects that could be added to this potion include:\n<ul>\n";
        else
          $outstr .= "Useful effects that could be added to this potion include:\n<ul>\n";
      }
      foreach ($poss_extra as $e => $v) {
        if (count($poss_extra)>1)
          $outstr .= "<li> ";
        $outstr .= '<input type="submit" value="'. $alleffs[$e]->get_name();
        $outstr .= $alleffs[$e]->get_textstrength(0);
        $outstr .= '" name="set_eff_'. join($eff_to_get, '_'). "_$e". '"';
        $outstr .= ' title="Add '.$alleffs[$e]->get_name().' to list of requested effects and generate new list of potions"';
        $outstr .= '></input>';
        if (count($poss_extra)>1)
          $outstr .= "</li>";
        $outstr .= "\n";
      }
      if (count($poss_extra)>1)
        $outstr .= "</ul>";
      $outstr .= "</p>\n";
    }

    $button_shown = 0;
    $nsub = 'a';
    foreach ($prgroups as $prid => $v) {
      usort($prgroups[$prid], 'sort_by_altid');
      $outstr .= "<h3 class=\"potion-header\"><a name=\"$prlabel[$prid]\">($nsec$nsub) ";
      $outstr .= $prtitle[$prid];
      $outstr .= "</a>\n";
      $outstr .= "<span class=\"rightalign_small\"><a href=\"#Potions\">Top of Potion List</a></span></h3>\n";
      $nsub++;
      
      $ntot = $ncurr = 0;
      foreach ($prgroups[$prid] as $effid) {
        $ntot += $npot['effid'][$effid];
        foreach ($npot['id-fq'][$effid] as $freq => $v) {
          if (isset($toprint[$effid][$freq]))
            $ncurr += $npot['id-fq'][$effid][$freq];
        }
      }
      $outstr .= "<p>\n";
      if ($ncurr < $ntot)
        $outstr .= "$ncurr of the $ntot potions found in this category are being shown.\n";
      elseif ($ntot>1)
        $outstr .= "All of the $ncurr potions found in this category are being shown.\n";
      else
        $outstr .= "The only potion found in this category is being shown.\n";
      $outstr .= "</p>\n";

      foreach ($prgroups[$prid] as $effid) {
        $outstr .= print_effid($effid, $prid);
      }

      $button_shown = 0;
      if ($ncurr < $ntot || $reduce_level || (count($eff_to_get)==1 && count($prgroups>1))) {
        if (is_numeric($prid)) {
          $outstr .= "<input type=\"submit\" value=\"To view more potions with this set of effects\" ";
          $outstr .= "name=\"set_eff_". join($eff_to_get, '_'). "_$prid\"></input>\n";
          $button_shown = 1;
        }
        elseif ($prid === 'base' && count($prgroups)>1) {
          $outstr .= "<input type=\"submit\" value=\"To view potions with only this exact set of effects\" ";
          $outstr .= "name=\"exact_eff_". join($eff_to_get, '_'). "_$prid\"></input>\n";
          $button_shown = 1;
        }
      }

      if ($button_shown) {
        $outstr .= "<span class=\"rightalign\">\n";
        $outstr .= '<input type="submit" value="Show Ingredient List" name="show_ing"></input>'. "\n";
        $outstr .= '<input type="submit" value="Show Recipe List" name="show_recipe"></input>'. "\n";
        $outstr .= '<input type="submit" value="Update Page" name="submit-potions"></input>'. "\n";
        $outstr .= "</span>\n";
      }
    }
    if (!$button_shown) {
      $outstr .= '<input type="submit" value="Show Ingredient List" name="show_ing"></input>'. "\n";
      $outstr .= '<input type="submit" value="Show Recipe List" name="show_recipe"></input>'. "\n";;
      $outstr .= '<input type="submit" value="Update Page" name="submit-potions"></input>'. "\n";;
    }
  }

  return $outstr;
}

function find_potions($elist, $opt, $ilist=NULL, $j0=0, $curr_effs=NULL) {
# This has turned into a pretty long and ugly function, but
# it's necessary to avoid looking for useless recipes
  global $allings, $alleffs;
  global $my_eff_to_get, $extraeffs, $extraeffs_lim, $possible_ings;
  global $alchemy;
  global $allpotions, $reduce_level;
  global $poteffs, $keyeffs, $eff_score_con, $eff_score_pro;
  global $finde;
  global $find_poison, $allow_neg, $exact_match;
  global $freqmax, $baseid;

  $nfound = 0;
  if (count($allpotions)>MAXPOTION*1.5) {
    $reduce_level = 3;
    return $nfound;
  }

  if (!is_array($elist)) {
    $elist = array($elist);
  }
  if (is_null($ilist))
    $ilist = array();
  if (is_null($curr_effs)) {
    $curr_effs = array();
    foreach ($ilist as $i)
      $allings[$i]->add_effs($curr_effs);
  }

  while (count($elist) &&
	 isset($curr_effs[$elist[0]]) &&
	 $curr_effs[$elist[0]]>=2) {
    array_shift($elist);
    $j0 = 0;
  }

  if (!count($elist))
    return 0;

  $e = $elist[0];
  $ni = count($ilist);
  if (!isset($possible_ings[$e])) {
    $possible_ings[$e] = $alleffs[$e]->set_possible_ings(0);
    if (count($possible_ings[$e])<2)
      return 0;
  }

  $freqbase = 100;
  foreach ($ilist as $i)
    $freqbase -= pow(5-$allings[$i]->get_freq(),2);

# determine whether or not check do master-level (one-ingredient) potions
# - only done first time through (opt==0)
# - only done on first ingredient (ni==0)
# - only done if master-level (alchemy>=100)
# - only done if only one effect is being looked for (count($elist)==1)
  $do_master = ($opt==0  && $ni==0 && $alchemy>=100 && count($elist)==1);

  for ($j=$j0; $j<count($possible_ings[$e]); $j++) {
    $i = $possible_ings[$e][$j];

    $ilist[$ni] = $i;
    $cet = $curr_effs;
    $allings[$i]->add_effs($cet);
    if ($do_master)
      $master_e = $allings[$i]->master_eff();

# decide 
# (a) is this recipe complete?
# (b) if it is complete, is it worth adding it?
# (c) is it worth adding extra ingredients?
    $save = 1;
    $cont = 1;
    if ($ni>=3)
      $cont = 0;

    if ($ni<1) {
      if ($do_master && $master_e==$elist[0]) {
# master-level potion matches requested effect
	$save = 1;
      }
      else {
        $save = 0;
      }
    }
    else {
      foreach ($elist as $et) {
# This is all that's needed to figure out if the recipe is complete
# The harder question is whether it's worth adding it
        if (!isset($cet[$et]) || $cet[$et]<2)
          $save = 0;
      }
    }
    if ($save || $cont) {      
      $sc_con = 0;
      $sc_pro = 0;
      $keyid = $possid = $effid = "";
      $done = array();
# set up ID to list effects in order
# a) requested effects
# b) related effects
# c) unrelated effects, same sign
# d) unrelated effects, opposite sign

# am I really using scores at this point??
# the scores are getting shifted in this rewrite, but it should be
# a uniform set of shifts
      foreach ($my_eff_to_get as $et) {
        if ((!isset($cet[$et]) || $cet[$et]<2) && (!$do_master || $master_e != $et))
          continue;
        $done[$et] = 1;
        $effid .= ".$et";
	$sc_pro += 100;
      }
# possid represents what might be possible if I build on this recipe
# BUT it shouldn't add in effects with no pairs... unless opt>0, in
# which case previous pairs list is no longer valid
      $ex_effids = array();
      if (!$opt)
        $exlist = $extraeffs_lim;
      else
        $exlist = $extraeffs;
      foreach ($exlist as $et => $v) {
        if (!isset($cet[$et]) || isset($done[$et]))
          continue;
        if ($cet[$et]<2) {
          if ($alleffs[$et]->is_ing($i))
            $possid .= ".$et";
          continue;
        }
        if ($exact_match) {
          $cont = $save = 0;
          break;
        }
        $done[$et] = 1;
        $effid .= ".$et";
        $possid .= ".$et";
        $ex_effids[] = $et;
        $sc_pro += 10*$extraeffs[$et];
      }
# get rid of leading dot before copying value
      if ($effid!="")
        $effid = substr($effid, 1);
# save "key" values (i.e., based only upon requested effects and
# related effects)
      $sc_key = $sc_pro;
      $keyid = $effid;
      $echk = array_keys($cet);
      sort($echk);
      foreach ($echk as $et) {
        if (!isset($cet[$et]) || $cet[$et]<2 || isset($done[$et]))
          continue;
        if ($exact_match) {
          $cont = $save = 0;
          break;
        }
        if ($alleffs[$et]->is_poison()!=$find_poison)
          continue;
        $done[$et] = 1;
        $effid .= ".$et";
        $sc_pro ++;
      }
      $antiused = array();
      foreach ($echk as $et) {
        if (!isset($cet[$et]) || $cet[$et]<2 || isset($done[$et]))
          continue;
        if ($exact_match) {
          $cont = $save = 0;
          break;
        }
#OK, now all we're left with are the side-effects
# Are they acceptable side-effects?
        $effid .= ".$et";

	$ok = 0;
	if ($find_poison) {
# if I'm trying to make a poison it cannot include any good effects
          $save = 0;
          $cont = 0;
        }
        elseif (isset($cet[$alleffs[$et]->get_anti()]) &&
                $cet[$alleffs[$et]->get_anti()]>=2 &&
                !isset($antiused[$alleffs[$et]->get_anti()])) {
# if this is a negative sideffect, with a counteracting postive
# effect that has been included in the potion, the potion is actually OK
          $ok = 1;
          $antiused[$alleffs[$et]->get_anti()] = 1;
        }
        elseif (!$allow_neg &&
                ($opt>1 || $alleffs[$et]->get_poison()!=2 || count($ilist)==4)) {
# I'm making a potion and allow_neg is 0
# note that some sideeffects are being allowed through at $opt=0, $opt=1
#  (later check whether they can be counteracted... but only if more
#   ingredients can be added)
          $save = 0;
        }
        elseif ($opt==1) {
# if I'm specifically looking to counter negatives right now, don't save
# anything that adds more negatives
          $save = 0;
        }
	if (!$ok) {
	  $sc_con += 10*max(1,$alleffs[$et]->get_poison());
        }
        else {
# Still give a small negative even if the effect has been counteracted
# (Since it is worse than a potion with no negative at all)
	  $sc_con ++;
        }
      }
    }
    if ($save && !$do_master)
      $cont = 0;

    if (count($allpotions)>MAXPOTION)
      $reduce_level = 2;

    if ($cont && $reduce_level) {
      $possid = $baseid . $possid;
      if (isset($freqmax[$possid])) {
# This is the maximum possible frequency if I continue with this
# recipe
        $freqtot = $freqbase - pow(5-$allings[$i]->get_freq(),2);
# Only at minimal reduce level: skip if unlikely to be an improvement
        if ($reduce_level==1 &&
            $freqmax[$possid]>=$freqtot+FREQDIFF/2)
          $cont = 0;
# More aggressive reductions needed: skip if it's not going to be an
# improvement over existing recipes
        if ($reduce_level==2 &&
            ($freqmax[$possid]>$freqtot || $sc_con>=10))
          $cont = 0;
      }
    }

    if ($save) {
      if ($opt==3)
	return ++$nfound;

# calculate statistics specific to this set of ingredients
      $ingid = join($ilist, '.');
# freqtot is set up to have a maximum value of 100 (if all ingredients have a value of 5)
# it then goes down for each ingredient below 5, with very large
# deductions for particularly small freqs
# minimum value of 0, if all 4 ingredients are quest-specific
      $freqtot = $freqbase - pow(5-$allings[$i]->get_freq(),2);
      if ($reduce_level==1 &&
          isset($freqmax[$keyid]) &&
          $freqmax[$keyid]>=$freqtot+FREQDIFF)
        $save = 0;
      if ($reduce_level==2 &&
          isset($freqmax[$keyid]) &&
          ($freqmax[$keyid]>$freqtot || $sc_con>=10))
        $save = 0;
    }
# OK, I'm actually going to save this recipe
    if ($save && $opt<=2) {
      $nfound++;
      $freqmax[$keyid] = isset($freqmax[$keyid]) ? max($freqmax[$keyid], $freqtot) : $freqtot;
      $freqmax[$baseid] = isset($freqmax[$baseid]) ? max($freqmax[$baseid], $freqtot) : $freqtot;
      if (count($ex_effids)>1) {
        foreach ($ex_effids as $et) {
          $tid = $baseid . '.' . $et;
          $freqmax[$tid] = isset($freqmax[$tid]) ? max($freqmax[$tid], $freqtot) : $freqtot;
        }
      }

      $allpotions[$ingid] = new Potion($ingid, $ilist, $freqtot, $effid, $keyid,
	                               $sc_pro, $sc_con, $sc_key);
    }

    if ($cont) {
      $nfound += find_potions($elist, $opt, $ilist, $j+1, $cet);
      if ($opt==3)
	return $nfound;
      if ($reduce_level>=3)
        return $nfound;
    }
  }
  return $nfound;
}

# all initialization related to effect strengths
function setup_ingreds() {
  global $new_sess, $do_ingred, $badinput;
  global $allings, $custom_use, $custom_freq;

  $do_ingred = 1;

  if ($new_sess) {
# new session: provide default values for variables
    for ($i=0; $i<count($allings); $i++)
      $allings[$i]->reset_custom($custom_use[$i], $custom_freq[$i]);
  }

  if (isset($_SERVER['REQUEST_METHOD']) &&
      $_SERVER['REQUEST_METHOD'] == 'POST') {

    if (isset($_POST['do_ingred'])) {
      $do_ingred = (bool)$_POST['do_ingred'];
    }
    if (!$do_ingred)
      return;

# customized ingredients:
#  custom_use is integer, 0-2
#  custom_freq is integer, 0-5
# if there is bad input data for custom_use, ignore it: only possible if user
#   is not using form
# if there is bad input data for custom_freq, provide error message
    if (isset($_POST['default_ing']) &&
        $_POST["default_ing"]=="Reset Ingredients to Defaults") {
      for ($i=0; $i<count($allings); $i++)
        $allings[$i]->reset_custom($custom_use[$i], $custom_freq[$i]);
    }
    else {
# Occasionally, all values of custom_use get set to a single value
# I'm not sure what causes this yet
# It seems to actually be a browser issue rather than a server issue:
# The HTML shows the correct option being selected, but then it shows up
#  incorrectly
      for ($i=0; $i<count($allings); $i++) {
        if (isset($_POST["custom_use_$i"]) &&
	    ctype_digit($_POST["custom_use_$i"]) &&
            $_POST["custom_use_$i"]<=2 &&
            $custom_use[$i]!=intval($_POST["custom_use_$i"])) {
          $custom_use[$i] = intval($_POST["custom_use_$i"]);
        }
        if (isset($_POST["custom_freq_$i"]) &&
	    ctype_digit($_POST["custom_freq_$i"]) &&
            $_POST["custom_freq_$i"]<=5 &&
            $custom_freq[$i]!=intval($_POST["custom_freq_$i"])) {
          $custom_freq[$i] = intval($_POST["custom_freq_$i"]);
        }
        elseif (isset($_POST["custom_freq_$i"]) &&
	        !(ctype_digit($_POST["custom_freq_$i"]) &&
	          $_POST["custom_freq_$i"]<=5)) {
          $badinput['custom_freq'][$i] = 1;
        }
      }
    }
  }
  elseif (!$new_sess) {
# turn off any must-use ingredients
    for ($i=0; $i<count($allings); $i++) {
      if ($custom_use[$i]>1)
        $custom_use[$i] = 1;
    }
  }
}

function output_ingreds(&$nsec, $docurr=NULL) {
  global $neff, $eff_to_get, $possible_ings;
  global $allings, $alleffs;
  global $toprint, $poteffs, $allpotions;

  $outstr = "";
  if (isset($docurr) && $docurr)
    $outstr .= '<a name="Curr"></a>'."\n";
  $outstr .= '<a name="Ingredients"></a><h2>('.$nsec.') Filter Ingredients</h2>'."\n";
  $outstr .= <<< EOF1
Use this table to control which ingredients will be used in your potions:
<ul>
<li> <b>Y</b>es: Select to require that this ingredient must be used in all potions.</li>
<li> <b>M</b>aybe: This ingredient may be used in a potion (default setting).</li>
<li> <b>N</b>ever: Select to prevent this ingredient from ever being used in a potion.</li>
<li> <b>F</b>requency: This is an integer from 0 to 5 indicating whether the ingredient is commonly available.  Recipes will preferentially use ingredients with the highest frequency.  0 is reserved for quest-specific ingredients; 1 is used for rare ingredients.</li>
</ul>
<table border="1" cellspacing="0" class="ingtable">
<tr>
<th>Ingredients</th>
<th>Y</th>
<th>M</th>
<th>N</th>
<th>F</th>
<th colspan="4">Effects</th>
<th>Weight</th>
<th>Price</th>
</tr>
EOF1;

  $shown = array();
  $currlist = array();
  for ($f=0; $f<$neff; $f++) {
    $e = $eff_to_get[$f];
    for ($i=0; $i<count($possible_ings[$e]); $i++) {
      if ($allings[$possible_ings[$e][$i]]->get_score_pro()>=200) {
        $currlist[] = $possible_ings[$e][$i];
      }
    }
  }
  if (count($currlist)) {
    sort($currlist);
    $outstr .= "<tr><td colspan=\"11\" class=\"potionheading\"><center>Ingredients with Multiple Requested Effects</center></td></tr>\n";
    foreach ($currlist as $i) {
      $outstr .= $allings[$i]->print_custom(2);
      $shown[$i] = 1;
    }
  }
  for ($f=0; $f<$neff; $f++) {
    $currlist = array();
    $e = $eff_to_get[$f];
    for ($i=0; $i<count($possible_ings[$e]); $i++) {
      if (isset($shown[$possible_ings[$e][$i]]))
        continue;
      $currlist[] = $possible_ings[$e][$i];
    }
    if (count($currlist)) {
      sort($currlist);
      $outstr .= "<tr><td colspan=\"11\" class=\"potionheading\"><center> Available Ingredients with Effect <b>". $alleffs[$e]->get_name(). "</b></center></td></tr>\n";
      foreach ($currlist as $i) {
        $outstr .= $allings[$i]->print_custom(1);
        $shown[$i] = 1;
      }
    }
  }
  for ($f=0; $f<$neff; $f++) {
    $currlist = $alleffs[$eff_to_get[$f]]->get_unshown($shown);
    if (count($currlist)) {
      sort($currlist);
      $outstr .= "<tr><td colspan=\"11\" class=\"potionheading\"><center>Other Ingredients with Effect <b>". $alleffs[$eff_to_get[$f]]->get_name(). "</b> (not available)</center></td></tr>\n";
      foreach ($currlist as $i) {
        $outstr .= $allings[$i]->print_custom(0);
        $shown[$i] = 1;
      }
    }
  }
# Add in any other ingredients that are being shown in potion list
# (others may be added when counteracting effects, or looking for bonus
#  effects)
  $currlist = array();
  if (count($toprint)) {
    foreach ($toprint as $effid => $v1) {
      foreach ($toprint[$effid] as $freq => $v2) {
        foreach ($poteffs[$effid][$freq] as $ingid => $v3) {
          foreach ($allpotions[$ingid]->get_ings() as $i) {
            if (isset($shown[$i]))
              continue;
            $currlist[] = $i;
            $shown[$i] = 1;
          }
        }
      }
    }
  }
  if (count($currlist)) {
    sort($currlist);
    $outstr .= "<tr><td colspan=\"11\" class=\"potionheading\"><center>Other Ingredients used in Displayed Potions</center></td></tr>\n";
    foreach ($currlist as $i) {
      $outstr .= $allings[$i]->print_custom(0);
      $shown[$i] = 1;
    }
  }

# If no effect has been selected, just show the entire list
  if (!$neff) {
    $outstr .= "<tr><td colspan=\"11\" class=\"potionheading\"><center>Available Ingredients</center></td></tr>\n";
    foreach ($allings as $i => $v) {
      if ($allings[$i]->is_avail())
        $outstr .= $allings[$i]->print_custom(0);
    }
    $outstr .= "<tr><td colspan=\"11\" class=\"potionheading\"><center>Unavailable Ingredients</center></td></tr>\n";
    foreach ($allings as $i => $v) {
      if (!$allings[$i]->is_avail())
        $outstr .= $allings[$i]->print_custom(0);
    }
  }

  $outstr .= <<< EOF2
</table>
<input type="submit" value="Update Page" name="submit-ings"></input>
<input type="submit" value="Close Ingredient List" name="close_ing"></input>
<input type="submit" value="Reset Ingredients to Defaults" name="default_ing"></input>
EOF2;
  
  return $outstr;
}

# all initialization related to adding recipes
function setup_recipes() {
  global $new_sess, $badinput, $curr_link, $show_section;
  global $recipes, $recipes_to_del, $recipes_to_add;

  if ($new_sess) {
# new session: provide default values for variables
    $recipes = array();
  }

  $recipes_to_del = $recipes_to_add = array();

  if (isset($_SERVER['REQUEST_METHOD']) &&
      $_SERVER['REQUEST_METHOD'] == 'POST') {
# recipe names: Allow pretty much any standard character
# Recipe class is responsible for any error checking
#   error message if name is not OK
#   (although currently class isn't error checking, just encoding)
    for ($n=0; $n<count($recipes); $n++) {
      if (!isset($recipes[$n]) || !$recipes[$n]->is_valid())
        continue;
      if (isset($_POST["recipe_name_$n"])) {
        if (!$recipes[$n]->set_name($_POST["recipe_name_$n"])) {
          $badinput['recipe_name'][] = $n;
        }
      }
      if (isset($_POST["recipe_del_$n"]) && (bool)$_POST["recipe_del_$n"]) {
        $recipes_to_del[] = $n;
      }
    }

# adding new recipes
# uses POST variable name of form add_recipe_[\-\d\_x]*
# for now assume anything with that format is OK
# ignore anything else, since it is only possible if user not using form
    foreach ($_POST as $key => $val) {
# check for a variable named "add_recipe_"
      if (substr($key,0,11)=="add_recipe_") {
        $inglist = substr($key,11);
        if (preg_match('/^[\-\d\_x]*$/', $inglist)) {
          $recipes[] = new Recipe($inglist, $key);
          $recipes_to_add[] = count($recipes)-1;
          $show_section['recipe'] = 1;
# Make display jump to recipe section... this might override other requests
# so may want to eventually do something fancier
          $curr_link = "Recipes";
        }
      }
    }
  }
}

function output_recipe_header(&$nsec, $docurr=NULL, $doonly=NULL) {
global $neff, $eff_to_get;
  $outstr = "";
  if (isset($docurr) && $docurr)
    $outstr .= '<a name="Curr"></a>'."\n";
  if (isset($doonly) && $doonly) {
    if ($doonly==1) {
// if this is the header for a "recipe_only" call, need to have a return
// button
      $outstr .= "<form name=\"mainform\" action=\"".$_SERVER['PHP_SELF']."#Curr\" method=\"POST\">\n";
      for ($j=0; $j<$neff; $j++) {
        $outstr .= "<input type=\"hidden\" value=\"".$eff_to_get[$j];
        $outstr .= "\" name=\"effect$j\"></input>\n";
      }
      $outstr .= "<span class=\"rightalign\">";
      $outstr .= "<input type=\"submit\" value=\"Return to Full Page\" name=\"show_recipe\"></input>";
      $outstr .= "</span></form>\n";
    }
  }
  else {
// otherwise, want a button to do a recipe_only call
    $outstr .= "<span class=\"rightalign\">";
    $outstr .= "<input type=\"submit\" value=\"Show Recipes Only (for printing)\" name=\"recipe_only\"></input>";
    $outstr .= "</span>\n";
  }
  $outstr .= '<a name="Recipes"></a><h2>('.$nsec.') Recipes</h2>'."\n";
  return $outstr;
}

function output_recipe_footer() {
  $outstr = "";
  $outstr .= <<< EOF3
<div class="recipe_footer">
<br />
<input type="submit" value="Update Page" name="submit-recipe"></input>
<input type="submit" value="Close Recipe List" name="close_recipe"></input>
</div>
EOF3;
  return $outstr;
}

function output_recipes(&$nsec, $docurr=NULL, $doonly=NULL) {
  global $recipes;

  $outstr = output_recipe_header($nsec, $docurr, $doonly);

  for ($r=0, $rout=1; $r<count($recipes); $r++) {
    if (!isset($recipes[$r]) || !$recipes[$r]->is_valid())
      continue;
    $outstr .= $recipes[$r]->print_recipe($r, $rout, $doonly);
    $rout++;
  }
  if ($rout==1) {
    $outstr .= "<p>No recipes have been saved.</p>\n";
  }

  $outstr .= output_recipe_footer();
  return $outstr;
}

function print_effid($effid, $prid) {
  global $toprint, $poteffs, $finde;
  global $allpotions;
  global $altid;
  global $alleffs, $allings, $eff_to_get;
  $outstr = "";

  $fqmin = 1000;
  $fqmax = -1000;
  foreach ($toprint[$effid] as $freq => $v) {
    $fqmin = min($fqmin, $freq);
    $fqmax = max($fqmax, $freq);
  }

  $es = preg_split('/\.\-|\+|\-|\./', $altid[$effid]);
  if (is_numeric($prid))
    array_unshift($es, $prid);
  $es = array_merge($eff_to_get, $es);
  $first = 1;
  $done = array();
  foreach ($es as $e) {
    if ($e==="")
      continue;
    if (isset($done[$e]))
      continue;
    if (!$first)
      $outstr .= " + ";
    $first = 0;
    if (isset($finde[$e]))
      $outstr .= $alleffs[$e]->get_htmlname();
    else
      $outstr .= $alleffs[$e]->get_htmlname(NULL, NULL, 1);
    $done[$e] = 1;
  }
#  $outstr .= " ($fqmin-$fqmax)<br />\n";

  $outstr .= "<table class=\"potiontable\">\n";
  $allids = array();
  foreach ($toprint[$effid] as $freq => $v) {
    $allids = array_merge($allids, array_keys($poteffs[$effid][$freq]));
  }
  for ($n=1; $n<=4; $n++) {
    $currids = array();
    foreach ($allids as $ingid) {
      if ($allpotions[$ingid]->get_ning()==$n)
        array_push($currids, $ingid);
    }
    if (!count($currids))
      continue;
    $outstr .= print_effid_ings($currids, $n);
  }
  $outstr .= "</table>\n";

  return $outstr;
}

# take a set of recipes that provide the same effects with same number
#  of ingredients
# group those recipes as much as possible (e.g., find common ingredients)
# print out table of the recipes
function print_effid_ings($ingids, $ning) {
  global $req_ings, $nuse;
  global $allings, $allpotions;

  $outstr = "";

  $nset = 0;
  $slot[0] = $curr = array();
  $skip_ings = array();
# put any required ingredients at the front of the list
# (since they appear everywhere, they should get peeled off right away...
#  but pulling them out right away ensures that they are always in the
#  same positions)
  foreach ($req_ings as $i) {
    $slot[$nset][] = array($i);
    $skip_ings[$i] = 1;
  }
  foreach ($ingids as $ingid) {
    $s = isset($curr[$nset]) ? count($curr[$nset]) : 0;
    foreach ($allpotions[$ingid]->get_ings() as $i) {
      if (isset($skip_ings[$i]))
        continue;
      $curr[$nset][$s][$i] = 1;
    }
  }

  $nset = 0;
  while (1) {
    if ($nset>=count($curr))
      break;
    $add_nset = 1;

    $nuse = array();
    for ($c=0; $c<count($curr[$nset]); $c++) {
      foreach ($curr[$nset][$c] as $i => $v) {
        increment_array($nuse,1,$i);
      }
    }

    $use_is = array_keys($nuse);
    usort($use_is, 'sort_by_nuse');

    for ($j=0; $j<count($use_is); $j++) {
      $i = $use_is[$j];
      if ($nuse[$i]==count($curr[$nset])) {
# this ingredient is used in all current recipes
        $s = count($slot[$nset]);
        $slot[$nset][$s] = array($i);
        for ($c=0; $c<count($curr[$nset]); $c++) {
          unset($curr[$nset][$c][$i]);
        }
        continue;
      }
      elseif (count($slot[$nset])+1==$ning) {
# only one slot left to be filled... all remaining ingredients belong there
        $s = count($slot[$nset]);
        $slot[$nset][$s] = array_slice($use_is, $j);
        break;
      }

# OK, easy cases have been peeled off now
# With short recipe lists, the remainder of this section may never be needed

      $chk2 = array();
# First see if this an "any 2 of the following ingredients" case
      if (count($slot[$nset])==$ning-2) {
#if (0) {
# Only appropriate if there are only two slots left to fill
# Come of with quick list of possible ingredients
        for ($jt=$j; $jt<count($use_is); $jt++) {
# Each possible ingredient must be used enough times
          if ($nuse[$use_is[$jt]]<count($chk2)-1)
            break;
# There have to be enough potions in this set to allow all possible
# combinations
          $nneed = (count($chk2)+1)*count($chk2)/2;
          if ($nneed>count($curr[$nset]))
            break;

          $chk2[$use_is[$jt]] = 1;
        }
      }

### need to also possibly allow for overlap... which means taking into
### account combos that have already been cleared
      while (count($chk2)>=$nuse[$i]/2) {
# Now check that all possible combinations of these really are included
        $nchk2 = array();
        $matches = array();
        $unmatches = array();
        for ($c=0; $c<count($curr[$nset]); $c++) {
          $is = array_keys($curr[$nset][$c]);
          if (isset($is[0]) && isset($chk2[$is[0]]) && isset($is[1]) && isset($chk2[$is[1]])) {
            $matches[] = $c;
            increment_array($nchk2,1,$is[0]);
            increment_array($nchk2,1,$is[1]);
          }
          else {
            $unmatches[] = $c;
          }
        }

        $nchkmin = 1000;
        $ichkmin = NULL;
        $nchkmax = 0;
        foreach ($nchk2 as $it => $vt) {
          if ($nchk2[$it]<$nchkmin) {
            $nchkmin = $nchk2[$it];
            $ichkmin = $it;
          }
          if ($nchk2[$it]>$nchkmax) {
            $nchkmax = $nchk2[$it];
          }
        }
        if ($nchkmin < $nchkmax) {
# they don't all have the same number of uses... delete the one with
# the fewest uses and try again
          unset($chk2[$ichkmin]);
        }
        else {
# nchkmin=nchkmax... looks like a good set so break to next set of processing
          break;
        }
      }

# we should now have a good set, but do a last few sanity checks
# if it fails any of these, it will just fall through to the next
# search for matches
      if (count($chk2)>=$nuse[$i]/2 &&
# no point if it's just any 2 of following 2
          count($chk2)>2 &&
# sanity checks: numbers all add up as expected
          count($chk2)==count($nchk2) &&
          $nchkmax == count($nchk2)-1 &&
          count($matches) == ($nchkmax*($nchkmax+1)/2) &&
# only do any 2 of following 3 if it uses up final entries
          (count($chk2)>3 || count($curr[$nset])==3)) {

        if (count($unmatches)) {
# any potions that don't fit "any 2 of x" get moved to next nset
          array_splice($slot, $nset+1, 0, array($slot[$nset]));
          array_splice($curr, $nset+1, 0, array(array()));
          $c = 0;
          foreach ($unmatches as $c0) {
# can't just use $c=count($curr[$nset+1]) here, because array_splice
# creates an empty entry at 0, which needs to be overwritten, not added after
            $curr[$nset+1][$c++] = $curr[$nset][$c0];
          }
        }

        $s = count($slot[$nset]);
#### need to set up printing to recognize -1!
        $slot[$nset][$s++] = array(-1);
        $slot[$nset][$s] = array_keys($chk2);
        sort($slot[$nset][$s]);          

# this nset has now been finished, break out of j-loop and proceed to
# next nset
        $add_nset = 1;
        break;
      }

# Check to see if there are ingredients that match this one

# Make up list of ingredients that need to be in other recipes
      $ids = array();
      $nt = $to_match = array();
      for ($c=0; $c<count($curr[$nset]); $c++) {
        $is = array_keys($curr[$nset][$c]);
        sort($is);
        $id = join($is, '.');
        $ids[$id] = 1;
        if (isset($curr[$nset][$c][$i])) {
          $t = count($to_match);
          foreach ($curr[$nset][$c] as $ib => $vb) {
            if ($ib!=$i)
              $to_match[$t][$ib] = 1;
          }
        }
      }

# find other recipes that also have the to_match list of ingredients
      $itmax = NULL;
      $nmmax = 0;
      $nmatch = array();
      for ($c=0; $c<count($curr[$nset]); $c++) {
# skip any recipe that includes the target ingredient, $i
        if (isset($curr[$nset][$c][$i]))
          continue;
        for ($t=0; $t<count($to_match); $t++) {
          $ex = array();
          foreach ($curr[$nset][$c] as $ib => $v) {
            if (!isset($to_match[$t][$ib]))
              $ex[] = $ib;
          }
          if (count($ex)==1) {
            $nmatch[$ex[0]][$t] = 1;
            $nm = count($nmatch[$ex[0]]);
            if ($nm>$nmmax) {
              $nmmax = $nm;
              $itmax = $ex[0];
            }
            increment_array($nt,1,$t);
# don't want to break yet... one recipe could match multiple to_match sets
          }
        }
      }

      if ($nmmax>=2 && $nmmax>count($to_match)/2) {
        $nmmin = $nmmax;
        $itmin = $itmax;
        foreach ($nmatch as $it => $v) {
          $nm = count($nmatch[$it]);
          if ($nm<2 || $nm<$nmmax-2) {
             unset($nmatch[$it]);
          }
          else {
            $no = 0;
            foreach ($nmatch[$it] as $it2 => $v2) {
              if (isset($nmatch[$itmax][$it2]))
                $no++;
              else {
                unset($nmatch[$it][$it2]);
              }
            }
            if ($no<2 || $no<$nmmax-2) {
              unset($nmatch[$it]);
            }
            elseif ($no<$nmmin) {
              $nmmin = $no;
              $itmin = $it;
            }
          }
        }

# $nmatch[$itmin] should now contain a list of $to_match indices that match everywhere
# $nmatch should contain a list of the ingredients comparable to $i
        $newset = array();
        $error = 0;
        foreach ($nmatch[$itmin] as $t => $v) {
          $is = array_keys($to_match[$t]);
          $s = count($newset);
          $newset[$s] = $to_match[$t];
          $its = array_merge(array($i), array_keys($nmatch));
          sort($its);
          foreach ($its as $it) {
            $newis = array_merge(array($it), $is);
            sort($newis);
            $newid = join($newis, '.');
            if (isset($ids[$newid])) {
              unset($ids[$newid]);
            }
            else {
              $error = 1;
              break 2;
            }
          }
        }
      }
      else {
        $error = 1;
      }

      if (!$error) {
# a set of similar ingredients has been found... separate them out
        if (count($ids)) {

          array_splice($slot, $nset+1, 0, array($slot[$nset]));
          array_splice($curr, $nset+1, 0, array(array()));
          $c = 0;
          foreach ($ids as $id => $v) {
# can't just use $c=count($curr[$nset+1]) here, because array_splice
# creates an empty entry at 0, which needs to be overwritten, not added after
            $is = explode('.', $id);
            foreach ($is as $it) {
              $curr[$nset+1][$c][$it] = 1;
            }
            $c++;
          }
        }

        $curr[$nset] = $newset;
        $s = count($slot[$nset]);
        $slot[$nset][$s] = array_merge(array($i), array_keys($nmatch)); 
        sort($slot[$nset][$s]);

# basically want to finish processing this value of $nset, i.e. redo loop
        $add_nset = 0;
        break;
      }

# no similar ingredients found: split off a single ingredient
      array_splice($slot, $nset+1, 0, array($slot[$nset]));
      $s = count($slot[$nset]);
      $slot[$nset][$s] = array($i);

      array_splice($curr, $nset+1, 0, array(array()));
      $cn = $c = 0;
      while (1) {
         if ($c>=count($curr[$nset]))
           break;
	 $add_c = 1;
         if (isset($curr[$nset][$c][$i])) {
            unset($curr[$nset][$c][$i]);
         }
         else {
           $curr[$nset+1][$cn] = $curr[$nset][$c];
           array_splice($curr[$nset], $c, 1);
           $add_c = 0;
           $cn++;
         }
         if ($add_c)
           $c++;
      }
      $add_nset = 0;
      break;
    }
    if ($add_nset)
      $nset++;
  }

#OK, now I should have a list of ingredients to print in $slot

#first, resort each slot to be in alphabetical order
#(need to do all before I start so comparisons work)
  $countmax = 0;
  for ($nset=0; $nset<count($slot); $nset++) {
    for ($s=0; $s<count($slot[$nset]); $s++) {
      sort($slot[$nset][$s]);
      $countmax = max($countmax, count($slot[$nset][$s]));
    }
  }

  $domerge = 1;
# if some of the entries are particularly long, don't bother to try to
# merge, because the tables become very hard to read, and prone to
# browser display problems
  if ($countmax>12)
    $domerge = 0;
  $merge = array();
  for ($nset=0; $nset<count($slot); $nset++) {
    $outstr .= "<tr>\n";
    $recipeid = "";
    $do2col = 0;
    for ($s=0; $s<count($slot[$nset]); $s++) {
      if ($s)
        $recipeid .= "x";
      $recipeid .= join($slot[$nset][$s], "_");
      if ($slot[$nset][$s][0]<0) {
         $do2col = 1;
         $merge[$s] = $nset;
         continue;
      }
      if ($domerge && isset($merge[$s]) && $merge[$s]>=$nset) {
        continue;
      }
      if ($domerge) {
        for ($ns=$nset+1; $ns<count($slot); $ns++) {
          $same = 1;
          if (count($slot[$nset][$s])!=count($slot[$ns][$s]))
            $same = 0;
          else {
            $diff = array_diff_assoc($slot[$nset][$s], $slot[$ns][$s]);
            if (count($diff))
              $same = 0;
          }
          if ($do2col && $slot[$ns][$s-1][0]>=0)
            $same = 0;
          if (!$same)
            break;
          $merge[$s] = $ns;
        }
      }
      else {
        $merge[$s] = $nset;
      }
      $outstr .= '<td class="potioncolumn';
      if ($do2col)
        $outstr .= '_double" colspan="2"';
      else
        $outstr .= '"';
      if (isset($merge[$s]) && $merge[$s]>$nset)
        $outstr .= ' rowspan="'. ($merge[$s]-$nset+1) . '"';
      $outstr .= ">\n";
      if ($do2col)
        $outstr .= "<strong>Any TWO of the following:</strong><br />\n";
      for ($in=0; $in<count($slot[$nset][$s]); $in++) {
        $i = $slot[$nset][$s][$in];
        if ($in && $in==count($slot[$nset][$s])-1)
          $outstr .= "OR ";
        $outstr .= $allings[$i]->get_name_and_title();
        if ($in==count($slot[$nset][$s])-2)
          $outstr .= "<br />";
        elseif ($in!=count($slot[$nset][$s])-1)
          $outstr .= ",<br />";
      }
      $outstr .= "</td>\n";
      $do2col = 0;
    }
    for ($s=count($slot[$nset]); $s<4; $s++) {
      $outstr .= "<td class=\"potioncolumn_unused\"></td>\n";
    }
    calc_weight($slot[$nset], $totwtmin, $totwtmax);
    $outstr .= "<td class=\"potioncolumn_wt\">$totwtmin";
    if ($totwtmax>$totwtmin)
      $outstr .= " - $totwtmax";
    $outstr .= " lbs</td>\n";
    $outstr .= "<td class=\"potioncolumn_add\">";
    $outstr .= "<input type=\"checkbox\" value=\"1\" name=\"add_recipe_$recipeid\"> Save</input></td>\n";
    $outstr .= "</tr>\n";
  }

  return $outstr;
}

# figure out minimum/maximum weight for a recipe
function calc_weight($ingarray, &$totwtmin, &$totwtmax) {
  global $allings;
  $totwtmax = $totwtmin = $do2col = 0;
  for ($ns=0; $ns<count($ingarray); $ns++) {
    if (is_array($ingarray[$ns])) {
      if ($ingarray[$ns][0]<0) {
        $do2col = 1;
        continue;
      }
      $wts = array();
      foreach ($ingarray[$ns] as $i) {
        $wts[] = $allings[$i]->get_weight();
      }
      sort($wts);
      $totwtmax += $wts[count($wts)-1];
      $totwtmin += $wts[0];
      if ($do2col) {
        $totwtmax += $wts[count($wts)-2];
        $totwtmin += $wts[1];
      }
    }
    else {
      $totwtmax += $allings[$ingarray[$ns]]->get_weight();
      $totwtmin += $allings[$ingarray[$ns]]->get_weight();
    }
    $do2col = 0;
  }
  $totwtmax /= count($ingarray);
  if ($totwtmax>1) {
    $totwtmax = floor($totwtmax);
  }
  else {
    $totwtmax = floor($totwtmax*10+0.5)/10;
  }
  $totwtmin /= count($ingarray);
  if ($totwtmin>1) {
    $totwtmin = floor($totwtmin);
  }
  else {
    $totwtmin = floor($totwtmin*10+0.5)/10;
  }
}

# figure out minimum/maximum ingredient cost for a recipe
function calc_ingcost($ingarray, &$totcostmin, &$totcostmax) {
  global $allings;
  $totcostmax = $totcostmin = $do2col = 0;
  for ($ns=0; $ns<count($ingarray); $ns++) {
    if (is_array($ingarray[$ns])) {
      if ($ingarray[$ns][0]<0) {
        $do2col = 1;
        continue;
      }
      $cts = array();
      foreach ($ingarray[$ns] as $i) {
        $cts[] = $allings[$i]->get_cost();
      }
      sort($cts);
      $totcostmax += $cts[count($cts)-1];
      $totcostmin += $cts[0];
      if ($do2col) {
        $totcostmax += $cts[count($cts)-2];
        $totcostmin += $cts[1];
      }
    }
    else {
      $totcostmax += $allings[$ingarray[$ns]]->get_cost();
      $totcostmin += $allings[$ingarray[$ns]]->get_cost();
    }
    $do2col = 0;
  }
}

###
### Sorting functions
###
function sort_by_ingcount($a, $b) {
  global $possible_ings;
  return comp_ab(count($possible_ings[$a]), count($possible_ings[$b]));
}

function sort_by_score($a, $b) {
  global $allings;
  if ($allings[$a]->get_score_pro()==$allings[$b]->get_score_pro())
    return comp_ab($allings[$a]->get_score_con(), $allings[$b]->get_score_con());
  else
    return comp_ab($allings[$b]->get_score_pro(), $allings[$a]->get_score_pro());
}

function sort_extraeff($a, $b) {
		// Note: Use $GLOBALS instead of global to get around warning like "uksort: Array was modified by the user..."
  if ($GLOBALS['extraeffs'][$a]==$GLOBALS['extraeffs'][$b])
    return comp_ab($a, $b);
  else
    return comp_ab($GLOBALS['extraeffs'][$b], $GLOBALS['extraeffs'][$a]);
}

function sort_by_eff_score($a, $b) {
  global $eff_score_pro, $eff_score_con;
  if ($eff_score_pro[$a]==$eff_score_pro[$b])
    return comp_ab($eff_score_con[$a], $eff_score_con[$b]);
  else
    return comp_ab($eff_score_pro[$b], $eff_score_pro[$a]);
}

function sort_by_eff_score_noneg($a, $b) {
  global $eff_score_pro, $eff_score_con;
  if ($eff_score_con[$a]==$eff_score_con[$b])
    return comp_ab($eff_score_pro[$b], $eff_score_pro[$a]);
  else
    return comp_ab($eff_score_con[$a], $eff_score_con[$b]);
}

# originally this function sorted by effect strength
# but now it just goes alphabetically
function sort_by_eff_str($a, $b) {
  // Note: Use $GLOBALS instead of global to get around warning like "uksort: Array was modified by the user..."
  //global $alleffs;
  
  if ($a === 'base')
    return -1;
  elseif ($b === 'base')
    return 1;
  elseif ($a === 'multi')
    return -1;
  elseif ($b === 'multi')
    return 1;
  elseif ($a === 'other')
    return 1;
  elseif ($b === 'other')
    return -1;

#  if ($alleffs[$a]->get_cost()==$alleffs[$b]->get_cost())
    return comp_ab($a, $b);
#  else
#    return comp_ab($alleffs[$b]->get_cost(), $alleffs[$a]->get_cost());
}

function sort_by_altid($a, $b) {
  global $altid;

  $splita = explode('.', $altid[$a]);
  $splitb = explode('.', $altid[$b]);
  $suba = explode('+', $splita[0]);
  $subb = explode('+', $splitb[0]);
  if (count($suba)!=count($subb))
    return comp_ab(count($subb), count($suba));
  for ($n=0; $n<count($suba); $n++) {
    if ($suba[$n]!=$subb[$n])
      return sort_by_eff_str($suba[$n], $subb[$n]);
  }
  $suba = explode('-', $splita[1]);
  $subb = explode('-', $splitb[1]);
  if (count($suba)!=count($subb))
    return comp_ab(count($suba), count($subb));
  for ($n=0; $n<count($suba); $n++) {
    if ($suba[$n]!=$subb[$n])
      return sort_by_eff_str($suba[$n], $subb[$n]);
  }
}

function sort_by_nuse($a, $b) {
  global $nuse;
  global $custom_freq;
# sort first by nuse (highest value first)
#  then by custom_freq (highest value first)
#  then alphabetically (lowest first)
  if ($nuse[$a]==$nuse[$b]) {
    if ($custom_freq[$a]==$custom_freq[$b])
      return comp_ab($a, $b);
    else 
      return comp_ab($custom_freq[$b], $custom_freq[$a]);
  }
  else
    return comp_ab($nuse[$b], $nuse[$a]);
}

function comp_ab($a, $b) {
  return ($a==$b) ? 0 : (($a<$b) ? -1 : 1);
}

?>