<?php

namespace Leenooks\OpenPGP;

use Leenooks\OpenPGP;

/**
 * OpenPGP Signature packet (tag 2).
 * Be sure to NULL the trailer if you update a signature packet!
 *
 * @see http://tools.ietf.org/html/rfc4880#section-5.2
 */
class SignaturePacket extends Packet
{
	protected $tag = 2;
	public $version, $signature_type, $hash_algorithm, $key_algorithm, $hashed_subpackets, $unhashed_subpackets, $hash_head;

	// This is the literal bytes that get tacked on the end of the message when verifying the signature
	public $trailer;

	static $hash_algorithms = [
		1 => 'MD5',
		2 => 'SHA1',
		3 => 'RIPEMD160',
		8 => 'SHA256',
		9 => 'SHA384',
		10 => 'SHA512',
		11 => 'SHA224'
	];

	static $subpacket_types = [
		//0 => 'Reserved',
		//1 => 'Reserved',
		2 => 'SignatureCreationTime',
		3 => 'SignatureExpirationTime',
		4 => 'ExportableCertification',
		5 => 'TrustSignature',
		6 => 'RegularExpression',
		7 => 'Revocable',
		//8 => 'Reserved',
		9 => 'KeyExpirationTime',
		//10 => 'Placeholder for backward compatibility',
		11 => 'PreferredSymmetricAlgorithms',
		12 => 'RevocationKey',
		//13 => 'Reserved',
		//14 => 'Reserved',
		//15 => 'Reserved',
		16 => 'Issuer',
		//17 => 'Reserved',
		//18 => 'Reserved',
		//19 => 'Reserved',
		20 => 'NotationData',
		21 => 'PreferredHashAlgorithms',
		22 => 'PreferredCompressionAlgorithms',
		23 => 'KeyServerPreferences',
		24 => 'PreferredKeyServer',
		25 => 'PrimaryUserID',
		26 => 'PolicyURI',
		27 => 'KeyFlags',
		28 => 'SignersUserID',
		29 => 'ReasonforRevocation',
		30 => 'Features',
		31 => 'SignatureTarget',
		32 => 'EmbeddedSignature',
	];

	function __construct($data=NULL,$key_algorithm=NULL,$hash_algorithm=NULL)
	{
		parent::__construct();

		// Default to version 4 sigs
		$this->version = 4;

		if (is_string($this->hash_algorithm = $hash_algorithm)) {
			$this->hash_algorithm = array_search($this->hash_algorithm, self::$hash_algorithms);
		}

		if (is_string($this->key_algorithm = $key_algorithm)) {
			$this->key_algorithm = array_search($this->key_algorithm,PublicKeyPacket::$algorithms);
		}

		// If we have any data, set up the creation time
		if ($data) {
			$this->hashed_subpackets = [new SignaturePacket\SignatureCreationTimePacket(time())];
		}

		if ($data instanceof LiteralDataPacket) {
			$this->signature_type = ($data->format == 'b') ? 0x00 : 0x01;
			$data->normalize();
			$data = $data->data;

		} elseif ($data instanceof Message && $data[0] instanceof PublicKeyPacket) {
			// $data is a message with PublicKey first, UserID second
			$key = implode('',$data[0]->fingerprint_material());
			$user_id = $data[1]->body();
			$data = $key.chr(0xB4).pack('N',strlen($user_id)).$user_id;
		}

		// Store to-be-signed data in here until the signing happens
		$this->data = $data;
	}

	function body()
	{
		switch($this->version) {
			case 2:
			case 3:
				$body = chr($this->version).chr(5).chr($this->signature_type);

				foreach ((array)$this->unhashed_subpackets as $p) {
					if ($p instanceof SignaturePacket\SignatureCreationTimePacket) {
						$body .= pack('N',$p->data);

						break;
					}
				}

				foreach ((array)$this->unhashed_subpackets as $p) {
					if ($p instanceof SignaturePacket\IssuerPacket) {
						for($i = 0; $i < strlen($p->data); $i += 2) {
							$body .= chr(hexdec($p->data[$i].$p->data[$i+1]));
						}

						break;
					}
				}

				$body .= chr($this->key_algorithm);
				$body .= chr($this->hash_algorithm);
				$body .= pack('n',$this->hash_head);

				foreach ($this->data as $mpi) {
					$body .= pack('n',OpenPGP::bitlength($mpi)).$mpi;
				}

				return $body;

			case 4:
				if (!$this->trailer)
					$this->trailer = $this->calculate_trailer();

				$body = substr($this->trailer,0,-6);

				$unhashed_subpackets = '';
				foreach((array)$this->unhashed_subpackets as $p) {
					$unhashed_subpackets .= (string)$p;
				}

				$body .= pack('n',strlen($unhashed_subpackets)).$unhashed_subpackets;

				$body .= pack('n',$this->hash_head);

				foreach ((array)$this->data as $mpi) {
					$body .= pack('n',OpenPGP::bitlength($mpi)).$mpi;
				}

				return $body;
		}
	}

	function body_start()
	{
		$body = chr(4).chr($this->signature_type).chr($this->key_algorithm).chr($this->hash_algorithm);

		$hashed_subpackets = '';
		foreach((array)$this->hashed_subpackets as $p) {
			$hashed_subpackets .= (string)$p;
		}

		$body .= pack('n',strlen($hashed_subpackets)).$hashed_subpackets;

		return $body;
	}

	function calculate_trailer() {
		// The trailer is just the top of the body plus some crap
		$body = $this->body_start();

		return $body.chr(4).chr(0xff).pack('N',strlen($body));
	}

	static function class_for($tag)
	{
		return (isset(self::$subpacket_types[$tag]) AND class_exists($class='Leenooks\OpenPGP\SignaturePacket\\'.self::$subpacket_types[$tag].'Packet'))
			? $class
			: 'Leenooks\OpenPGP\SignaturePacket\Subpacket';
	}

	static function get_subpacket(&$input)
	{
		if (self::$DEBUG)
			dump(['method'=>__METHOD__,'input'=>$input]);

		$len = ord($input[0]);
		$length_of_length = 1;

		if (self::$DEBUG)
			dump(['len'=>$len]);

		// if($len < 192) One octet length, no furthur processing
		if ($len > 190 && $len < 255) { // Two octet length
			$length_of_length = 2;
			$len = (($len - 192) << 8) + ord($input[1]) + 192;
		}

		// Five octet length
		if ($len == 255) {
			$length_of_length = 5;
			$unpacked = unpack('N', substr($input, 1, 4));
			$len = reset($unpacked);
			if (self::$DEBUG)
				dump(['len'=>$len,'unpacked'=>$unpacked]);
		}

		$input = substr($input,$length_of_length); // Chop off length header
		$tag = ord($input[0]);

		$class = self::class_for($tag);

		if (self::$DEBUG)
			dump(['class'=>$class,'tag'=>$tag]);

		if ($class) {
			$packet = new $class;

			// In case we got the catch all class.
			if ($class == 'Leenooks\OpenPGP\SignaturePacket\Subpacket')
				$packet->setTag($tag);

			if ($packet->tag() !== $tag)
				throw new OpenPGP\Exceptions\PacketTagException(sprintf('Packet tag [%s] doesnt match tag [%s]?',$packet->tag(),$tag));
			//$packet->tag = $tag;				// @todo Tag should already be set.
			$packet->input = substr($input, 1, $len-1);
			$packet->length = $len-1;
			$packet->read();
			unset($packet->input);
			unset($packet->length);
		}

		// Chop off the data from this packet
		$input = substr($input,$len);

		return $packet;
	}

	/**
	 * @see http://tools.ietf.org/html/rfc4880#section-5.2.3.1
	 */
	static function get_subpackets($input)
	{
		$subpackets = array();

		while(($length = strlen($input)) > 0) {
			$subpackets[] = self::get_subpacket($input);

			// Parsing stuck?
			if ($length == strlen($input)) {
				break;
			}
		}

		return $subpackets;
	}

	function hash_algorithm_name()
	{
		return self::$hash_algorithms[$this->hash_algorithm];
	}

	function issuer()
	{
		foreach ($this->hashed_subpackets as $p) {
			if ($p instanceof SignaturePacket\IssuerPacket)
				return $p->data;
		}

		foreach($this->unhashed_subpackets as $p) {
			if ($p instanceof SignaturePacket\IssuerPacket)
				return $p->data;
		}

		return NULL;
	}

	function key_algorithm_name()
	{
		return PublicKeyPacket::$algorithms[$this->key_algorithm];
	}

	function read()
	{
		switch($this->version = ord($this->read_byte())) {
			case 2:
			case 3:
				if (ord($this->read_byte()) != 5) {
					throw new Exception("Invalid version 2 or 3 SignaturePacket");
				}

				$this->signature_type = ord($this->read_byte());
				$creation_time = $this->read_timestamp();
				$keyid = $this->read_bytes(8);
				$keyidHex = '';

				// Store KeyID in Hex
				for ($i=0;$i<strlen($keyid);$i++) {
					$keyidHex .= sprintf('%02X',ord($keyid[$i]));
				}

				$this->hashed_subpackets = [];
				$this->unhashed_subpackets = [
					new SignaturePacket\SignatureCreationTimePacket($creation_time),
					new SignaturePacket\IssuerPacket($keyidHex)
				];

				$this->key_algorithm = ord($this->read_byte());
				$this->hash_algorithm = ord($this->read_byte());
				$this->hash_head = $this->read_unpacked(2, 'n');
				$this->data = array();

				while (strlen($this->input)>0) {
					$this->data[] = $this->read_mpi();
				}

				break;

			case 4:
				$this->signature_type = ord($this->read_byte());
				$this->key_algorithm = ord($this->read_byte());
				$this->hash_algorithm = ord($this->read_byte());
				$this->trailer = chr(4).chr($this->signature_type).chr($this->key_algorithm).chr($this->hash_algorithm);

				$hashed_size = $this->read_unpacked(2, 'n');
				$hashed_subpackets = $this->read_bytes($hashed_size);
				$this->trailer .= pack('n', $hashed_size).$hashed_subpackets;
				$this->hashed_subpackets = self::get_subpackets($hashed_subpackets);

				$this->trailer .= chr(4).chr(0xff).pack('N', 6 + $hashed_size);

				$unhashed_size = $this->read_unpacked(2, 'n');
				$this->unhashed_subpackets = self::get_subpackets($this->read_bytes($unhashed_size));

				$this->hash_head = $this->read_unpacked(2, 'n');

				$this->data = array();

				while(strlen($this->input) > 0) {
					$this->data[] = $this->read_mpi();
				}

				break;
		}
	}

	/**
	 * $this->data must be set to the data to sign (done by constructor)
	 * $signers in the same format as $verifiers for Message.
	 */
	public function sign_data($signers)
	{
		$this->trailer = $this->calculate_trailer();
		$signer = $signers[$this->key_algorithm_name()][$this->hash_algorithm_name()];
		$this->data = call_user_func($signer,$this->data.$this->trailer);
		$unpacked = unpack('n', substr(implode('',$this->data),0,2));
		$this->hash_head = reset($unpacked);
	}
}