package com.jscape.inet.terminal;

import java.util.HashMap;

/**
 * Represents control sequence. There are several types of control sequence
 * depending of the second character in sequence. These types are implemented
 * as "Strategy" pattern [Gamma94]. Control sequence may be in three states:
 * closed (if there is no sequence opening character),
 * in-progress (if sequence opening character is received)
 * and complete (when ending character is received).
 * Each control sequence has "canonical" form which is used to map
 * incoming control sequence to <code>ControlCommand</code> objects.
 */
public class ControlSequence {

    public static final char DEFAULT_OPENING_CHAR = '\033';
    private static final HashMap TYPE_MAP = new HashMap();

    private final StringBuffer value = new StringBuffer();
    private boolean inProgress = false;
    private SequenceType currentType = SimpleType.getInstance();

    //----------------------------------------------------------------------------
    static {
        addType('[', CSIType.getInstance());
        addType('#', ICType.getInstance());
        addType('/', ICType.getInstance());
        addType('(', ICType.getInstance());
        addType(')', ICType.getInstance());
    }

    //----------------------------------------------------------------------------

    /**
     * Adds map of second sequence character to <code>SequenceType</code> object.
     *
     * @param keyValue second character in sequence.
     * @param type     <code>SequenceType</code> object.
     *
     * @return true if map was successfully added.
     */
    public static boolean addType(char keyValue, SequenceType type) {
        if (type == null) {
            throw new NullPointerException();
        }

        Character key = new Character(keyValue);

        if (TYPE_MAP.containsKey(key)) {
            return false;
        } else {
            TYPE_MAP.put(key, type);
        }

        return true;
    }

    //----------------------------------------------------------------------------

    /**
     * Removes map.
     *
     * @param keyValue second character in sequence.
     *
     * @return true if map was successfully removed.
     */
    public static boolean removeType(char keyValue) {
        Character key = new Character(keyValue);

        if (!TYPE_MAP.containsKey(key)) {
            return false;
        } else {
            TYPE_MAP.remove(key);
        }

        return true;
    }

    //----------------------------------------------------------------------------

    /**
     * Creates <code>ControlSequence</code> object giving opening char.
     *
     * @param openingCh sequence opening char.
     */
    public ControlSequence(char openingCh) {
        this.value.append(openingCh);
    }

    //----------------------------------------------------------------------------

    /**
     * Creates <code>ControlSequence</code> object with default opening char
     * ('\033').
     */
    public ControlSequence() {
        this(DEFAULT_OPENING_CHAR);
    }

    //----------------------------------------------------------------------------

    /**
     * Returns "canonical" form of control sequence (i.e. without parameters).
     * For example: having "\033[0;7m" incoming sequence its "canonical" form
     * will be "\033[m".
     *
     * @return string with "canonical" sequence form.
     */
    public String getCanonicalForm() {
        ensureComplete();
        return this.currentType.getCanonicalForm(this.value);
    }

    //----------------------------------------------------------------------------
    public String toString() {
        ensureComplete();
        return this.value.toString();
    }

    //----------------------------------------------------------------------------

    /**
     * Returns sequence opening char.
     *
     * @return sequence opening char.
     */
    public char getOpeningChar() {
        return this.value.charAt(0);
    }

    //----------------------------------------------------------------------------

    /**
     * Checks if sequence is complete.
     *
     * @return true if sequence is complete.
     */
    public boolean isComplete() {
        return this.currentType.isComplete(this.value);
    }

    //----------------------------------------------------------------------------

    /**
     * Checks if sequence is in progress.
     *
     * @return true if sequence is in progress.
     */
    public boolean inProgress() {
        return this.inProgress;
    }

    //----------------------------------------------------------------------------

    /**
     * Checks if giving char is opening char for current sequence.
     *
     * @param ch char to check.
     *
     * @return true if giving char is opening char for current sequence.
     */
    public boolean isOpeningChar(char ch) {
        return (ch == this.value.charAt(0));
    }

    //----------------------------------------------------------------------------

    /**
     * Extracts and returns sequence parameters as array of <code>int</code>.
     *
     * @return array of <code>int</code>.
     */
    public int[] getParameters() {
        ensureComplete();
        return this.currentType.getParameters(this.value);
    }

    //----------------------------------------------------------------------------

    /**
     * Adds char to current sequence.
     *
     * @param ch character to add.
     */
    public void append(char ch) {
        if (!this.inProgress) {
            throw new IllegalStateException("ControlSequence.append(): sequence is closed");
        }

        this.value.append(ch);

        if (canChooseType()) {
            setType();
        }

        if (isComplete()) {
            this.inProgress = false;
        }
    }

    //----------------------------------------------------------------------------

    /**
     * Resets current sequence.
     */
    public void reset() {
        this.value.delete(1, this.value.length());
        this.inProgress = false;
        this.currentType = SimpleType.getInstance();
    }

    //----------------------------------------------------------------------------

    /**
     * Resets sequence and starts a new one.
     */
    public void startNew() {
        reset();
        this.inProgress = true;
    }

    //----------------------------------------------------------------------------

    /**
     * Checks if sequence type can be determined.
     *
     * @return true if sequence type can be determined.
     */
    private boolean canChooseType() {
        return (this.value.length() == 2);
    }

    //----------------------------------------------------------------------------

    /**
     * Sets type of current sequence.
     */
    private void setType() {
        //assert (this.value.lenght() == 2)
        Character key = new Character(this.value.charAt(1));

        this.currentType = TYPE_MAP.containsKey(key) ? (SequenceType) TYPE_MAP.get(key) : SimpleType.getInstance();
    }

    //----------------------------------------------------------------------------

    /**
     * Ensures sequence is complete.
     */
    private void ensureComplete() {
        if (!isComplete()) {
            throw new IllegalStateException();
        }
    }

}