/*
 * Scriptel Coorporation - 2015.
 */
package com.scriptel.easyscript;

import java.util.*;

/**
 * This class creates the live signature and coordinates that are generated by a compressed 
 * ScripTouch EasyScript device. 
 */
public final class EasyScriptCompressedDecoder implements EasyScriptDecoder {
    /**
     * The maximum value for the compressed device.
     */
    private static final int 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.
     */
    private static final int REPEAT_HALF = 59;
    /**
     * Contains the value that indicates there is a new stroke.
     */
    private static final int NEW_STROKE_CODE = 60;
    /**
     * Contains the value that indicates that the sign of x is reversed.
     */
    private static final int REVERSE_X_CODE = 61;
    /**
     * Contains the value that indicates that the sign of y is reversed.
     */
    private static final int REVERSE_Y_CODE = 62;
    /**
     * Contains the value that indicates that the signs of x and y are reversed.
     */
    private static final int REVERSE_X_AND_Y_CODE = 63;
    /**
     * Contains the value that should be converted to the number 61 when it occurs.
     */
    private static final int REVERSE_X_CODE_SWAP = 3001;
    /**
     * Contains the value that should be converted to the number 62 when it occurs.
     */
    private static final int REVERSE_Y_CODE_SWAP = 3002;
    /**
     * Contains the value that should be converted to the number 63 when it occurs.
     */
    private static final int REVERSE_X_AND_Y_CODE_SWAP = 3003;
    /**
     * Contains the value that should be converted to the number 59 when it occurs.
     */
    private static final int REPEAT_HALF_SWAP = 3004;
    /**
     * Contains the value that should be converted to the number 60 when it occurs.
     */
    private static final int NEW_STROKE_CODE_SWAP = 3005;
    /**
     * Contains the value that indicates that the user has canceled the signature.
     */
    private static final int CANCEL_CODE = 3007;
    /**
     * This object reads the binary tree.
     */
    private final BinaryTreeReader tree = new BinaryTreeReader();
    /**
     * Default constructor, uses the STN protocol.
    */
    protected List<EasyScriptEventListener> coordinateReceiverListeners = null;     
    /**
     * This is s string builder that contains the characters of the incoming signature. 
     */    
    private final StringBuilder streamedSignature = new StringBuilder();
    /**
     * Handles the binary stream that is used to find X and Y values in the binary tree.
     */
    private final LinkedList<Boolean> stream;
    /**
     * This is the used signature protocol.
     */
    private final SignatureProtocol signatureProtocol;
    /**
     * keyValues
     */
    private final KeyboardDecoder keyboardDecoder;
    /**
     * Flag indicates whether we are decoding x or y.
     */
    private boolean decodingX =  true;
    /**
     * Determines if the sign of X is currently plus or minus.
     */
    private int xSign = 1;
    /**
     * Determines if the sign of X is currently plus or minus.
     */
    private int ySign = 1;
    /**
     * Handles that last value of X.
     */
    private int previousX = 0;
    /**
     * Handles that last value of Y.
     */
    private int previousY = 0;
    /**
     * Contains the position of the character that is being processed.
     */
    private int charPosition;
    /**
     * An instance of the binary tree.
     */
    private BinaryTree nodePointer;
    /**
     * Contains the value after decompression.
     */
    private int makeupValue;
    /**
     * Contains the values of x and y respectively.
     */
    private int x, y;
    /**
     * This is the calculate coordinate.
     */
    private final Coordinate currentPoint = new Coordinate(0,0);
    /**
     * currently working on the terminator code
     */
    boolean isMakeupCode = false;
    /**
     * This is true when there is a new stroke.
     */
    boolean isNewStroke = true;
    /**
     * Constructor, creates a new instance of EasyScriptEncoder class.
     * @param signatureProtocol Is the current used signature protocol
     * @param position the position of the first character of signature data in the entire stream. Follows the meta data.
     * @param listOfCoordinateReceiverListener Interested listeners.
     */
    public EasyScriptCompressedDecoder(SignatureProtocol signatureProtocol, int position, List<EasyScriptEventListener> listOfCoordinateReceiverListener) {
        coordinateReceiverListeners = listOfCoordinateReceiverListener;
        this.signatureProtocol = signatureProtocol;
    	stream = new LinkedList<Boolean>();       
        charPosition = position;
        keyboardDecoder = new KeyboardDecoder(getSignatureProtocol().getXValues(), getSignatureProtocol().getYValues());
        nodePointer = tree.getTree();
    }
    
    /**
     * This method parses the binary tree to find the value corresponding to a binary string
     * @param stream a list of boolean values represents the binary stream.
     * @return The integer value corresponding to the binary stream. If there weren't enough bits, returns -1, but remembers where it left off.
     */
    protected int searchBinaryTree(List<Boolean> stream) {
        while(!stream.isEmpty()) {
            boolean currentBit = stream.get(0);
            nodePointer = currentBit ? nodePointer.getRight() : nodePointer.getLeft();
            stream.remove(0);
            if(nodePointer.getLeft() == null && nodePointer.getRight() == null) {
                int value = nodePointer.getValue();
                nodePointer = tree.getTree();
                return value;
            } 
        }
        return -1;		
    }

    /**
     * This method parses the compressed signature and decompressed it into coordinates
     * @param byteOfBitStream One byte of the incoming bit stream
     * @throws SignatureInvalidException
     */
    private void addValueToSignature(int byteOfBitStream) throws SignatureInvalidException {        
        addValueToStream(byteOfBitStream);
        int value = 0;
        while (!stream.isEmpty()){
            if (!isMakeupCode) {
                value = searchBinaryTree(stream);  
                if (value < 0) {
                    return;     // need more bits
                }
                if (value >= 64) {
                    makeupValue = value;
                    isMakeupCode = true;
                } else {
                    makeupValue = 0;
                }
            }
            if (isMakeupCode) {
                value = searchBinaryTree(stream);
                if (value < 0) {
                    return;     // need more bits
                }
                value += makeupValue;
                isMakeupCode = false;
                makeupValue = 0;
            }
            if(decodingX) {
                switch (value) {
                    case NEW_STROKE_CODE:
                        isNewStroke = true;
                        break;
                    case CANCEL_CODE:
                        for (EasyScriptEventListener listener : coordinateReceiverListeners) {
                            listener.cancel();
                        }
                        break;
                    case REVERSE_X_CODE:
                        xSign = -xSign;
                        break;
                    case REVERSE_Y_CODE:
                        ySign = -ySign;
                        break;
                    case REVERSE_X_AND_Y_CODE:
                        xSign = -xSign;
                        ySign = -ySign;
                        break;
                    case REPEAT_HALF:
                        x = previousX;
                        decodingX = false;
                        break;
                    case REPEAT_HALF_SWAP:
                        x = REPEAT_HALF;
                        decodingX = false;
                        break;
                    case REVERSE_X_CODE_SWAP:
                        x = REVERSE_X_CODE;
                        decodingX = false;
                        break;
                    case REVERSE_Y_CODE_SWAP:
                        x = REVERSE_Y_CODE;
                        decodingX = false;
                        break;
                    case REVERSE_X_AND_Y_CODE_SWAP:
                        x = REVERSE_X_AND_Y_CODE; 
                        decodingX = false;
                        break;
                    case NEW_STROKE_CODE_SWAP:
                        x = NEW_STROKE_CODE;
                        decodingX = false;
                        break;
                    default:
                        x = value;
                        decodingX = false;
                        break;
                } // end of switch 
            } else {
                switch (value) {
                    case REPEAT_HALF:
                        y = previousY;
                        break;
                    case REPEAT_HALF_SWAP:
                        y = REPEAT_HALF;
                        break;
                    default:
                        y = value;
                        previousY = y;
                        break;
                }                               
                decodingX = true; 
                previousX = x;        
                x *= xSign;
                y *= ySign;
                currentPoint.setX(currentPoint.getX() + x);
                currentPoint.setY(currentPoint.getY() + y);
                x = 0;
                y = 0;
                if (isNewStroke){
                    for (EasyScriptEventListener listener : coordinateReceiverListeners) {
                        listener.newStroke();
                    }
                    isNewStroke = false;
                }
                Coordinate scaledCoordinate = new Coordinate(
                        currentPoint.getX() * signatureProtocol.getWidth() / MAXIMUM_COMPRESSED_VALUE,
                        currentPoint.getY() * signatureProtocol.getHeight() / MAXIMUM_COMPRESSED_VALUE
                );
                for (EasyScriptEventListener listener : coordinateReceiverListeners) {
                    listener.receiveCoordinate(scaledCoordinate);
                } 
            } 
        }
    }
   
    /**
     * 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 signature String Represents the compressed signature     
     * @return the converted values of the characters or -1 if it could not be done...a third character may be required.
     * @throws SignatureInvalidException Thrown in the event of a problem.
     */
    protected int convertBodnarCharsToValue(StringBuilder signature) throws SignatureInvalidException {
        if (signature.length() < 2){
            return -1;   
        }
        int value;
        try {
            value = keyboardDecoder.getValue(signature.charAt(0)) + keyboardDecoder.getValue(signature.charAt(1));          
            if (value == 505) {
                if (signature.length() > 2) {
                    value += keyboardDecoder.getValue(signature.charAt(2));
                } else {
                    return -1;
                }
            }
        } catch (SignatureInvalidException ex) {
            SignatureInvalidException rethrow = new SignatureInvalidException(ex.toString(), charPosition);
            rethrow.initCause(ex);
            throw rethrow;
        }
        return value;
    }
    
    /**
     * Gets the current signature protocol.
     * @return the signatureProtocol
     */
    public SignatureProtocol getSignatureProtocol() {
        return signatureProtocol;
    }
     /**
     * This method converts a decimal number into a 9-bit format string.
     * @param decNumber Is an integer number represents the format (num1 * 23 + num2) 
     */
    private void addValueToStream(int decNumber) {
        boolean currentBit;
        String binary = Integer.toBinaryString(decNumber);
        binary = ("000000000").substring(binary.length()) + binary;
        for (int i = 0; i < binary.length(); i++){
            currentBit = binary.charAt(i) != '0';
            stream.add(currentBit);
        }
        
    }
    
    /**
     * This method parses the signature character by character to decode it. 
     * @param ch A char read from the signature.
     * @throws SignatureInvalidException Thrown in the event of a problem.
     */
    @Override
    public void parseSignature(char ch) throws SignatureInvalidException {
        charPosition++;
        if (ch == signatureProtocol.getEndStream()) {
            //End of Signature
            for (EasyScriptEventListener listener : coordinateReceiverListeners) { 
                listener.endOfSignature();
            }
            return;
        } else if(ch == signatureProtocol.getCancel()) {
            //Signature cancelled.
            for (EasyScriptEventListener listener : coordinateReceiverListeners) { 
                listener.cancel();
            }
            return;
        }
        streamedSignature.append(ch);
        if (streamedSignature.length() < 2) {
            return;     // we know we need at least 2 characters
        }
        int result = convertBodnarCharsToValue(streamedSignature);
        if (result >= 0) {
            addValueToSignature(result);
            streamedSignature.setLength(0);
        }
    }
}
