package com.jscape.inet.terminal;

import java.util.Iterator;
import java.util.Vector;

/**
 * Represents terminal screen as array of rows. By default it has 24 x 80 cells
 * and 500 rows history. Lines and columns are numbered consecutively,
 * with the origin being line 1, column 1.
 */
public class Screen {

    public static final int DEFAULT_HISTORY_SIZE = 500;
    public static final int DEFAULT_MAX_ROW = 24;
    public static final int DEFAULT_MAX_COL = 80;

    private final Object lock = new Object();
    private int maxRow;
    private int maxCol;
    private int historySize;
    private Vector history;
    private Vector screen;
    private Cursor cursor;
    private boolean originMode;
    private int upperMargin;
    private int bottomMargin;
    private int upperBorder;
    private int bottomBorder;

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

    /**
     * Creates <code>Screen</code> object givng its dimensions and history size.
     *
     * @param rowNum      quantity of rows in screen.
     * @param colNum      quantity of columns in screen.
     * @param historySize size of history (in rows).
     */
    public Screen(int rowNum, int colNum, int historySize) {
        if (rowNum <= 0 || colNum <= 0 || historySize < 0) {
            throw new IllegalArgumentException("Screen.constructor(): wrong dimension or buffer size");
        }

        this.maxRow = rowNum;
        this.maxCol = colNum;
        this.historySize = historySize;
        this.history = new Vector(this.historySize);
        this.screen = new Vector(this.maxRow);

        for (int i = 0; i < this.maxRow; ++i) {
            this.screen.add(new Row(this.maxCol));
        }

        this.cursor = new Cursor(1, 1);
        this.originMode = false;
        this.upperMargin = 1;
        this.bottomMargin = this.maxRow;
        this.upperBorder = 0;
        this.bottomBorder = this.maxRow - 1;
    }

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

    /**
     * Creates <code>Screen</code> object givng its history size.
     *
     * @param bufferSize size of history (in rows).
     */
    public Screen(int bufferSize) {
        this(DEFAULT_MAX_ROW, DEFAULT_MAX_COL, bufferSize);
    }

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

    /**
     * Creates <code>Screen</code> object givng its dimensions.
     *
     * @param rowNum quantity of rows in screen.
     * @param colNum quantity of columns in screen.
     */
    public Screen(int rowNum, int colNum) {
        this(rowNum, colNum, DEFAULT_HISTORY_SIZE);
    }

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

    /**
     * Creates <code>Screen</code> object with default
     * dimensions and history size.
     */
    public Screen() {
        this(DEFAULT_HISTORY_SIZE);
    }

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

    /**
     * Copy constructor.
     *
     * @param screen screen to by copied.
     */
    public Screen(Screen screen) {
        if (screen == null) {
            throw new NullPointerException("Screen.constructor(): null passed");
        }

        synchronized (screen.lock) {
            this.maxRow = screen.maxRow;
            this.maxCol = screen.maxCol;
            this.historySize = screen.historySize;
            this.history = new Vector(screen.history.size());
            this.screen = new Vector(screen.maxRow);

            Iterator i = screen.history.iterator();

            while (i.hasNext()) {
                Row r = (Row) i.next();
                this.history.add(new Row(r));
            }

            i = screen.screen.iterator();

            while (i.hasNext()) {
                Row r = (Row) i.next();
                this.screen.add(new Row(r));
            }

            this.cursor = new Cursor(screen.cursor);
            this.originMode = screen.originMode;
            this.upperMargin = screen.upperMargin;
            this.bottomMargin = screen.bottomMargin;
            this.upperBorder = screen.upperBorder;
            this.bottomBorder = screen.bottomBorder;
        }
    }

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

    /**
     * Returns quantity of rows in current screen.
     *
     * @return int quantity of rows in current screen.
     */
    public int getMaxRow() {
        return this.maxRow;
    }

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

    /**
     * Returns quantity of columns in current screen.
     *
     * @return int quantity of columns in current screen.
     */
    public int getMaxCol() {
        return this.maxCol;
    }

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

    /**
     * Returns current <code>Cursor</code> in screen.
     *
     * @return the current <code>Cursor</code> for this <code>Screen</code>
     */
    public Cursor getCursor() {
        return this.cursor;
    }

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

    /**
     * Gives access to screen cell.
     *
     * @param row    cell row number.
     * @param column cell column number.
     *
     * @return <code>Cell</code> object at specified position.
     */
    public Cell getCell(int row, int column) {
        if (row < 1 || row > this.maxRow) {
            throw new IndexOutOfBoundsException("Screen.getCell(): wrong position");
        }

        synchronized (this.lock) {
            return ((Row) this.screen.elementAt(row - 1)).cellAt(column - 1);
        }
    }

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

    /**
     * Returns upper margin value.
     *
     * @return upper margin value.
     */
    public int getUpperMargin() {
        return this.upperMargin;
    }

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

    /**
     * Returns bottom margin value.
     *
     * @return bottom margin value.
     */
    public int getBottomMargin() {
        return this.bottomMargin;
    }

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

    /**
     * Sets screen margins for scrolling region.
     * The minimum size of the scrolling region allowed is two lines.
     *
     * @param upperMargin  upper margin
     * @param bottomMargin bottom margin
     */
    public void setMargins(int upperMargin, int bottomMargin) {
        synchronized (this.lock) {
            upperMargin = setupPosition(upperMargin, 1, this.maxRow);
            bottomMargin = setupPosition(bottomMargin, 1, this.maxRow);

            if (upperMargin > bottomMargin - 2) {
                throw new IllegalArgumentException("Screen.setMargins(): wrong margins passed");
            }

            this.upperMargin = upperMargin;
            this.bottomMargin = bottomMargin;

            if (originMode) {
                this.upperBorder = this.upperMargin - 1;
                this.bottomBorder = this.bottomMargin - 1;
            }
        }
    }

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

    /**
     * Checks if screen is in Origin mode.
     *
     * @return true if screen is in Origin mode.
     */
    public boolean isOriginMode() {
        return this.originMode;
    }

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

    /**
     * Sets screen Origin mode.
     *
     * @param originMode true if screen must be in Origin mode.
     */
    public void setOriginMode(boolean originMode) {
        synchronized (this.lock) {
            this.originMode = originMode;

            this.upperBorder = this.originMode ? this.upperMargin - 1 : 0;
            this.bottomBorder =
                    this.originMode ? this.bottomMargin - 1 : this.maxRow - 1;

            this.cursor.moveTo(1, 1);
        }
    }

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

    /**
     * Represents screen as two dimensional array of <code>Cell</code> objects.
     *
     * @return two dimensional array of <code>Cell</code> objects.
     */
    public Cell[][] toCellArray() {
        return toCellArray(this.screen);
    }

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

    /**
     * Represents history buffer as two dimensional array
     * of <code>Cell</code> objects.
     *
     * @return two dimensional array of <code>Cell</code> objects.
     */
    public Cell[][] getHistory() {
        return toCellArray(this.history);
    }

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

    /**
     * Gets String representation of this <code>Screen</code> with each <code>Row</code> in
     * <code>Screen</code> terminated by a \r\n.
     *
     * @return screen contents
     */
    public String toString() {
        StringBuffer result = new StringBuffer();

        synchronized (this.lock) {
            for (int i = 0; i < this.screen.size(); ++i) {
                String row = this.screen.elementAt(i).toString();
                result = result.append(row).append("\r\n");
            }
        }

        return result.toString();
    }

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

    /**
     * Erases current cursor line giving start and end columns by resetting it's
     * content.
     *
     * @param start 'from' column.
     * @param end   'to' column.
     */
    public void eraseLine(int start, int end) {
        start = setupPosition(start, 1, this.maxCol);
        end = setupPosition(end, 1, this.maxCol);

        synchronized (this.lock) {
            int row = this.cursor.row();

            for (int i = start; i <= end; ++i) {
                getCell(row, i).reset();
            }
        }
    }

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

    /**
     * Clears screen area starting from provided row and column arguments to current
     * cursor position.
     *
     * @param row starting row number.
     * @param col starting column number.
     */
    public void erase(int row, int col) {
        row = setupPosition(row, 1, this.maxRow);
        col = setupPosition(col, 1, this.maxCol);

        synchronized (this.lock) {
            int curRow = this.cursor.row();
            int curCol = this.cursor.column();
            int startRow = (row < curRow) ? row : curRow;
            int endRow = (row < curRow) ? curRow : row;
            int startCol = (col < curCol) ? col : curCol;
            int endCol = (col < curCol) ? curCol : col;

            for (int i = startRow; i <= endRow; ++i) {
                for (int j = startCol; j <= endCol; ++j) {
                    getCell(i, j).reset();
                }
            }
        }
    }

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

    /**
     * Clears entire screen.
     */
    public void erase() {
        synchronized (this.lock) {
            int row = this.cursor.row();
            int col = this.cursor.column();
            this.cursor.moveTo(this.maxRow, this.maxCol);
            erase(1, 1);
            this.cursor.moveTo(row, col);
        }
    }

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

    /**
     * Clears all tabstops in screen.
     */
    public void clearTabs() {
        synchronized (this.lock) {
            for (int i = 0; i < this.maxRow; ++i) {
                for (int j = 0; j < this.maxCol; ++j) {
                    getCell(i, j).setTabstop(false);
                }
            }
        }
    }

    //----------------------------------------------------------------------------
    private Cell[][] toCellArray(Vector buffer) {
        synchronized (this.lock) {
            int sz = buffer.size();
            Cell[][] result = new Cell[sz][this.maxCol];

            for (int i = 0; i < sz; ++i) {
                result[i] = ((Row) buffer.elementAt(i)).toCellArray();
            }

            return result;
        }
    }

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

    /**
     * Performs screen one row scroll down. Takes and adds one last row
     * from history to current screen. Does nothing if history is empty.
     */
    private void scrollDown() {
        synchronized (this.lock) {
            if (!this.history.isEmpty()) {
                Row r = (Row) this.history.lastElement();
                Row last = (Row) this.screen.remove(this.bottomBorder);
                this.screen.add(this.upperBorder, r);
                this.screen.add(this.maxRow, last);
                this.history.remove(r);
            }
        }
    }

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

    /**
     * Performs screen one row scroll up. Takes and adds one first row from screen
     * to history. Shrinks history if it's size is bigger than specified.
     */
    private void scrollUp() {
        synchronized (this.lock) {
            Row r = (Row) this.screen.remove(this.upperBorder);
            this.history.add(r);

            if (this.screen.size() < this.maxRow) {
                this.screen.add(this.bottomBorder, new Row(this.maxCol));
            } else {
                Row last = (Row) this.screen.remove(this.maxRow);
                this.screen.add(this.bottomBorder, last);
            }

            if (this.history.size() > this.historySize) {
                this.history.remove(0);
            }
        }
    }

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

    /**
     * Used for cursor positioning. Guarantees that position is in [0..max].
     *
     * @param pos position to revise.
     * @param max max position number.
     *
     * @return revised position.
     */
    private int setupPosition(int pos, int min, int max) {
        if (pos < min) {
            pos = min;
        } else if (pos > max) {
            pos = max;
        }

        return pos;
    }

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

    /**
     * Represents cursor position in screen.
     */
    public class Cursor {

        private int row = 1;
        private int column = 1;

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

        /**
         * Creates <code>Cursor</code> object giving its position.
         *
         * @param row    row number.
         * @param column column number.
         */
        public Cursor(int row, int column) {
            this.row = row;
            this.column = column;
        }

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

        /**
         * Copy constructor.
         *
         * @param cursor <code>Cursor</code> object to be copied.
         */
        public Cursor(Cursor cursor) {
            if (cursor == null) {
                throw new NullPointerException("Cursor.constructor(): null passed");
            }

            this.row = cursor.row;
            this.column = cursor.column;
        }

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

        /**
         * Current row number.
         *
         * @return current row number.
         */
        public int row() {
            return this.row;
        }

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

        /**
         * Current column number.
         *
         * @return current column number.
         */
        public int column() {
            return this.column;
        }

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

        /**
         * Gives access to underlying <code>Cell</code> object.
         *
         * @return <code>Cell</code> object.
         */
        public Cell getCell() {
            return Screen.this.getCell(this.row, this.column);
        }

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

        /**
         * Sets cursor position in the cells. According to VT100 User's Manual
         * doesn't verify passed parameters. It means, that if passed row or
         * column number is greater or less than current cells dimensions
         * cursor just stops by corresponded border.
         *
         * @param row    row number.
         * @param column column number
         */
        public void moveTo(int row, int column) {
            synchronized (Screen.this.lock) {
                this.row =
                        Screen.this.setupPosition(
                                row,
                                1,
                                Screen.this.bottomBorder - Screen.this.upperBorder + 1);
                this.column =
                        Screen.this.setupPosition(column, 1, Screen.this.maxCol);
            }
        }

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

        /**
         * Moves cursor down for giving row quantity.
         *
         * @param num row quantity.
         */
        public void moveDown(int num) {
            moveTo(this.row + num, this.column);
        }

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

        /**
         * Moves cursor up for giving row quantity.
         *
         * @param num row quantity.
         */
        public void moveUp(int num) {
            moveTo(this.row() - num, this.column);
        }

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

        /**
         * Moves cursor left for giving column quantity.
         *
         * @param num column quantity.
         */
        public void moveLeft(int num) {
            moveTo(this.row, this.column - num);
        }

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

        /**
         * Moves cursor right for giving column quantity.
         *
         * @param num column quantity.
         */
        public void moveRight(int num) {
            moveTo(this.row, this.column + num);
        }

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

        /**
         * Moves cursor to home position.
         */
        public void moveHome() {
            moveTo(this.row, 1);
        }

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

        /**
         * Moves cursor to end position.
         */
        public void moveEnd() {
            moveTo(this.row, Screen.this.maxCol);
        }

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

        /**
         * Moves cursor up for one row. Performs screen scroll down if
         * current row is the first.
         */
        public void scrollDown() {
            synchronized (Screen.this.lock) {
                if (this.row == 1) {
                    Screen.this.scrollDown();
                }

                moveUp(1);
            }
        }

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

        /**
         * Moves cursor down for one row. Performs screen scroll up
         * if current row is the last.
         */
        public void scrollUp() {
            synchronized (Screen.this.lock) {
                if (this.row == (Screen.this.bottomBorder - Screen.this.upperBorder + 1)) {
                    Screen.this.scrollUp();
                }

                moveDown(1);
            }
        }

    }

}