<?php
/**
 * This file contains the {@link EasyScriptCompressedDecoder} class.
 * @copyright (c) 2015, Scriptel Corporation.
 */
namespace com\scriptel;

/**
 * This class creates the live signature and coordinates that are generated by a compressed
 * ScripTouch EasyScript device.
 */
class EasyScriptCompressedDecoder implements EasyScriptDecoder{
    /**
     * The maximum value for the compressed device.
     */
    const MAXIMUM_COMPRESSED_VALUE = 2999;
    /**
     * Contains the value that occurs when the difference between current
     * x and previous x or current y and previous y in a coordinate is repeated.
     */
    const REPEAT_HALF = 59;
    /**
     * Contains the value that indicates there is a new stroke.
     */
    const NEW_STROKE_CODE = 60;
    /**
     * Contains the value that indicates that the sign of x is reversed.
     */
    const REVERSE_X_CODE = 61;
    /**
     * Contains the value that indicates that the sign of y is reversed.
     */
    const REVERSE_Y_CODE = 62;
    /**
     * Contains the value that indicates that the signs of x and y are reversed.
     */
    const REVERSE_X_AND_Y_CODE = 63;
    /**
     * Contains the value that should be converted to the number 61 when it occurs.
     */
    const REVERSE_X_CODE_SWAP = 3001;
    /**
     * Contains the value that should be converted to the number 62 when it occurs.
     */
    const REVERSE_Y_CODE_SWAP = 3002;
    /**
     * Contains the value that should be converted to the number 63 when it occurs.
     */
    const REVERSE_X_AND_Y_CODE_SWAP = 3003;
    /**
     * Contains the value that should be converted to the number 59 when it occurs.
     */
    const REPEAT_HALF_SWAP = 3004;
    /**
     * Contains the value that should be converted to the number 60 when it occurs.
     */
    const NEW_STROKE_CODE_SWAP = 3005;
    /**
     * Contains the value indicates that the user has canceled the signature.
     */
    const CANCEL_CODE = 3007;
    /**
     * This object reads the binary tree.
     * @var BinaryTreeReader
     */
    private $tree;
    /**
     * Default constructor, uses the STN protocol.
     * @var EasyScriptEventListener[].
     */
    private $coordinateReceiverListeners;
    /**
     * This is a string that contains the characters of the incoming signature.
     * @var string
     */
    private $streamedSignature;
    /**
     * Handles the binary stream that is used to find X and Y values in the binary tree.
     * @var boolean[]
     */
    private $binaryStream;
    /**
     * This is the used signature protocol.
     * @var SignatureProtocol
     */
    private $sigantureProtocol;
    /**
     * keyValues
     * @var KeyBoardDecoder
     */
    private $keyboardDecoder;
    /**
     * Flag indicates whether we are decoding x or y.
     * @var boolean
     */
    private $decodingX;
    /**
     * Determines if the sign of X is currently plus or minus.
     * @var integer
     */
    private $xSign;
    /**
     * Determines if the sign of X is currently plus or minus.
     * @var integer
     */
    private $ySign;
    /**
     * Handles that last value of X.
     * @var integer
     */
    private $previousX;
    /**
     * Handles that last value of Y.
     * @var integer
     */
    private $previousY;
    /**
     * Contains the position of the character that is being processed.
     * @var integer
     */
    private $charPosition;
    /**
     * An instance of the binary tree.
     * @var BinaryTree
     */
    private $nodePointer;
    /**
     * Contains the value after decompression.
     * @var integer
     */
    private $makeupValue;
    /**
     * Contains the value of x.
     * @var integer
     */
    private $x;
    /**
     * Contains the value of y.
     * @var integer
     */
    private $y;
    /**
     * This is the calculate coordinate.
     * @var Coordinate
     */
    private $currentPoint;
    /**
     * currently working on the terminator code
     * @var boolean
     */
    private $isMakeupCode;
    /**
     * This is true when there is a new stroke.
     * @var boolean
     */
    private $isNewStroke;

    /**
     * Constructor, creates a new instance of EasyScriptCompressedDecoder class.
     * @param SignatureProtocol $signatureProtocol Is the current used signature protocol
     * @param integer $pos the position of the first character of signature data in the entire stream. Follows the meta data.
     * @param EasyScriptEventListener $listOfCoordinateReceiverListener
     */
    public function __construct(SignatureProtocol $signatureProtocol, $pos, $listOfCoordinateReceiverListener){
        $this->tree = new BinaryTreeReader;
        $this->decodingX = TRUE;
        $this->xSign = 1;
        $this->ySign = 1;
        $this->previousX = 0;
        $this->previousY = 0;
        $this->currentPoint = new Coordinate(0,0);
        $this->isMakeupCode = FALSE;
        $this->isNewStroke = TRUE;
        $this->coordinateReceiverListeners = $listOfCoordinateReceiverListener;
        $this->sigantureProtocol = $signatureProtocol;
        $this->binaryStream = array();
        $this->charPosition = $pos;
        $this->keyboardDecoder = new KeyboardDecoder($this->getSigantureProtocol()->getXValues(), $this->getSigantureProtocol()->getYValues());
        $this->streamedSignature = '';
        $this->nodePointer = $this->tree->getTree();
    }

    /**
     * This method parses the compressed signature and decompressed it into coordinates
     * @param integer $byteOfBitStream One byte of the incoming bit stream
     * @return Coordinate.
     */
    public function addValueToSignature($byteOfBitStream){
        $this->addValueToStream($byteOfBitStream);
        $value = 0;
        while (count($this->binaryStream) > 0){
            if (!$this->isMakeupCode) {
                $value = $this->searchBinaryTree($this->binaryStream);
                if ($value < 0) {
                    return;     // need more bits
                }
                if ($value >= 64) {
                    $this->makeupValue = $value;
                    $this->isMakeupCode = TRUE;
                } else {
                    $this->makeupValue = 0;
                }
            }
            if ($this->isMakeupCode) {
                $value = $this->searchBinaryTree($this->binaryStream);
                if ($value < 0) {
                    return;     // need more bits
                }
                $value += $this->makeupValue;
                $this->isMakeupCode = FALSE;
                $this->makeupValue = 0;
            }
            if($this->decodingX) {
                switch ($value) {
                    case self::NEW_STROKE_CODE:
                        $this->isNewStroke = TRUE;
                        break;
                    case self::CANCEL_CODE:
                        foreach ($this->coordinateReceiverListeners as $listener) {
                            $listener->cancel();
                        }
                        break;
                    case self::REVERSE_X_CODE:
                        $this->xSign = -$this->xSign;
                        break;
                    case self::REVERSE_Y_CODE:
                        $this->ySign = -$this->ySign;
                        break;
                    case self::REVERSE_X_AND_Y_CODE:
                        $this->xSign = -$this->xSign;
                        $this->ySign = -$this->ySign;
                        break;
                    case self::REPEAT_HALF:
                        $this->x = $this->previousX;
                        $this->decodingX = FALSE;
                        break;
                    case self::REPEAT_HALF_SWAP:
                        $this->x = self::REPEAT_HALF;
                        $this->decodingX = FALSE;
                        break;
                    case self::REVERSE_X_CODE_SWAP:
                        $this->x = self::REVERSE_X_CODE;
                        $this->decodingX = FALSE;
                        break;
                    case self::REVERSE_Y_CODE_SWAP:
                        $this->x = self::REVERSE_Y_CODE;
                        $this->decodingX = FALSE;
                        break;
                    case self::REVERSE_X_AND_Y_CODE_SWAP:
                        $this->x = self::REVERSE_X_AND_Y_CODE;
                        $this->decodingX = FALSE;
                        break;
                    case self::NEW_STROKE_CODE_SWAP:
                        $this->x = self::NEW_STROKE_CODE;
                        $this->decodingX = FALSE;
                        break;
                    default:
                        $this->x = $value;
                        $this->decodingX = FALSE;
                        break;
                } // end of switch
            } else {
                switch ($value) {
                    case self::REPEAT_HALF:
                        $this->y = $this->previousY;
                        break;
                    case self::REPEAT_HALF_SWAP:
                        $this->y = self::REPEAT_HALF;
                        break;
                    default:
                        $this->y = $value;
                        $this->previousY = $this->y;
                        break;
                }
                $this->decodingX = TRUE;
                $this->previousX = $this->x;
                $this->x *= $this->xSign;
                $this->y *= $this->ySign;
                $this->currentPoint->setX($this->currentPoint->getX() + $this->x);
                $this->currentPoint->setY($this->currentPoint->getY() + $this->y);
                $this->x = 0;
                $this->y = 0;
                if ($this->isNewStroke){
                    foreach ($this->coordinateReceiverListeners as $listener) {
                        $listener->newStroke();
                    }
                    $this->isNewStroke = FALSE;
                }
                $scaledCoordinate = new Coordinate(
                        $this->currentPoint->getX() * $this->sigantureProtocol->getWidth() / self::MAXIMUM_COMPRESSED_VALUE,
                        $this->currentPoint->getY() * $this->sigantureProtocol->getHeight() / self::MAXIMUM_COMPRESSED_VALUE
                );
                foreach ($this->coordinateReceiverListeners as $listener) {
                    $listener->receiveCoordinate($scaledCoordinate);
                }
            }
        }
    }

    /**
     * This method parses the binary tree to find the value corresponding to a binary string
     * @param array $stream is a list of boolean values represents the binary stream.
     * @return integer The value corresponding to the binary stream. If there weren't enough bits, returns -1, but remembers where it left off.
     */
    public function searchBinaryTree($stream){
        $this->binaryStream = $stream;
        while(!empty($this->binaryStream)){
            $currentBit = $this->binaryStream[0];
            $this->nodePointer = $currentBit ? $this->nodePointer->getRight() : $this->nodePointer->getLeft();
            array_splice($this->binaryStream, 0, 1);
            if ($this->nodePointer->getLeft() == NULL && $this->nodePointer->getRight() == NULL){
                $value = $this->nodePointer->getValue();
                $this->nodePointer = $this->tree->getTree();
                return $value;
            }
        }
        return -1;
    }

    /**
     * This method converts a decimal number into a 9-bit format string.
     * @param integer $decNumber Is an integer number represents the format (num1 * 23 + num2).
     */
    public function addValueToStream($decNumber){
        $binary = decbin($decNumber);
        $binary9Bit = substr('000000000', strlen($binary)) . $binary;
        for ($i = 0, $strLength = strlen($binary9Bit); $i < $strLength; $i++){
            $currentBit = substr($binary9Bit, $i, 1) === '0' ? FALSE : TRUE;
            array_push($this->binaryStream, $currentBit);
        }
    }

    /**
     * This method reads characters from signature and puts them in 9-bit binary value
     * It requires 2 or 3 characters and should only be given as many.
     * @param string $signature Represents the compressed signature.
     * @return integer The converted values of the characters or -1 if it could not be done...a third character may be required.
     * @throws SignatureInvalidException
     */
    public function convertBodnarCharsToValue($signature){
        if (strlen($signature) < 2){
            return -1;
        }
        try {
            $value = $this->keyboardDecoder->getValue(substr($signature, 0, 1)) + $this->keyboardDecoder->getValue(substr($signature, 1, 1));
            if ($value === 505){
                if (strlen($signature) > 2) {
                    $value += $this->keyboardDecoder->getValue(substr($signature, 2, 1));
                } else {
                    return -1;
                }
            }
        } catch (SignatureInvalidException $ex) {
            throw new SignatureInvalidException('There is an error in position: ', $this->charPosition);
        }
        return $value;
    }

    /**
     * This method parses the incoming signature in the batch mode. It converts the signature to
     * single chars to handle it in the streaming mode.
     * @param str The incoming signature.
     * @throws SignatureInvalidException
     */
    public function parse($str){
        for ($i = 0, $strLength = strlen($str); $i < $strLength; $i++) {
            $c = substr($str, $i, 1);
            $this->parseSignature($c);
        }
    }

    /**
     * This method parses the signature character by character to decode it.
     * @param ch A char read from the signature.
     * @throws SignatureInvalidException
     */
    public function parseSignature($ch){
        $this->charPosition++;
        if ($ch === $this->sigantureProtocol->getEndStream()) {
            /* @var EasyScriptEventListener */
            foreach ($this->coordinateReceiverListeners as $listener){
                $listener->endOfSignature();
            }
            return;     // end of signature
        }
        $this->streamedSignature .= $ch;
        if (strlen($this->streamedSignature) < 2) {
            return;     // we know we need at least 2 characters
        }
        $result = $this->convertBodnarCharsToValue($this->streamedSignature);
        if ($result >= 0) {
            $this->addValueToSignature($result);
            $this->streamedSignature = '';
        }
    }

    /**
     * Gets the current signature protocol.
     * @return the signatureProtocol
     */
    function getSigantureProtocol() {
        return $this->sigantureProtocol;
    }


}
