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

/*
 * Load all files that are part of this library.
 */
require_once 'CardSwipeProtocol.php';
require_once 'SignatureProtocol.php';
require_once 'STNCardSwipeProtocol.php';
require_once 'STNSignatureProtocol.php';
require_once 'BoundingBox.php';
require_once 'EasyScriptDecoder.php';
require_once 'EasyScriptCardState.php';
require_once 'EasyScriptCompressedDecoder.php';
require_once 'EasyScriptUncompressedDecoder.php';
require_once 'EasyScriptEventListener.php';
require_once 'EasyScriptStreamingState.php';
require_once 'BatchListener.php';
require_once 'SignatureMetaData.php';
require_once 'Signature.php';
require_once 'CardSwipeInvalidException.php';
require_once 'CardSwipe.php';
require_once 'FinancialCard.php';
require_once 'FinancialCardIssuer.php';
require_once 'FinancialCardTrackOne.php';
require_once 'FinancialCardTrackTwo.php';
require_once 'IdentificationCard.php';
require_once 'SignatureInvalidException.php';
require_once 'IdentificationCardTrackOne.php';
require_once 'IdentificationCardTrackTwo.php';
require_once 'IdentificationCardTrackThree.php';
require_once 'IdentificationSex.php';
require_once 'BinaryTreeReader.php';
require_once 'BinaryTree.php';
require_once 'Coordinate.php';
require_once 'KeyboardDecoder.php';

/**
 * This class parses the signatures or swipe cards or both and returns the coordinates of signature or
 * info on the cards.
 */
class EasyScript {
    /**
     * This contains the current version of the library.
     */
    const LIBRARY_VERSION = '3.5.29';
    /**
     * This contains the date on which the library was built (YYYY-MM-DD HH:MM:SS).
     */
    const LIBRARY_BUILD_DATE = '2022-08-30 17:12:23-0400';
    /**
     * The protocol by which to decode the incoming signature strings.
     * @var SignatureProtocol.
     */
    private $signatureProtocol;
    /**
     * This is the protocol by which we'll decode incoming card swipe strings.
     * @var CardSwipeProtocol
     */
    private $cardProtocol;
    /**
     * Keeps track of the current card state.
     * @var EasyScriptCardState.
     */
    private $cardState = EasyScriptCardState::UNKNOWN;
    /**
     * Contains the incoming signature.
     * @var string
     */
    private $signatureBuffer;
    /**
     * Keeps a card buffer we'll use to check to make sure we're looking at a card.
     * @var string
     */
    private $cardBuffer = '';
    /**
     * Contains the header of the signature.
     * @var SignatureMetaData
     */
    private $metadata;
    /**
     * An instance EasyScriptDecoder interface.
     * @var EasyScriptDecoder
     */
    private $decoder;
    /**
     * A list of Coordinate Listeners.
     * @var EasyScriptEventListener[]
     */
    private $eventListeners;
    /**
     * The curent position in the stream.
     * @var integer
     */
    private $currentPosition = 0;
    /**
     * The current state of the streaming parser.
     * @var EasyScriptStreamingState
     */
    private $currentState;

    /**
     * Constructor, creates a new instance of EasyScript class.
     * @param STNSignatureProtocol $signatureProtocol Protocol for the device you're trying to decode signatures for.
     * @param STNCardSwipeProtocol $cardProtocol Protocol for the device you're trying to decode card swipes for.
     */
    public function __construct($signatureProtocol = false, $cardProtocol = false) {
        if($signatureProtocol === false) {
            $signatureProtocol = new STNSignatureProtocol();
        }
        if($cardProtocol === false) {
            $cardProtocol = new STNCardSwipeProtocol();
        }
        $this->currentState = new EasyScriptStreamingState(EasyScriptStreamingState::UNKNOWN);
        $this->signatureProtocol = $signatureProtocol;
        $this->cardProtocol = $cardProtocol;
        $this->eventListeners = array();
        $this->clearBuffers();
        $this->metadata = new SignatureMetaData('', '', '');
        $this->state = new EasyScriptCardState(EasyScriptCardState::UNKNOWN);
    }

    /**
     * This method takes an EasyScript string and attempts to to parse it into a Signature
     * or a CardSwipe object.
     * @param string $sig The signature or card swipe data that would be decoded.
     * @return Signature | CardSwipe
     * @throws SignatureInvalidException
     */
    public function parse($sig) {
        $this->eventListeners = $this->getEventListeners();
        if ($sig === NULL){
            throw new SignatureInvalidException('Signature or Card data was null');
        }
        if (empty($sig)){
            throw new SignatureInvalidException('Signature or Card data was empty');
        }
        if(substr($sig, 0, 1) != $this->signatureProtocol->getStartStream() && substr($sig, 0, 1) != $this->cardProtocol->getStartStream()){
            throw new SignatureInvalidException('Signature does not start with the start sentinel in position: ' . $this->currentPosition);
        }
        /* @var $listener BatchListener */
        $listener = new BatchListener();
        $this->addListener($listener);
        for ($i = 0; $i < strlen($sig); $i++){
            $this->parseCharacter(substr($sig, $i, 1));
        }
        $this->removeListener($listener);
        if ($listener->isDone()){
                return new Signature($this->metadata, $listener->getStrokes());
        } else {
            if ($listener->getSwipe() !== NULL){
                return $listener->getSwipe();
            } else {
                if (!$listener->isDone()){
                    throw new SignatureInvalidException('No end of signature sentinal found!');
                } else if ($listener->isCanceled()){
                    return NULL;
                }
            }
        }
    }

    /**
     * This method allows you to stream in a set of characters and the library
     * will attempt to decode signatures and card swipes as the characters are
     * passed in. To use this method be sure to implement your own EasyScriptEventListener
     * and pass it to addListener() prior to passing characters to this function.
     * This listener will be called when specific events are generated by decoding
     * the stream.
     * @param string $chr Next character in the stream to parse.
     * @throws SignatureInvalidException Thrown in the event an unexpected condition occurs while parsing the stream.
     */
    public function parseCharacter($chr) {
        try{
            $this->currentPosition++;
            switch($this->currentState){
                case EasyScriptStreamingState::UNKNOWN:
                   $this->transitionFromUnknownToSentinel($chr);
                   break;
                case EasyScriptStreamingState::CARD_SENTINEL:
                   $this->state = $this->parseCard($chr);
                   $this->transitionFromCardSentinelToData($this->state);
                   break;
                case EasyScriptStreamingState::SIGNATURE_SENTINEL:
                   $this->tranistionFromSignatureSentinelToProtocolVersion($chr);
                   break;
                case EasyScriptStreamingState::PROTOCOL_VERSION:
                    $this->transitionFromProtocolVersionToModel($chr);
                    break;
                case EasyScriptStreamingState::MODEL:
                    $this->transitionFromModelToFirmwareVersion($chr);
                    break;
                case EasyScriptStreamingState::FIRMWARE_VERSION:
                    $this->transitionFromFirmwareVersionToSignature($chr);
                    break;
                case EasyScriptStreamingState::SIGNATURE_UNCOMPRESSED;
                case EasyScriptStreamingState::SIGNATURE_COMPRESSED:
                    $this->signatureCompressedState($chr);
                    break;
                case EasyScriptStreamingState::CARD_INTERRUPTING_UNCOMPRESSED:
                case EasyScriptStreamingState::CARD_INTERRUPTING_COMPRESSED:
                    $this->interruptedSignature($chr);
                    break;
                default:
                    break;
            }

        } catch (SignatureInvalidException $ex) {
            $this->currentState = EasyScriptStreamingState::UNKNOWN;
            throw $ex;
        }
    }

    /**
     * This method attempts to parse a magnetic card swipe from a ScripTouch
     * device with a magnetic strip reader.
     * @param string $swipe ScripTouch magnetic strip data.
     * @return CardSwipe Parsed card swipe.
     * @throws CardSwipeInvalidException Thrown in the event the card couldn't be properly parsed.
     */
    public function parseCardSwipe($swipe) {
        $lastCharPos = strlen($swipe)-1;
        $lastChar = substr($swipe , $lastCharPos, 1);
        if($swipe === NULL || empty($swipe)) {
            throw new CardSwipeInvalidException("Card swipe stream is empty.");
        } else if(substr($swipe, 0, 1) !== $this->cardProtocol->getStartStream()) {
            throw new CardSwipeInvalidException("Card swipe stream doesn't start with the correct character (" . substr($swipe, 0, 1) . " != " . $this->cardProtocol->getStartStream() . ")");
        } else if($lastChar !== $this->cardProtocol->getEndStream()) {
            throw new CardSwipeInvalidException("Card swipe stream doesn't end with the correct character (" . substr($swipe, 0, 1) . " != " . $this->cardProtocol->getEndStream() . ")");
        } else if(!strcasecmp(substr($swipe, 1, strlen($this->cardProtocol->getSentinel())+1), $this->cardProtocol->getSentinel())) {
            throw new CardSwipeInvalidException("Card swipe stream doesn't start with the correct sentinel in position 1");
        }

        $result = new CardSwipe();
        $result->setProtocolVersion(substr($swipe, 8, 1));
        $result->setCardData(substr($swipe, 10, strlen($swipe)-1));
        /* @var $financialCard FinancialCard */
        $financialCard = FinancialCard::parse($result->getCardData());
        $result->setFinancialCard($financialCard);
        $result->setIdentificationCard(IdentificationCard::parse($result->getCardData()));

        return $result;
    }

    /**
     * Adds the listener to the list of listeners.
     * @param EasyScriptEventListener $cr
     */
    public function addListener($cr) {
        array_push($this->eventListeners, $cr);
    }

    /**
     * Removes a listener from the list of listeners.
     * @param EasyScriptEventListener $cr
     */
    public function removeListener($cr) {
        $retr = array_search($cr, $this->eventListeners, true);
        if($retr !== false) {
            array_splice($this->eventListeners, $retr, 1);
        }
    }


//*******************  SIGNATURE STATE TRANSITION FUNCTIONS  **********************************//

    /**
     * Transitions from UNKNOWN state to SIGNATURE_SENTINEL state or CARD_SENTINEL state.
     * @param string $chr Current character in the stream.
     */
    private function transitionFromUnknownToSentinel($chr){
        if ($chr == $this->signatureProtocol->getStartStream()) {
            $this->currentState = EasyScriptStreamingState::SIGNATURE_SENTINEL;
            $this->currentPosition = 1;
            $this->SigantureBuffer = "";
        } else if ($chr == $this->cardProtocol->getStartStream()){
            $this->currentState = EasyScriptStreamingState::CARD_SENTINEL;
            $this->currentPosition = 1;
            $this->signatureBuffer = "";
            $this->cardBuffer .= $chr;
            $this->cardState = EasyScriptCardState::UNKNOWN;
        }
    }

    /**
     * Transitions from CARD_SENTINEL state to CARD_DATA state.
     * @param EasyScriptCardState $state Final state of the card.
     */
    private function transitionFromCardSentinelToData($state) {
        if($state === EasyScriptCardState::CARD_PROCESSED || $state === EasyScriptCardState::NOT_A_CARD){
            $this->cardState = EasyScriptCardState::UNKNOWN;
            $this->currentState = EasyScriptStreamingState::UNKNOWN;
        }
    }

    /**
     * Transitions from SIGNATURE_SENTINEL state to PROTOCOL_VERSION state.
     * @param string $chr Current character in the stream.
     * @throws SignatureInvalidException
     */
    private function tranistionFromSignatureSentinelToProtocolVersion($chr) {
        if($chr === $this->signatureProtocol->getPenUp()) {
            if($this->signatureBuffer === $this->signatureProtocol->getSentinel()) {
                $this->currentState = EasyScriptStreamingState::PROTOCOL_VERSION;
                $this->signatureBuffer = '';
                $this->metadata = new SignatureMetaData('', '', '');
            } else {
                throw new SignatureInvalidException("Signature sentinel doesn't appear to be correct: " . $this->signatureBuffer);
            }
        } else {
            $this->signatureBuffer .= $chr;
        }
    }

    /**
     * Transitions from PROTOCOL_VERSION state to MODEL state.
     * @param string $chr Current character in the stream.
     */
    private function transitionFromProtocolVersionToModel($chr) {
        if($chr === $this->signatureProtocol->getPenUp()) {
            $this->metadata->setProtocolVersion($this->signatureBuffer);
            $this->currentState = EasyScriptStreamingState::MODEL;
            $this->signatureBuffer = '';
        } else {
            $this->signatureBuffer .= $chr;
        }
    }
    /**
     * Transitions from MODEL state to FIRMWARE_VERSION state.
     * @param string $chr Current character in the stream.
     */
    private function transitionFromModelToFirmwareVersion($chr) {
        if($chr === $this->signatureProtocol->getPenUp()) {
            $this->metadata->setModel($this->signatureBuffer);
            $this->signatureBuffer = '';
            $this->currentState = EasyScriptStreamingState::FIRMWARE_VERSION;
        } else {
            $this->signatureBuffer .= $chr;
        }
    }

    /**
     * Transitions from FIRMWARE_VERSION to SIGNATURE_UNCOMPRESSED or SIGNATURE_COMPRESSED.
     * @param string $chr Current character in the stream.
     * @var $eventListeners EasyScriptEventListener
     * @throws SignatureInvalidException
     */
    private function transitionFromFirmwareVersionToSignature($chr) {
        if($chr === $this->signatureProtocol->getPenUp()) {
            $this->metadata->setVersion($this->signatureBuffer);
            $this->signatureBuffer = '';
            //We could have a beacon here, strip the protocol version
            //down to just one character.
            if(!empty($this->metadata->getProtocolVersion())){
                $this->metadata->setProtocolVersion(substr($this->metadata->getProtocolVersion(), 0, 1));
            }
            /* @var $listener EasyScriptEventListener */
            foreach($this->eventListeners as $listener) {
                $listener->signatureMetaData($this->metadata);
            }

            if("A" === $this->metadata->getProtocolVersion() || "B" === $this->metadata->getProtocolVersion() || "C" === $this->metadata->getProtocolVersion()) {
                //Uncompressed
                $this->currentState = EasyScriptStreamingState::SIGNATURE_UNCOMPRESSED;
                $this->decoder = new EasyScriptUncompressedDecoder($this->signatureProtocol, $this->currentPosition, $this->eventListeners);
            } else if("D" === $this->metadata->getProtocolVersion() || "E" === $this->metadata->getProtocolVersion()) {
                //Compressed
                $this->currentState = EasyScriptStreamingState::SIGNATURE_COMPRESSED;
                $this->decoder = new EasyScriptCompressedDecoder($this->signatureProtocol, $this->currentPosition, $this->eventListeners);
            } else {
                throw new SignatureInvalidException("Unrecognized protocol version: " . $this->metadata->getProtocolVersion() . " . Protocol version should be A, B, C, D or E.");
            }
        } else {
            $this->signatureBuffer .= $chr;
        }
    }
    /**
     * The state in which the signature is compressed.
     * @param string $chr Current character in the stream.
     * @var $eventListeners EasyScriptEventListener
     */
    private function signatureCompressedState($chr) {
        if($chr === $this->signatureProtocol->getStartStream()) {
            //Appears to be a new signature, assume the previous one was canceled.
            foreach($this->eventListeners as $r) {
                $r->cancel();
            }
            $this->currentState = EasyScriptStreamingState::UNKNOWN;
            $this->signatureBuffer = '';
            //Re-enter this function.
            $this->parseCharacter($chr);
            return;
        } else if ($chr === $this->signatureProtocol->getEndStream()) {
            foreach($this->eventListeners as $el){
                $el->endOfSignature();
            }
            $this->currentState = EasyScriptStreamingState::UNKNOWN;
            $this->signatureBuffer = '';
            return;
        } else if ($chr === $this->signatureProtocol->getCancel()) {
            foreach($this->eventListeners as $el){
                $el->cancel();
            }
            $this->currentState = EasyScriptStreamingState::UNKNOWN;
            $this->signatureBuffer = '';
            return;
        }
        if($chr === $this->cardProtocol->getStartStream() && $this->cardState !== EasyScriptCardState::NOT_A_CARD) {
            //This might be a card, give the card parser a swing at this.
            $this->currentState = ($this->currentState === EasyScriptStreamingState::SIGNATURE_UNCOMPRESSED) ? EasyScriptStreamingState::CARD_INTERRUPTING_UNCOMPRESSED : EasyScriptStreamingState::CARD_INTERRUPTING_COMPRESSED;
            $this->cardState = EasyScriptCardState::UNKNOWN;
            $this->cardBuffer = '';
            $this->cardBuffer .= $chr;
        } else {
            $this->decoder->parseSignature($chr);
        }
    }

    /**
     * The state in which the signature is interrupted.
     * @param string $chr Current character in the stream.
     */
    private function interruptedSignature($chr) {
        $this->state = $this->parseCard($chr);
        if($this->state === EasyScriptCardState::NOT_A_CARD) {
            //We were mistaken, this wasn't a card swipe after all.
            $this->currentState = ($this->currentState === EasyScriptStreamingState::CARD_INTERRUPTING_UNCOMPRESSED) ? EasyScriptStreamingState::SIGNATURE_UNCOMPRESSED : EasyScriptStreamingState::SIGNATURE_COMPRESSED;
            for($i = 0; $i < strlen($this->cardBuffer); $i++) {
                $this->parseCharacter(substr($this->cardBuffer, $i, 1));
            }
            $this->cardState = EasyScriptCardState::UNKNOWN;
            $this->cardBuffer = '';
        } else if($this->state === EasyScriptCardState::CARD_PROCESSED) {
            $this->currentState = ($this->currentState === EasyScriptStreamingState::CARD_INTERRUPTING_UNCOMPRESSED) ? EasyScriptStreamingState::SIGNATURE_UNCOMPRESSED : EasyScriptStreamingState::SIGNATURE_COMPRESSED;
        }
    }

    /**
     * This method parses the card swipe.
     * @param string $chr Current character in the stream.
     * @return EasyScriptCardState
     */
    private function parseCard($chr) {
       $this->cardBuffer .= $chr;
        switch($this->cardState) {
            case EasyScriptCardState::UNKNOWN:
                if($chr === substr($this->cardProtocol->getSentinel(), strlen($this->cardBuffer) - 2, 1)) {
                    $this->cardState = EasyScriptCardState::CARD_DATA;
                    return EasyScriptCardState::CARD_DATA;
                }
                $this->cardState = EasyScriptCardState::NOT_A_CARD;
                return EasyScriptCardState::NOT_A_CARD;
            case EasyScriptCardState::CARD_DATA:
                if($chr === $this->cardProtocol->getEndStream()) {
                try {
                    $swipe = $this->parseCardSwipe($this->cardBuffer);
                    foreach ($this->eventListeners as $r) {
                        $r->cardSwipe($swipe);
                    }
                } catch(CardSwipeInvalidException $e) {

                }
                $this->cardState = EasyScriptCardState::CARD_PROCESSED;
                return EasyScriptCardState::CARD_PROCESSED;
                }
                break;
            default:
                return EasyScriptCardState::UNKNOWN;
        }
        return EasyScriptCardState::UNKNOWN;
    }

    /**
     * Returns a list of event listeners.
     * @return EasyScriptEventListener[]
     */
    function getEventListeners() {
        return $this->eventListeners;
    }

    /**
     * Initialize the instance variables.
     */
    private function clearBuffers() {
        $this->signatureBuffer = NULL;
        $this->currentState = EasyScriptStreamingState::UNKNOWN;
        $this->currentPosition = 0;
    }
}
