<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

abstract class Shop_Abstract_Shipping_Gateway
{
	public abstract function get_rates($gateway_data);

	/**
	 * Returns the tracking URL for this shipping gateway, if applicable.
	 * @param string $tracking_number The tracking number to be appended to the tracking URL.
	 * @return string The tracking URL that can be used to view tracking information.
	 */
	public abstract function tracking_url($tracking_number);

	/**
	 * Returns the display name for this shipping gateway.
	 * @return string The name of this shipping gateway.
	 */
	public abstract function name();

	/**
	 * Does a preliminary check to see if the order qualifies for this gateway to be displayed.
	 * @return boolean True if the gateway should be available, false otherwise.
	 */
	public abstract function qualifies($gateway_data);
}

function compare_cm3($a, $b)
{
	if($a->cm3() == $b->cm3())
	{
	    return 0;
	}
  	return ($a->cm3() > $b->cm3()) ? -1 : 1;
}

function compare_cost($a, $b)
{
	if($a->getCost() == $b->getCost())
	{
	    return 0;
	}
  	return ($a->getCost() < $b->getCost()) ? -1 : 1;
}

class NerivonShippingCalculator
{
	private $boxes = array();
	private $boxes_temp = array();
	private $items = array();
	private $box_sizes = array();
	private $results = array();

	public function getItems()
	{
		return $this->items;
	}

	public function addBoxSize($l, $w, $h, $cost)
	{
		$this->box_sizes[] = new ShippingBoxSize($l, $w, $h, $cost);
	}

	public function countBoxes()
	{
		return count($this->boxes);
	}

	public function countItems()
	{
		return count($this->items);
	}

	public function addItem($item)
	{
		$this->items[] = $item;
	}

	public function addItems($items)
	{
		foreach($items as $item)
		{
			$this->addItem($item);
		}
	}

	public function calculate()
	{
		$this->boxes_temp = array();

		//sort the boxes so we check cheapest first
		usort($this->box_sizes, 'compare_cost');

		for($i=0; $i<count($this->box_sizes); $i++)
		{
			$this->results[$i] = $this->_calculate($this->box_sizes[$i]->getLength(), $this->box_sizes[$i]->getWidth(), $this->box_sizes[$i]->getHeight(), $this->box_sizes[$i]->getCost());
		}

		$cheapest = null;
		$best_box_size = null;
		$best_boxes = array();
		$box_count = 0;

		for($i=0; $i<count($this->box_sizes); $i++)
		{
			$boxes_required = $this->results[$i];

			//for whatever reason, we didn't put all items in this box so just ignore it as a "best box"
			if($boxes_required != false)
			{
				$cost = $this->box_sizes[$i]->getCost();
				$total_cost = $cost * $boxes_required;

				if($boxes_required > 0 && ($total_cost < $cheapest || $cheapest == null))
				{
					$cheapest = $total_cost;
					$best_box_size = $this->box_sizes[$i];
					$best_box_size->setQty($boxes_required);
				}
			}
		}

		for($j=0; $j<count($this->boxes_temp); $j++)
		{
			if($this->boxes_temp[$j]->getLength() == $best_box_size->getLength() &&
			   $this->boxes_temp[$j]->getWidth() == $best_box_size->getWidth() &&
			   $this->boxes_temp[$j]->getHeight() == $best_box_size->getHeight()
			   )
			{
				//there could be multiple boxes of the same size if multiple boxes were needed
				//we're going to return an array of "best boxes"
				$best_boxes[$box_count] = $this->boxes_temp[$j];
				$best_boxes[$box_count]->setCost($best_box_size->getCost());
				$best_boxes[$box_count]->setQty($best_box_size->getQty());
				$box_count++;
			}
		}

		return $best_boxes;
		//return $this->boxes_temp;
	}

	private function _calculate($l, $w, $h, $cost)
	{
		//reset number of boxes
		//$this->boxes_temp = array_merge($this->boxes_temp, $this->boxes);
		$this->boxes = array();
		$count = 0;

		$items_left = $this->items;

		while(true)
		{
			$this->boxes[$count] = new ShippingBox($l, $w, $h);
			$items_left = $this->boxes[$count]->addItems($items_left);

			if(count($items_left) == 0)
			{
				//all items were placed in a box

				break;
			}

			if($this->boxes[$count]->countItems() == 0)
			{
				//any remaining items won't fit in a box even by themselves
				array_pop($this->boxes); //kill our last box
				$this->countBoxes();
				break;
			}

			$count++;
		}

		$this->boxes_temp = array_merge($this->boxes_temp, $this->boxes);

		return $this->countBoxes();
	}

	public function debug($messages=false)
	{
		foreach($this->boxes as $box)
		{
			$box->debug($messages);
		}
	}

	public function html5()
	{
		foreach($this->boxes as $box)
		{
			$box->html5();
			return; //only do one of them for now
		}
	}
}

class ShippingBoxSize
{
	private $length;
	private $width;
	private $height;
	private $cost;
	private $qty = 0;

	public function __construct($length, $width, $height, $cost)
	{
		$this->length = $length;
		$this->width = $width;
		$this->height = $height;
		$this->cost = $cost;
	}

	public function getLength()
	{
		return $this->length;
	}

	public function getWidth()
	{
		return $this->width;
	}

	public function getHeight()
	{
		return $this->height;
	}

	public function getCost()
	{
		return $this->cost;
	}

	public function setQty($qty)
	{
		$this->qty = $qty;
	}

	public function getQty()
	{
		return $this->qty;
	}
}

class ShippingBox
{
	private $items = array();
	private $grid = array();
	private $length;
	private $width;
	private $height;
	private $weight;
	private $original_length;
	private $original_width;
	private $original_height;
	private $orientation;
	private $cost;
	private $qty = 0;
	public static $colours = array("", "000000", "000033", "000066", "000099", "0000CC", "0000FF", "003300", "003333", "003366", "003399", "0033CC", "0033FF", "006600", "006633", "006666", "006699", "0066CC", "0066FF", "009900", "009933", "009966", "009999", "0099CC", "0099FF", "00CC00", "00CC33", "00CC66", "00CC99", "00CCCC", "00CCFF", "00FF00", "00FF33", "00FF66", "00FF99", "00FFCC", "00FFFF", "330000", "330033", "330066", "330099", "3300CC", "3300FF", "333300", "333333", "333366", "333399", "3333CC", "3333FF", "336600", "336633", "336666", "336699", "3366CC", "3366FF", "339900", "339933", "339966", "339999", "3399CC", "3399FF", "33CC00", "33CC33", "33CC66", "33CC99", "33CCCC", "33CCFF", "33FF00", "33FF33", "33FF66", "33FF99", "33FFCC", "33FFFF", "660000", "660033", "660066", "660099", "6600CC", "6600FF", "663300", "663333", "663366", "663399", "6633CC", "6633FF", "666600", "666633", "666666", "666699", "6666CC", "6666FF", "669900", "669933", "669966", "669999", "6699CC", "6699FF", "66CC00", "66CC33", "66CC66", "66CC99", "66CCCC", "66CCFF", "66FF00", "66FF33", "66FF66", "66FF99", "66FFCC", "66FFFF", "990000", "990033", "990066", "990099", "9900CC", "9900FF", "993300", "993333", "993366", "993399", "9933CC", "9933FF", "996600", "996633", "996666", "996699", "9966CC", "9966FF", "999900", "999933", "999966", "999999", "9999CC", "9999FF", "99CC00", "99CC33", "99CC66", "99CC99", "99CCCC", "99CCFF", "99FF00", "99FF33", "99FF66", "99FF99", "99FFCC", "99FFFF", "CC0000", "CC0033", "CC0066", "CC0099", "CC00CC", "CC00FF", "CC3300", "CC3333", "CC3366", "CC3399", "CC33CC", "CC33FF", "CC6600", "CC6633", "CC6666", "CC6699", "CC66CC", "CC66FF", "CC9900", "CC9933", "CC9966", "CC9999", "CC99CC", "CC99FF", "CCCC00", "CCCC33", "CCCC66", "CCCC99", "CCCCCC", "CCCCFF", "CCFF00", "CCFF33", "CCFF66", "CCFF99", "CCFFCC", "CCFFFF", "FF0000", "FF0033", "FF0066", "FF0099", "FF00CC", "FF00FF", "FF3300", "FF3333", "FF3366", "FF3399", "FF33CC", "FF33FF", "FF6600", "FF6633", "FF6666", "FF6699", "FF66CC", "FF66FF", "FF9900", "FF9933", "FF9966", "FF9999", "FF99CC", "FF99FF", "FFCC00", "FFCC33", "FFCC66", "FFCC99", "FFCCCC", "FFCCFF", "FFFF00", "FFFF33", "FFFF66", "FFFF99", "FFFFCC");

	public function __construct($length, $width, $height)
	{
		$this->length = $length;
		$this->width = $width;
		$this->height = $height;

		$this->grid = array();

		for($x=1; $x<=$this->length; $x++)
		{
			$x = round($x);
			$this->grid[$x] = array();				//holds one for each length block

			for($y=1; $y<=$this->width; $y++)
			{
				$y = round($y);
				$this->grid[$x][$y] = array();		//holds one for each width block

				for($z=1; $z<=$this->height; $z++)
				{
					$z = round($z);
					$this->grid[$x][$y][$z] = 0;	//holds one for each height block
				}
			}
		}
	}

	public function countItems()
	{
		return count($this->items);
	}

	public function getLength()
	{
		return $this->length;
	}

	public function setLength($length)
	{
		$this->length = $length;
	}

	public function getWidth()
	{
		return $this->width;
	}

	public function setWidth($width)
	{
		$this->width = $width;
	}

	public function getHeight()
	{
		return $this->height;
	}

	public function setHeight($height)
	{
		$this->height = $height;
	}

	public function setCost($cost)
	{
		$this->cost = $cost;
	}

	public function getCost()
	{
		return $this->cost;
	}

	public function setQty($qty)
	{
		$this->qty = $qty;
	}

	public function getQty()
	{
		return $this->qty;
	}

	public function cm3()
	{
		return $this->length * $this->width * $this->height;
	}

	public function getWeight()
	{
		return $this->weight;
	}

	public function addWeight($weight)
	{
		$this->weight += $weight;
	}

	public function removeWeight($weight)
	{
		$this->weight -= $weight;
	}

	public function debug($messages=false)
	{
		if($messages)
		{
			devEcho("Box Size (LxWxH): " . $this->length . "x" . $this->width . "x" . $this->height . " (" . $this->cm3() . " cubic cm)");
		}

		foreach($this->items as $item)
		{
			if($item->will_fit)
			{
				$constant = "ITEM_ORIENTATION_" . $item->getOrientation();

				if($messages)
				{
					devEcho("Item will fit: " . $item->getLength() . "x" . $item->getWidth() . "x" . $item->getHeight() . "  (" . $item->cm3() . " cubic cm) - " . constant($constant) . " at position " . $item->x_pos . ", " . $item->y_pos . ", " . $item->z_pos . " (#" . ShippingBox::$colours[$item->colour_value] . ")");
				}
			}
			else
			{
				if($messages)
				{
					devEcho("Item won't fit: " . $item->getLength() . "x" . $item->getWidth() . "x" . $item->getHeight() . "  (" . $item->cm3() . " cubic cm)");
				}
			}
		}
		?>
		<table width="100%" border="0">
			<tr>
				<td valign="top">
					<h4 style="text-align: center;">Top View</h4>
					<table border="0" cellspacing="0" cellpadding="0">
						<tr>
							<th rowspan="1000">L<br />e<br />n<br />g<br />t<br />h</th><th colspan="1000"><-- Left / Width / Right --></th>
						</tr>
						<?php
						for($x=$this->length; $x>=1; $x--)
						{
							$x = round($x);
							?><tr><?php
							for($y=1; $y<=$this->width; $y++)
							{
								$y = round($y);
								$bg = "white";

								//this gives us a cross-section of our box, with the closer items showing in front of others
								for($z=1; $z<=$this->height; $z++)
								{
									$z = round($z);

									if($this->grid[$x][$y][$z] >= 1 && $this->grid[$x][$y][$z] != count(ShippingBox::$colours)-1)
									{
										$bg = ShippingBox::$colours[$this->grid[$x][$y][$z]];
									}
								}
								?>
								<td style="background-color: #<?php echo($bg); ?>; width: 15px; height: 15px; font-size: 0.75em; border: 1px solid gray;">&nbsp;</div>
								</td>
								<?php
							}
							?></tr><?php
						}
						?>
					</table>
				</td>
				<td valign="top">
					<h4 style="text-align: center;">Front View</h4>
					<table border="0" cellspacing="0" cellpadding="0">
						<tr>
							<th rowspan="1000">H<br />e<br />i<br />g<br />h<br />t</th><th colspan="1000"><-- Left / Width / Right --></th>
						</tr>
						<?php
						for($z=$this->height; $z>=1; $z--)
						{
							$z = round($z);
							?><tr><?php
							for($y=1; $y<=$this->width; $y++)
							{
								$y = round($y);
								$bg = "white";

								//this gives us a cross-section of our box, with the closer items showing in front of others
								for($x=$this->length; $x>=1; $x--)
								{
									$x = round($x);
									if($this->grid[$x][$y][$z] >= 1 && $this->grid[$x][$y][$z] != count(ShippingBox::$colours)-1)
									{
										$bg = ShippingBox::$colours[$this->grid[$x][$y][$z]];
									}
								}
								?>
								<td style="background-color: #<?php echo($bg); ?>; width: 15px; height: 15px; font-size: 0.75em; border: 1px solid gray;">&nbsp;</div>
								</td>
								<?php
							}
							?></tr><?php
						}
						?>
					</table>
				</td>
				<td valign="top">
					<h4 style="text-align: center;">Right Side View</h4>
					<table border="0" cellspacing="0" cellpadding="0">
						<tr>
							<th rowspan="1000">H<br />e<br />i<br />g<br />h<br />t</th><th colspan="1000"><-- Front / Length / Back --></th>
						</tr>
						<?php
						for($z=$this->height; $z>=1; $z--)
						{
							$z = round($z);
							?><tr><?php
							for($x=1; $x<=$this->length; $x++)
							{
								$x = round($x);
								$bg = "white";

								//this gives us a cross-section of our box, with the closer items showing in front of others
								for($y=1; $y<=$this->width; $y++)
								{
									$y = round($y);
									if($this->grid[$x][$y][$z] >= 1 && $this->grid[$x][$y][$z] != count(ShippingBox::$colours)-1)
									{
										$bg = ShippingBox::$colours[$this->grid[$x][$y][$z]];
									}
								}
								?>
								<td style="background-color: #<?php echo($bg); ?>; width: 15px; height: 15px; font-size: 0.75em; border: 1px solid gray;">&nbsp;</div>
								</td>
								<?php
							}
							?></tr><?php
						}
						?>
					</table>
				</td>
			</tr>

			<tr>
				<td valign="top">
					<h4 style="text-align: center;">Bottom View</h4>
					<table border="0" cellspacing="0" cellpadding="0">
						<tr>
							<th rowspan="1000">L<br />e<br />n<br />g<br />t<br />h</th><th colspan="1000"><-- Right / Width / Left --></th>
						</tr>
						<?php
						for($x=$this->length; $x>=1; $x--)
						{
							$x = round($x);
							?><tr><?php
							for($y=$this->width; $y>=1; $y--)
							{
								$y = round($y);
								$bg = "white";

								//this gives us a cross-section of our box, with the closer items showing in front of others
								for($z=1; $z<=$this->height; $z++)
								{
									$z = round($z);

									if($this->grid[$x][$y][$z] >= 1 && $this->grid[$x][$y][$z] != count(ShippingBox::$colours)-1)
									{
										$bg = ShippingBox::$colours[$this->grid[$x][$y][$z]];
									}
								}
								?>
								<td style="background-color: #<?php echo($bg); ?>; width: 15px; height: 15px; font-size: 0.75em; border: 1px solid gray;">&nbsp;</div>
								</td>
								<?php
							}
							?></tr><?php
						}
						?>
					</table>
				</td>
				<td valign="top">
					<h4 style="text-align: center;">Back View</h4>
					<table border="0" cellspacing="0" cellpadding="0">
						<tr>
							<th rowspan="1000">H<br />e<br />i<br />g<br />h<br />t</th><th colspan="1000"><-- Right / Width / Left --></th>
						</tr>
						<?php
						for($z=$this->height; $z>=1; $z--)
						{
							$z = round($z);
							?><tr><?php
							for($y=$this->width; $y>=1; $y--)
							{
								$y = round($y);
								$bg = "white";

								//this gives us a cross-section of our box, with the closer items showing in front of others
								for($x=$this->length; $x>=1; $x--)
								{
									$x = round($x);
									if($this->grid[$x][$y][$z] >= 1 && $this->grid[$x][$y][$z] != count(ShippingBox::$colours)-1)
									{
										$bg = ShippingBox::$colours[$this->grid[$x][$y][$z]];
									}
								}
								?>
								<td style="background-color: #<?php echo($bg); ?>; width: 15px; height: 15px; font-size: 0.75em; border: 1px solid gray;">&nbsp;</div>
								</td>
								<?php
							}
							?></tr><?php
						}
						?>
					</table>
				</td>
				<td valign="top">
					<h4 style="text-align: center;">Left Side View</h4>
					<table border="0" cellspacing="0" cellpadding="0">
						<tr>
							<th rowspan="1000">H<br />e<br />i<br />g<br />h<br />t</th><th colspan="1000"><-- Back / Length / Front --></th>
						</tr>
						<?php
						for($z=$this->height; $z>=1; $z--)
						{
							$z = round($z);
							?><tr><?php
							for($x=$this->length; $x>=1; $x--)
							{
								$x = round($x);
								$bg = "white";

								//this gives us a cross-section of our box, with the closer items showing in front of others
								for($y=$this->width; $y>=1; $y--)
								{
									$y = round($y);

									if($this->grid[$x][$y][$z] >= 1 && $this->grid[$x][$y][$z] != count(ShippingBox::$colours)-1)
									{
										$bg = ShippingBox::$colours[$this->grid[$x][$y][$z]];
									}
								}
								?>
								<td style="background-color: #<?php echo($bg); ?>; width: 15px; height: 15px; font-size: 0.75em; border: 1px solid gray;">&nbsp;</div>
								</td>
								<?php
							}
							?></tr><?php
						}
						?>
					</table>
				</td>
			</tr>
		</table>
		<?php
	}

	public function addItem($item)
	{
		$this->addItems(array($item));
	}

	public function addItems($items)
	{
		//sort the objects so we add biggest first
		usort($items, 'compare_cm3');

		$wont_fit = 0;
		$items_that_fit = array();
		$items_that_wont_fit = array();

		foreach($items as $item)
		{
			if($this->will_it_fit($item))
			{
				$items_that_fit[] = $item;
				$this->addWeight($item->getWeight());
			}
			else
			{
				$wont_fit++;
				$items_that_wont_fit[] = $item;
			}
			//echo("<hr />");
		}

		//$boxes = $this->how_many_boxes($items_that_fit);

		if($wont_fit > 0)
		{
			return $items_that_wont_fit;
		}
	}

	/**
	 * This is just to make our giant if statement shorter and easier to read.
	 * @return [type] [description]
	 */
	private function check_if_free($array, $x, $y, $z, $xy_padding=0, $z_padding=0)
	{
		//$free = false;

		if(isset($array[$x][$y][$z]) && $array[$x][$y][$z] == 0)
		{
			//this exact space is free, check its padding (todo)
			return true;
		}

		return false;
	}
	/**
	 * If the item fits in the box, the item will have its location properties set and true returned.
	 * If the item does not fit, all possible orientations will be set.  If it does not fit in any orientation, false is returned.
	 * We use the orientation and location from the actual item object, NOT the box until its found that it will fit.
	 * Think of it as physically manipulating the item to fit in the box as a human would.
	 *
	 * @param  [type]  $box         [description]
	 * @param  [type]  $item        [description]
	 * @param  integer $orientation [description]
	 * @return [type]               [description]
	 */
	private function will_it_fit(&$item)
	{
		$will_fit = false;

		//item is more cubic cm than our box
		if($item->cm3() > $this->cm3())
		{
			//reset item and return false
			$item->reset();
			$item->setOrientation(0);

			return false;
		}

		//starting at point 1,1,1 start checking
		for($zspace=1; $zspace<=$this->height; $zspace++)
		{
			$item->reset();
			$item->setOrientation(0);

			for($xx=1; $xx<=$this->length; $xx++)
			{
				$item->reset();
				$item->setOrientation(0);

				for($xy=1; $xy<=$this->width; $xy++)
				{
					$item->reset();
					$item->setOrientation(0);

					for($orientation=0; $orientation<=5; $orientation++)
					{
						// $base = $xx;
						// $north = $xx + $item->getLength() - 1;
						// $east = $xy + $item->getWidth() - 1;
						// $south = $xx + $item->getLength() - 1;
						// $true_zspace = $zspace + $item->getHeight() -1;

						if(
						   //check base
						   $this->check_if_free($this->grid, $xx, $xy, $zspace) &&
						   $this->check_if_free($this->grid, $xx + $item->getLength() - 1 + ($item->getXYPadding()*2), $xy, $zspace) &&
						   $this->check_if_free($this->grid, $xx, $xy + $item->getWidth() - 1 + ($item->getXYPadding()*2), $zspace) &&
						   $this->check_if_free($this->grid, $xx + $item->getLength() - 1 + ($item->getXYPadding()*2), $xy + $item->getWidth() - 1 + ($item->getXYPadding()*2), $zspace) &&

						   //check height (checking all the same points again, at our item's height)
						   $this->check_if_free($this->grid, $xx, $xy, $zspace + $item->getHeight() - 1 + ($item->getZPadding()*2)) &&
						   $this->check_if_free($this->grid, $xx + $item->getLength() - 1 + ($item->getXYPadding()*2), $xy, $zspace + $item->getHeight() - 1 + ($item->getZPadding()*2)) &&
						   $this->check_if_free($this->grid, $xx, $xy + $item->getWidth() - 1 + ($item->getXYPadding()*2), $zspace + $item->getHeight() - 1 + ($item->getZPadding()*2)) &&
						   $this->check_if_free($this->grid, $xx + $item->getLength() - 1 + ($item->getXYPadding()*2), $xy + $item->getWidth() - 1 + ($item->getXYPadding()*2), $zspace + $item->getHeight() - 1 + ($item->getZPadding()*2))
						   )
						{
							$will_fit = true;
							$item->x_pos = $xx;
							$item->y_pos = $xy;
							$item->z_pos = $zspace;
							$item->will_fit = true;

							$this->items[] = $item;

							break 4;	//break right back to the start
						}

						//the item didn't fit in this space, rotate it and try again
						//we're going to try every orientation in this position before moving on
						$item->rotate();
					}
				}
			}
		}

		if($will_fit)
		{
			//set our grid spaces to unavailable
			for($x=$item->x_pos; $x<=($item->x_pos + $item->getLength() - 1 + ($item->getXYPadding()*2)); $x++)
			{
				for($y=$item->y_pos; $y<=($item->y_pos + $item->getWidth() - 1 + ($item->getXYPadding()*2)); $y++)
				{
					for($z=$item->z_pos; $z<=($item->z_pos + $item->getHeight() - 1 + ($item->getZPadding()*2)); $z++)
					{
						$padding = false;

						if($x < $item->x_pos + $item->getXYPadding() || $x >= $item->x_pos + $item->getLength() + $item->getXYPadding())
						{
							$padding = true;
						}
						if($y < $item->y_pos + $item->getXYPadding() || $y >= $item->y_pos + $item->getWidth() + $item->getXYPadding())
						{
							$padding = true;
						}
						if($z < $item->z_pos + $item->getZPadding() || $z >= $item->z_pos + $item->getHeight() + $item->getZPadding())
						{
							$padding = true;
						}

						if($padding)
						{
							//the last colour in our array is reserved for padding
							$this->grid[$x][$y][$z] = count(ShippingBox::$colours)-1;
						}
						else
						{
							$this->grid[$x][$y][$z] = $item->colour_value;
						}
					}
				}
			}

			return true;
		}
		else
		{
			//reset item and return false
			$item->reset();
			$item->setOrientation(0);

			return false;
		}
	}

	public function html5()
	{
		?>
		<div id="container" style="border: 1px dashed gray;"></div>

		<script src="/js/three.min.js"></script>
		<script>

			var container, stats;
			var camera, controls, scene, renderer;
			var pickingData = [], pickingTexture, pickingScene;
			var objects = [];
			var highlightBox;

			var mouse = new THREE.Vector2();
			var offset = new THREE.Vector3( 10, 10, 10 );

			var width = window.innerWidth-40;
			var height = 300;

			init();
			animate();

			function init() {

				container = document.getElementById( "container" );

				camera = new THREE.PerspectiveCamera( 70, width / height, 1, 1000 );
				camera.position.z = 30;

				controls = new THREE.TrackballControls( camera );
				controls.rotateSpeed = 1.5;
				controls.zoomSpeed = 1.5;
				controls.panSpeed = 1.5;
				controls.noZoom = false;
				controls.noPan = false;
				controls.staticMoving = true;
				controls.dynamicDampingFactor = 0.3;

				scene = new THREE.Scene();

				pickingScene = new THREE.Scene();
				pickingTexture = new THREE.WebGLRenderTarget( width, height );
				pickingTexture.generateMipmaps = false;

				scene.add( new THREE.AmbientLight( 0x555555 ) );

				var light = new THREE.SpotLight( 0xffffff, 1.5 );
				light.position.set( 0, 500, 2000 );
				scene.add( light );

				var geometry = new THREE.Geometry(),
				pickingGeometry = new THREE.Geometry(),
				pickingMaterial = new THREE.MeshBasicMaterial( { vertexColors: THREE.VertexColors } ),
				defaultMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff, shading: THREE.FlatShading, vertexColors: THREE.VertexColors	} );

				function applyVertexColors( g, c ) {

					g.faces.forEach( function( f ) {

						var n = ( f instanceof THREE.Face3 ) ? 3 : 4;

						for( var j = 0; j < n; j ++ ) {

							f.vertexColors[ j ] = c;

						}

					} );

				}

				<?php
				$count = 0;
				foreach($this->items as $item)
				{
					?>
				//for ( var i = 0; i < 5000; i ++ ) {

					var position = new THREE.Vector3();

					position.x = <?php echo($item->x_pos); ?>;//Math.random() * 10000 - 5000;
					position.y = <?php echo($item->y_pos); ?>;//Math.random() * 6000 - 3000;
					position.z = <?php echo($item->z_pos); ?>;//Math.random() * 8000 - 4000;

					var rotation = new THREE.Euler();

					rotation.x = 0;//Math.random() * 2 * Math.PI;
					rotation.y = 0;//Math.random() * 2 * Math.PI;
					rotation.z = 0;//Math.random() * 2 * Math.PI;

					var scale = new THREE.Vector3();

					scale.x =  1;//Math.random() * 200 + 100;
					scale.y =  1;//Math.random() * 200 + 100;
					scale.z =  1;//Math.random() * 200 + 100;

					// give the geom's vertices a random color, to be displayed

					var geom = new THREE.CubeGeometry( <?php echo($item->getLength()); ?>, <?php echo($item->getWidth()); ?>, <?php echo($item->getHeight()); ?> );
					var color = new THREE.Color( 0x<?php echo(ShippingBox::$colours[$item->colour_value]); ?> );
					applyVertexColors( geom, color );

					var cube = new THREE.Mesh( geom );
					cube.position.copy( position );
					cube.rotation.copy( rotation );
					cube.scale.copy( scale );

					THREE.GeometryUtils.merge( geometry, cube );

					//give the pickingGeom's vertices a color corresponding to the "id"

					// var pickingGeom = new THREE.CubeGeometry( 1, 1, 1 );
					// var pickingColor = new THREE.Color( 1 );
					// applyVertexColors( pickingGeom, pickingColor );

					// var pickingCube = new THREE.Mesh( pickingGeom );
					// pickingCube.position.copy( position );
					// pickingCube.rotation.copy( rotation );
					// pickingCube.scale.copy( scale );

					// THREE.GeometryUtils.merge( pickingGeometry, pickingCube );

					// pickingData[ <?php echo($count); ?> ] = {

					// 	position: position,
					// 	rotation: rotation,
					// 	scale: scale

					// };
					<?php
					$count++;
				}
				?>
				var drawnObject = new THREE.Mesh( geometry, defaultMaterial );
				scene.add( drawnObject );

				pickingScene.add( new THREE.Mesh( pickingGeometry, pickingMaterial ) );

				highlightBox = new THREE.Mesh( new THREE.CubeGeometry( 1, 1, 1 ), new THREE.MeshLambertMaterial( { color: 0xffff00 } ) );
				scene.add( highlightBox );

				projector = new THREE.Projector();

				renderer = new THREE.WebGLRenderer( { antialias: true } );
				renderer.setClearColor( 0xffffff, 1 );
				renderer.setSize( width, height );
				renderer.sortObjects = false;

				container.appendChild( renderer.domElement );

				// stats = new Stats();
				// stats.domElement.style.position = 'absolute';
				// stats.domElement.style.top = '0px';
				// container.appendChild( stats.domElement );

				renderer.domElement.addEventListener( 'mousemove', onMouseMove );

			}

			//

			function onMouseMove( e ) {

				mouse.x = e.clientX;
				mouse.y = e.clientY;

			}

			function animate() {

				requestAnimationFrame( animate );

				render();
				//stats.update();

			}

			function pick() {

				//render the picking scene off-screen

				renderer.render( pickingScene, camera, pickingTexture );

				var gl = self.renderer.getContext();

				//read the pixel under the mouse from the texture

				var pixelBuffer = new Uint8Array( 4 );
				gl.readPixels( mouse.x, pickingTexture.height - mouse.y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixelBuffer );

				//interpret the pixel as an ID

				var id = ( pixelBuffer[0] << 16 ) | ( pixelBuffer[1] << 8 ) | ( pixelBuffer[2] );
				var data = pickingData[ id ];

				if ( data) {

					//move our highlightBox so that it surrounds the picked object

					if ( data.position && data.rotation && data.scale ){

						highlightBox.position.copy( data.position );
						highlightBox.rotation.copy( data.rotation );
						highlightBox.scale.copy( data.scale ).add( offset );
						highlightBox.visible = true;

					}

				} else {

					highlightBox.visible = false;

				}

			}

			function render() {

				controls.update();

				pick();

				renderer.render( scene, camera );

			}

		</script>
		<?php
	}
}

class ShippingItem
{
	private $original_length;
	private $original_width;
	private $original_height;
	private $length;
	private $width;
	private $height;
	private $weight;
	private $xy_padding;
	private $z_padding;
	private $original_xy_padding;
	private $original_z_padding;
	public $x_pos;	//length position
	public $y_pos;	//width position
	public $z_pos;	//height position
	private $orientation;
	public $colour_value;
	private static $colours_used = array();
	private $orientations = 5;
	public $will_fit = false;
	public $product_id = null;

	public function __construct($length, $width, $height, $weight, $xy_padding, $z_padding, $product_id)
	{
		mt_srand();

		while(true)
		{
			$this->colour_value = mt_rand(1, count(ShippingBox::$colours)-2);

			// //once we've used 6 colours, start over and recycle them
			// if(count(ShippingItem::$colours_used) == count(ShippingBox::$colours)-1)
			// {
			// 	ShippingItem::$colours_used = array();
			// }

			if(!in_array($this->colour_value, ShippingItem::$colours_used))
			{
				ShippingItem::$colours_used[] = $this->colour_value;
				break;
			}
		}

		$this->original_length = $length;
		$this->original_width = $width;
		$this->original_height = $height;
		$this->length = $length;
		$this->width = $width;
		$this->height = $height;
		$this->weight = $weight;

		$this->original_xy_padding = $xy_padding;
		$this->original_z_padding = $z_padding;
		$this->xy_padding = $xy_padding;
		$this->z_padding = $z_padding;

		$this->product_id = $product_id;

		//all items can assume they're located in the first possible position
		$this->x_pos = 1;
		$this->y_pos = 1;
		$this->z_pos = 1;
		$this->orientation = 0;
	}

	public function getLength()
	{
		return $this->length;
	}

	public function setLength($length)
	{
		$this->length = $length;
	}

	public function getWidth()
	{
		return $this->width;
	}

	public function setWidth($width)
	{
		$this->width = $width;
	}

	public function getHeight()
	{
		return $this->height;
	}

	public function setHeight($height)
	{
		$this->height = $height;
	}

	public function getWeight()
	{
		return $this->weight;
	}

	public function setWeight($weight)
	{
		$this->weight = $weight;
	}

	public function getOrientation()
	{
		return $this->orientation;
	}

	public function setOrientation($orientation)
	{
		$this->orientation = $orientation;
	}

	public function getXYPadding()
	{
		return $this->xy_padding;
	}

	public function setXYPadding($xy_padding)
	{
		$this->xy_padding = $xy_padding;
	}

	public function getZPadding()
	{
		return $this->z_padding;
	}

	public function setZPadding($z_padding)
	{
		$this->z_padding = $z_padding;
	}

	public function cm3()
	{
		return $this->length * $this->width * $this->height;
	}

	public function reset()
	{
		$this->length = $this->original_length;
		$this->width = $this->original_width;
		$this->height = $this->original_height;
		$this->xy_padding = $this->original_xy_padding;
		$this->z_padding = $this->original_z_padding;
		$this->x_pos = 1;
		$this->y_pos = 1;
		$this->z_pos = 1;
	}

	public function rotate()
	{
		$this->reset();
		$this->orientation++;

		if($this->orientation > $this->orientations)
		{
			$this->orientation = 0;
		}

		if($this->orientation == 0)
		{
			//standard orientation, nothing required
			/*
			-----
			|   |
			|   |
			|   |
			-----
			 */
		}
		else if($this->orientation == 1)
		{
			//rotate the item 90º so the length becomes width and width becomes length
			/*
			------------
			|          |
			|          |
			------------
			 */
			$length = $this->getLength();
			$this->setLength($this->getWidth());
			$this->setWidth($length);
		}
		else if($this->orientation == 2)
		{
			//rotate the item onto its side so that height/width swap
			/*
			--
			||
			||
			||
			--
			 */
			$height = $this->getHeight();
			$this->setHeight($this->getWidth());
			$this->setWidth($height);

			//top/bottom padding now becomes side padding and side padding now becomes top/bottom padding
			$xy_padding = $this->getXYPadding();
			$this->setXYPadding($this->getZPadding());
			$this->setZPadding($xy_padding);

		}
		else if($this->orientation == 3)
		{
			//rotate the item 90º and onto its side so that height/length swap and length/width swap
			/*
			------------
			------------
			 */
			//swap length/width
			$length = $this->getLength();
			$this->setLength($this->getWidth());
			$this->setWidth($length);

			//swap length/height
			$height = $this->getHeight();
			$this->setHeight($this->getLength());
			$this->setLength($height);

			//top/bottom padding now becomes side padding and side padding now becomes top/bottom padding
			$xy_padding = $this->getXYPadding();
			$this->setXYPadding($this->getZPadding());
			$this->setZPadding($xy_padding);
		}
		else if($this->orientation == 4)
		{
			//stand the item on its end so height/length swap
			/*
			-----
			-----
			 */
			$height = $this->getHeight();
			$this->setHeight($this->getLength());
			$this->setLength($height);

			//top/bottom padding now becomes side padding and side padding now becomes top/bottom padding
			$xy_padding = $this->getXYPadding();
			$this->setXYPadding($this->getZPadding());
			$this->setZPadding($xy_padding);
		}
		else if($this->orientation == 5)
		{
			//stand the item on its end so width/length and rotate 90º
			/*
			--
			||
			||
			--
			 */
			//rotate 90º
			$height = $this->getHeight();
			$this->setHeight($this->getWidth());
			$this->setWidth($height);

			//stand upright
			$height = $this->getHeight();
			$this->setHeight($this->getWidth());
			$this->setWidth($height);

			//top/bottom padding now becomes side padding and side padding now becomes top/bottom padding
			$xy_padding = $this->getXYPadding();
			$this->setXYPadding($this->getZPadding());
			$this->setZPadding($xy_padding);
		}
	}
}
?>
