<?php
/**
 * BCGgs1128.barcode.php
 *--------------------------------------------------------------------
 *
 * Calculate the GS1-128 based on the Code-128 encoding.
 *
 *--------------------------------------------------------------------
 * Revision History
 * V2.2.0	10 feb	2010	Kevin Gilbert, reviewed by Jean-Sébastien Goupil, new version
 *--------------------------------------------------------------------
 * $Id: BCGgs1128.barcode.php,v 1.1 2010/02/14 00:24:07 jsgoupil Exp $
 *--------------------------------------------------------------------
 * Copyright (C) Jean-Sebastien Goupil
 * http://www.barcodephp.com
 */
include_once('BCGcode128.barcode.php');

class BCGgs1128 extends BCGcode128 {
	const KIND_OF_DATA = 0;
	const MINLENGTH = 1;
	const MAXLENGTH = 2;
	const CHECKSUM = 3;
	const NUMERIC = 0;
	const ALPHA_NUMERIC = 1;
	const DATE_YYMMDD = 2;
	const ID = 0;
	const CONTENT = 1;
	const MAX_ID_FORMATED = 6;
	const MAX_ID_NOT_FORMATED = 4;
	const MAX_GS1128_CHARS = 48;

	private $errorTextGS1128;
	private $strictMode;
	private $identifiersId = array();
	private $identifiersContent = array();
	private $identifiersAi = array();

	/**
	 * Constructor
	 *
	 * @param char $start
	 */
	public function __construct($start = NULL) {
		if($start === NULL) {
			$start = 'C';
		}
		parent::__construct($start);
		
		/* Application Identifiers (AIs) */
		/* 
		array ( KIND_OF_DATA , MINLENGTH , MAXLENGTH , CHECKSUM )
		KIND_OF_DATA:		NUMERIC , ALPHA_NUMERIC or DATE_YYMMDD
		CHECKSUM:			bool (true / false)
		*/
		$this->identifiersAi = array(
			'00'	=>	array(self::NUMERIC,		18,	18,	true),
			'01'	=>	array(self::NUMERIC,		14,	14,	true),
			'02'	=>	array(self::NUMERIC,		14,	14,	true),
			'10'	=>	array(self::ALPHA_NUMERIC,	1,	20,	false),
			'11'	=>	array(self::DATE_YYMMDD,	6,	6,	false),
			'12'	=>	array(self::DATE_YYMMDD,	6,	6,	false),
			'13'	=>	array(self::DATE_YYMMDD,	6,	6,	false),
			'15'	=>	array(self::DATE_YYMMDD,	6,	6,	false),
			'17'	=>	array(self::DATE_YYMMDD,	6,	6,	false),
			'20'	=>	array(self::NUMERIC,		2,	2,	false),
			'21'	=>	array(self::ALPHA_NUMERIC,	1,	20,	false),
			'240'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'241'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'250'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'251'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'253'	=>	array(self::NUMERIC,		14,	30,	false),
			'30'	=>	array(self::NUMERIC,		1,	8,	false),
			'310y'	=>	array(self::NUMERIC,		6,	6,	false),
			'311y'	=>	array(self::NUMERIC,		6,	6,	false),
			'312y'	=>	array(self::NUMERIC,		6,	6,	false),
			'313y'	=>	array(self::NUMERIC,		6,	6,	false),
			'314y'	=>	array(self::NUMERIC,		6,	6,	false),
			'315y'	=>	array(self::NUMERIC,		6,	6,	false),
			'316y'	=>	array(self::NUMERIC,		6,	6,	false),
			'320y'	=>	array(self::NUMERIC,		6,	6,	false),
			'321y'	=>	array(self::NUMERIC,		6,	6,	false),
			'322y'	=>	array(self::NUMERIC,		6,	6,	false),
			'323y'	=>	array(self::NUMERIC,		6,	6,	false),
			'324y'	=>	array(self::NUMERIC,		6,	6,	false),
			'325y'	=>	array(self::NUMERIC,		6,	6,	false),
			'326y'	=>	array(self::NUMERIC,		6,	6,	false),
			'327y'	=>	array(self::NUMERIC,		6,	6,	false),
			'328y'	=>	array(self::NUMERIC,		6,	6,	false),
			'329y'	=>	array(self::NUMERIC,		6,	6,	false),
			'330y'	=>	array(self::NUMERIC,		6,	6,	false),
			'331y'	=>	array(self::NUMERIC,		6,	6,	false),
			'332y'	=>	array(self::NUMERIC,		6,	6,	false),
			'333y'	=>	array(self::NUMERIC,		6,	6,	false),
			'334y'	=>	array(self::NUMERIC,		6,	6,	false),
			'335y'	=>	array(self::NUMERIC,		6,	6,	false),
			'336y'	=>	array(self::NUMERIC,		6,	6,	false),
			'337y'	=>	array(self::NUMERIC,		6,	6,	false),
			'340y'	=>	array(self::NUMERIC,		6,	6,	false),
			'341y'	=>	array(self::NUMERIC,		6,	6,	false),
			'342y'	=>	array(self::NUMERIC,		6,	6,	false),
			'343y'	=>	array(self::NUMERIC,		6,	6,	false),
			'344y'	=>	array(self::NUMERIC,		6,	6,	false),
			'345y'	=>	array(self::NUMERIC,		6,	6,	false),
			'346y'	=>	array(self::NUMERIC,		6,	6,	false),
			'347y'	=>	array(self::NUMERIC,		6,	6,	false),
			'348y'	=>	array(self::NUMERIC,		6,	6,	false),
			'349y'	=>	array(self::NUMERIC,		6,	6,	false),
			'350y'	=>	array(self::NUMERIC,		6,	6,	false),
			'351y'	=>	array(self::NUMERIC,		6,	6,	false),
			'352y'	=>	array(self::NUMERIC,		6,	6,	false),
			'353y'	=>	array(self::NUMERIC,		6,	6,	false),
			'354y'	=>	array(self::NUMERIC,		6,	6,	false),
			'355y'	=>	array(self::NUMERIC,		6,	6,	false),
			'356y'	=>	array(self::NUMERIC,		6,	6,	false),
			'357y'	=>	array(self::NUMERIC,		6,	6,	false),
			'360y'	=>	array(self::NUMERIC,		6,	6,	false),
			'361y'	=>	array(self::NUMERIC,		6,	6,	false),
			'362y'	=>	array(self::NUMERIC,		6,	6,	false),
			'363y'	=>	array(self::NUMERIC,		6,	6,	false),
			'364y'	=>	array(self::NUMERIC,		6,	6,	false),
			'365y'	=>	array(self::NUMERIC,		6,	6,	false),
			'366y'	=>	array(self::NUMERIC,		6,	6,	false),
			'367y'	=>	array(self::NUMERIC,		6,	6,	false),
			'368y'	=>	array(self::NUMERIC,		6,	6,	false),
			'369y'	=>	array(self::NUMERIC,		6,	6,	false),
			'37'	=>	array(self::NUMERIC,		1,	8,	false),
			'390y'	=>	array(self::NUMERIC,		1,	15,	false),
			'391y'	=>	array(self::NUMERIC,		4,	18,	false),
			'392y'	=>	array(self::NUMERIC,		1,	15,	false),
			'393y'	=>	array(self::NUMERIC,		4,	18,	false),
			'400'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'401'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'402'	=>	array(self::NUMERIC,		17,	17,	false),
			'403'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'410'	=>	array(self::NUMERIC,		13,	13,	true),
			'411'	=>	array(self::NUMERIC,		13,	13,	true),
			'412'	=>	array(self::NUMERIC,		13,	13,	true),
			'413'	=>	array(self::NUMERIC,		13,	13,	true),
			'414'	=>	array(self::NUMERIC,		13,	13,	true),
			'415'	=>	array(self::NUMERIC,		13,	13,	true),
			'420'	=>	array(self::ALPHA_NUMERIC,	1,	20,	false),
			'421'	=>	array(self::ALPHA_NUMERIC,	4,	12,	false),
			'422'	=>	array(self::NUMERIC,		3,	3,	false),
			'8001'	=>	array(self::NUMERIC,		14,	14,	false),
			'8002'	=>	array(self::ALPHA_NUMERIC,	1,	20,	false),
			'8003'	=>	array(self::ALPHA_NUMERIC,	15,	30,	false),
			'8004'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'8005'	=>	array(self::NUMERIC,		6,	6,	false),
			'8006'	=>	array(self::NUMERIC,		18,	18,	false),
			'8007'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'8018'	=>	array(self::NUMERIC,		18,	18,	false),
			'8020'	=>	array(self::ALPHA_NUMERIC,	1,	25,	false),
			'8100'	=>	array(self::NUMERIC,		6,	6,	false),
			'8101'	=>	array(self::NUMERIC,		10,	10,	false),
			'8102'	=>	array(self::NUMERIC,		2,	2,	false),
			'90'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'91'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'92'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'93'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'94'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'95'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'96'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'97'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'98'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false),
			'99'	=>	array(self::ALPHA_NUMERIC,	1,	30,	false)
		);
		
		$this->setStrictMode(true);
		$this->setTilde(true);
	}
	
	/**
	 * Enables or disables the strict mode
	 *
	 * @param bool strictMode
	 */
	public function setStrictMode($strictMode) {
		$this->strictMode = $strictMode;
	}
	
	/**
	 * Parses Text
	 *
	 * @param string $text
	 */
	public function parse($text) {
		parent::parse($this->parseGs1128($text));
		$this->errorText .= $this->errorTextGS1128;
	}
	
	/**
	 * Formats data for gs1-128
	 *
	 * @return string
	 */
	private function formatGs1128() {
		$formatedText = '~F1';
		$formatedLabel = '';
		$c = count($this->identifiersId);
		
		for($i = 0; $i < $c; $i++) {
			if($i > 0) {
				$formatedLabel .= ' ';
			}
			$formatedLabel .= '(' . $this->identifiersId[$i] . ')';
			$formatedText .= $this->identifiersId[$i];
			
			$formatedLabel .= $this->identifiersContent[$i];
			$formatedText .= $this->identifiersContent[$i];
			
			if(isset($this->identifiersAi[$this->identifiersId[$i]])) {
				$ai_data = $this->identifiersAi[$this->identifiersId[$i]];
			} elseif(isset($this->identifiersId[$i][3])) {
				$identifierWithVar = substr($this->identifiersId[$i], 0, -1) . 'y';
				$ai_data = isset($this->identifiersAi[$identifierWithVar]) ? $this->identifiersAi[$identifierWithVar] : NULL;
			} else {
				$ai_data = NULL;
			}
			
			/* We'll check if we need to add a ~F1 (<GS>) char */
			/* If we use the legacy mode, we always add a ~F1 (<GS>) char between AIs */
			if($ai_data !== NULL) {
				if(strlen($this->identifiersContent[$i]) < $ai_data[self::MAXLENGTH] && ($i + 1) !== $c || !$this->strictMode && ($i + 1) !== $c) {
					$formatedText .= '~F1';
				}
			}
		}
		
		if((strlen($formatedText) - 3) > self::MAX_GS1128_CHARS) {
			$this->errorTextGS1128 = 'The barcode can\'t be bigger than ' . self::MAX_GS1128_CHARS . ' characters.';
		}
		
		$this->label = $formatedLabel;
		return $formatedText;
	}
	
	/**
	 * Parses the text to gs1-128
	 *
	 * @param mixed $text
	 * @return mixed
	 */
	private function parseGs1128($text) {
		/* We format correctly what the user gives */
		if(is_array($text)) {
			$formatArray = array();
			foreach($text as $content) {
				if(is_array($content)) { /* double array */
					if(count($content) === 2) {
						if(is_array($content[self::ID]) || is_array($content[self::CONTENT])) {
							$this->errorTextGS1128 = 'Double arrays can\'t contain arrays.';
							return false;
						} else {
							$formatArray[] = '(' . $content[self::ID] . ')' . $content[self::CONTENT];
						}
					} else {
						$this->errorTextGS1128 = 'Double arrays must contain 2 values.';
						return false;
					}
				} else { /* simple array */
					$formatArray[] = $content;
				}
			}
			unset($text);
			$text = $formatArray;
		} else { /* string */
			$text = array($text);
		}
		$textCount = count($text);
		for($cmpt = 0; $cmpt < $textCount; $cmpt++) {
			/* We parse the content of the array */
			if(!$this->parseContent($text[$cmpt])) {
				return false;
			}
		}
		return $this->formatGs1128();
	}
	
	/**
	 * Splits the id and the content for each application identifiers (AIs)
	 *
	 * @param string $text
	 * @param int $cmpt
	 *
	 * @return bool
	 */
	private function parseContent($text) {
		/* $yAlreadySet has 3 states: */
		/* null: There is no variable in the ID; true: the variable is already set; false: the variable is not set yet; */
		$yAlreadySet = NULL;
		$realNameId = NULL;
		$separatorsFound = 0;
		$checksumAdded = 0;
		$decimalPointRemoved = 0;
		
		$toParse = str_replace('~F1' ,chr(29), $text);
		$nbCharToParse = strlen($toParse);
		$isFormated = $toParse[0] === '(' ? true : false;
		$maxCharId = $isFormated ? self::MAX_ID_FORMATED : self::MAX_ID_NOT_FORMATED;

		$id = strtolower(substr($toParse, 0, min($maxCharId, $nbCharToParse)));
		$id = $isFormated ? $this->findIdFormated($id, $yAlreadySet, $realNameId) : $this->findIdNotFormated($id, $yAlreadySet, $realNameId);
		if($id === false) {
			return false;
		}
		$nbCharId = $isFormated ? strlen($id) + 2 : strlen($id);
		$n = min($this->identifiersAi[$realNameId][self::MAXLENGTH], $nbCharToParse);
		$content = substr($toParse, $nbCharId, $n);

		/* If we have an AI with an "y" var, we check if there is a decimal point in the next *MAXLENGTH* characters */
		/* if there is one, we take an extra character */
		if($yAlreadySet !== NULL) {
			if(strpos($content, '.') !== false || strpos($content, ',') !== false) {
				$n++;
				if($n <= $nbCharToParse) {
					/* We take an extra char */
					$content = substr($toParse, $nbCharId, $n);
				}
			}
		}
		/* We check for separator */
		$separator = strpos($content, chr(29));
		if($separator !== false) {
			$content = substr($content, 0, $separator);
			$separatorsFound++;
		}
		/* We check the conformity */
		if(!$this->checkConformity($content, $id, $realNameId)) {
			return false;
		}
		/* We check the checksum */
		if(!$this->checkChecksum($content, $id, $realNameId, $checksumAdded)) {
			return false;
		}
		/* We check the vars */
		if(!$this->checkVars($content, $id, $yAlreadySet, $decimalPointRemoved)) {
			return false;
		}

		$this->identifiersId[] = $id;
		$this->identifiersContent[] = $content;
		
		$nbCharLastContent = (((strlen($content) + $nbCharId) - $checksumAdded) + $decimalPointRemoved) + $separatorsFound;
		if($nbCharToParse - $nbCharLastContent > 0) {
			/* If there is more than one content in this array, we parse again */
			$otherContent = substr($toParse, $nbCharLastContent, $nbCharToParse);
			$nbCharOtherContent = strlen($otherContent);
			
			if($otherContent[0] === chr(29)) {
				$otherContent = substr($otherContent, 1);
				$nbCharOtherContent--;
			}
			if($nbCharOtherContent > 0) {
				$text = $otherContent;
				$this->parseContent($text);
			}
		}
		return true;
	}
	
	/**
	 * Checks if an id exists
	 *
	 * @param string $id
	 * @param bool $yAlreadySet
	 * @param string $realNameId
	 *
	 * @return bool
	 */
	private function idExists($id, &$yAlreadySet, &$realNameId) {
		$yFound = isset($id[3]) && $id[3] === 'y';
		$idVarAdded = substr($id, 0, -1) . 'y';
		
		if(isset($this->identifiersAi[$id])) {
			if($yFound) {
				$yAlreadySet = false;
			}
			$realNameId = $id;
			return true;
		} elseif(!$yFound && isset($this->identifiersAi[$idVarAdded])) {
			/* if the id don't exist, we try to find this id with "y" at the last char */
			$yAlreadySet = true;
			$realNameId = $idVarAdded;
			return true;
		}
		return false;
	}
	
	/**
	 * Finds ID with formated content
	 *
	 * @param string $id
	 * @param bool $yAlreadySet
	 * @param string $realNameId
	 * @return mixed
	 */
	private function findIdFormated($id, &$yAlreadySet, &$realNameId) {
		$pos = strpos($id, ')');
		if($pos === false) {
			$this->errorTextGS1128 = 'Identifiers must have no more than 4 characters.';
			return false;
		} else {
			if($pos < 3) {
				$this->errorTextGS1128 = 'Identifiers must have at least 2 characters.';
				return false;
			}
			$id = substr($id, 1, $pos - 1);
			
			if($this->idExists($id, $yAlreadySet, $realNameId)) {
				return $id;
			} else {
				$this->errorTextGS1128 =  'The identifier ' . $id . ' doesn\'t exist.';
				return false;
			}
		}
	}
	
	/**
	 * Finds ID with non-formated content
	 *
	 * @param string $id
	 * @param bool $yAlreadySet
	 * @param string $realNameId
	 * @return mixed
	 */
	private function findIdNotFormated($id, &$yAlreadySet, &$realNameId) {
		$tofind = $id;
		
		while(strlen($tofind) >= 2) {
			if($this->idExists($tofind, $yAlreadySet, $realNameId)) {
				return $tofind;
			} else {
				$tofind = substr($tofind, 0, -1);
			}
		}
		$this->errorTextGS1128 = 'Error in formatting, can\'t find an identifier.';
		return false;
	}
	
	/**
	 * Checks confirmity of the content
	 *
	 * @param string $content
	 * @param string $id
	 * @param string $realNameId
	 * @return bool
	 */
	private function checkConformity(&$content, $id, $realNameId) {
		switch($this->identifiersAi[$realNameId][self::KIND_OF_DATA]) {
		case self::NUMERIC:
			$content = str_replace(',', '.', $content);
			if(!preg_match("/^[0-9.]+$/", $content)) {
				$this->errorTextGS1128 = 'The value of "' . $id . '" must be numerical.';
				return false;
			}
			break;
		case self::DATE_YYMMDD:
			$valid_date = true;
			if(preg_match("/^[0-9]{6}$/", $content)) {
				$year = substr($content, 0, 2);
				$month = substr($content, 2, 2);
				$day = substr($content, 4, 2);
				
				/* day can be 00 if we only need month and year */
				if(intval($month) < 1 || intval($month) > 12 || intval($day) < 0 || intval($day) > 31) {
					$valid_date = false;
				}
			} else {
				$valid_date = false;
			}
			if(!$valid_date) {
				$this->errorTextGS1128 = 'The value of "' . $id . '" must be in YYMMDD format.';
				return false;
			}
			break;
		}
		
		/* We check the length of the content */
		$nbCharContent = strlen($content);
		$checksumChar = 0;
		$minlengthContent = $this->identifiersAi[$realNameId][self::MINLENGTH];
		$maxlengthContent = $this->identifiersAi[$realNameId][self::MAXLENGTH];

		if($this->identifiersAi[$realNameId][self::CHECKSUM]) {
			$checksumChar++;
		}
		if($nbCharContent < ($minlengthContent - $checksumChar)) {
			if($minlengthContent === $maxlengthContent) {
				$this->errorTextGS1128 = 'The value of "' . $id . '" must contain ' . $minlengthContent . ' character(s).';
				return false;
			} else {
				$this->errorTextGS1128 = 'The value of "' . $id . '" must contain between ' . $minlengthContent . ' and ' . $maxlengthContent . ' character(s).';
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Verifies the checksum
	 *
	 * @param string $content
	 * @param string $id
	 * @param int $realNameId
	 * @param int $checksumAdded
	 * @return bool
	 */
	private function checkChecksum(&$content, $id, $realNameId, &$checksumAdded) {
		if($this->identifiersAi[$realNameId][self::CHECKSUM]) {
			$nbCharContent = strlen($content);
			$minlengthContent = $this->identifiersAi[$realNameId][self::MINLENGTH];
			if($nbCharContent === ($minlengthContent - 1)) {
				/* we need to calculate the checksum */
				$content .= $this->calculateChecksumMod10($content);
				$checksumAdded++;
			} elseif($nbCharContent === $minlengthContent) {
				/* we need to check the checksum */
				$checksum = $this->calculateChecksumMod10(substr($content, 0, -1));
				if(intval($content[$nbCharContent - 1]) !== $checksum) {
					$this->errorTextGS1128 = 'The checksum of "(' . $id . ') ' . $content . '" must be: ' . $checksum;
					return false;
				}
			}
		}
		return true;
	}
	
	/**
	 * Checks vars "y"
	 *
	 * @param string $content
	 * @param string $id
	 * @param bool $yAlreadySet
	 * @param int $decimalPointRemoved
	 * @return bool
	 */
	private function checkVars(&$content, &$id, $yAlreadySet, &$decimalPointRemoved) {
		$nbCharContent = strlen($content);
		/* We check for "y" var in AI */
		if($yAlreadySet) {
			/* We'll check if we have a decimal point */
			if(strpos($content, '.') !== false) {
				$this->errorTextGS1128 = 'If you do not use any "y" variable, you have to insert a whole number.';
				return false;
			}
		} elseif($yAlreadySet !== NULL) {
			/* We need to replace the "y" var with the position of the decimal point */
			$pos = strpos($content, '.');
			if($pos === false) {
				$this->errorTextGS1128 = 'If you use a "y" variable, you have to insert a decimal number.';
				return false;
			} else {
				$id = str_replace('y', $nbCharContent - ($pos + 1), strtolower($id));
				$content = str_replace('.', '', $content);
				$decimalPointRemoved++;
			}
		}
		return true;
	}
	
	/**
	 * Checksum Mod10
	 * @param int $content
	 * @return int
	 */
	private function calculateChecksumMod10($content) {
		// Calculating Checksum
		// Consider the right-most digit of the message to be in an "odd" position,
		// and assign odd/even to each character moving from right to left
		// Odd Position = 3, Even Position = 1
		// Multiply it by the number
		// Add all of that and do 10-(?mod10)
		$odd = true;
		$checksumValue = 0;
		$c = strlen($content);
		
		for($i = $c; $i > 0; $i--) {
			if($odd === true) {
				$multiplier = 3;
				$odd = false;
			} else {
				$multiplier = 1;
				$odd = true;
			}
			$checksumValue += ($content[$i - 1] * $multiplier);
		}
		return (10 - $checksumValue % 10) % 10;
	}
};
?>