package assignment1;

import java.util.Arrays;

/**
  * This is a display grid class. It helps organize and format elements into grid
  * patterns for onscreen display.
  * @author Jon Olson
  */
public class OlsonJGrid
{

    /**
     * The grid itself.
     */
    protected Object[][] grid = null;
    /**
     * Column width information.
     */
    protected int[] widths = null;
    /**
     * Used to signal if the row has data at all.
     */
    protected boolean[] rowData = null;

    /**
     * Default constructor. It does nothing.
     */
    public OlsonJGrid()
    {
    }

    /**
     * Initialize the grid to a specific dimension
     * @param width the starting width
     * @param height the starting height
     */
    public OlsonJGrid( int width, int height )
    {
        expandSize( width, height );
    }

    /**
     * Add a single row to the grid. This is the same as using
     * {code}addColumns(1){/code}
     * @return the new height
     */
    public int addColumn()
    {
        return addColumns( 1 );
    }

    /**
     * Add a number of columsn to the grid. This is the same as calling
     * {code}expandSize(count, 0); result = getWidth();{/code}
     * @param count how many columns to add
     * @return the new width
     * @throws java.lang.IllegalArgumentException if count is < 0
     */
    public int addColumns( int count ) throws IllegalArgumentException
    {
        expandSize( count, 0 );

        return getWidth();
    }

    /**
     * Add a single row to the grid. This is the same as using
     * {code}addRows(1){/code}
     * @return the new height
     */
    public int addRow()
    {
        return addRows( 1 );
    }

    /**
     * Add a number of rows to the grid. This is the same as calling
     * {code}expandSize(0, count); result = getHeight();{/code}
     * @param count how many rows to add
     * @return the new height
     * @throws java.lang.IllegalArgumentException if count is < 0
     */
    public int addRows( int count ) throws IllegalArgumentException
    {
        expandSize( 0, count );

        return getHeight();
    }

    /**
     * Retrieve the number of rows in the grid
     * @return the number of rows
     */
    public int getHeight()
    {
        if ( rowData == null )
        {
            return 0;
        }
        else
        {
            return rowData.length;
        }
    }

    /**
     * Retrieve the number of columns in the grid
     * @return the number of columns
     */
    public int getWidth()
    {
        if ( widths == null )
        {
            return 0;
        }
        else
        {
            return widths.length;
        }
    }

    /**
     * Expand the size of the grid
     * @param byWidth how many columns to add
     * @param byHeight how many rows to add
     * @return If anything was changed. i.e. false if both parameters are zero
     * and true for all positive values.
     * @throws java.lang.IllegalArgumentException if either parameter is < 0
     */
    public boolean expandSize( int byWidth, int byHeight ) throws IllegalArgumentException
    {
        if ( byWidth < 0 || byHeight < 0 )
        {
            throw new IllegalArgumentException( "You can only add positive numbers of columns." );
        }
        else if ( byWidth == 0 && byHeight == 0 )
        {
            // nothing needs to be done.
            return false;
        }

        // existing measurements
        int width = getWidth(), height = getHeight();

        // ensure we have at least dimensions of one.
        int newWidth = Math.max( width + byWidth, 1 );
        int newHeight = Math.max( height + byHeight, 1 );

        // initialize the new grid
        Object[][] newGrid = new Object[ newHeight ][ newWidth ];
        for ( Object[] row : newGrid )
        {
            Arrays.fill( row, null );
        }

        // copy over the existing values from the current grid
        for ( int i = 0; i < height; ++i )
        {
            for ( int j = 0; j < width; ++j )
            {
                newGrid[i][j] = grid[i][j];
            }
        }
        grid = newGrid;

        // expand the widths array if needed
        if ( width < newWidth )
        {
            int[] newWidths = new int[ newWidth ];
            Arrays.fill( newWidths, 0 );
            for ( int i = 0; i < width; ++i )
            {
                newWidths[i] = widths[i];
            }
            widths = newWidths;
        }

        // expand the rowData array if needed
        if ( height < newHeight )
        {
            boolean[] newData = new boolean[ newHeight ];
            Arrays.fill( newData, false );
            for ( int i = 0; i < height; ++i )
            {
                newData[i] = rowData[i];
            }
            rowData = newData;
        }

        return true;
    }

    /**
     * Set the value of a particular cell. If the coordinate is outside the
     * current bounds of the grid, it will be expanded.
     * @param x the one-based horizontal coordinate of the cell to set
     * @param y the one-based vertical coordinate of the cell to set
     * @param value the value to use.
     */
    public void set( int x, int y, Object value )
    {
        if ( x <= 0 || y <= 0 )
        {
            throw new IllegalArgumentException( "Positive, non-zero coordinates required." );
        }

        int exX, exY = exX = 0;
        if ( rowData == null )
        {
            exY = y;
        }
        else if ( y > getHeight() )
        {
            exY = y - getHeight();
        }

        if ( widths == null )
        {
            exX = x;
        }
        else if ( x > getWidth() )
        {
            exX = x - getWidth();
        }

        expandSize( exX, exY );
        --x;
        --y;

        if ( (grid[y][x] == null) != (value == null) )
        {
            Object test;

            boolean empty = true;
            // check the row for emptiness
            for ( int i = 0; i < grid[y].length && empty; ++i )
            {
                if ( i == x )
                {
                    test = value;
                }
                else
                {
                    test = grid[y][i];
                }

                empty = test == null;
            }
            rowData[y] = !empty;

            int max = 0;
            // check the column largest cell
            for ( int i = 0; i < grid.length; ++i )
            {
                if ( i == y )
                {
                    test = value;
                }
                else
                {
                    test = grid[i][x];
                }

                if ( test != null )
                {
                    max = Math.max( max, test.toString().length() );
                }
            }
            widths[x] = max;
        }
        else if ( grid[y][x] != null && value != null )
        {
            widths[x] = Math.max( widths[x], value.toString().length() );
        }

        grid[y][x] = value;
    }

    /**
     * Clear the value from a specified coordinate. If the coordinate is outside
     * of the current grid, it will be expanded to fit.
     * @param x the one-based horizontal coordinate to clear
     * @param y the one-based vertical coordinate to clear
     */
    public void clear( int x, int y )
    {
        set( x, y, null );
    }

    /**
     * Generate a string showing the values stored in the grid.
     * @return the string
     */
    @Override
    public String toString()
    {
        // if there's no data, return an empty string.
        if ( getHeight() == 0 )
        {
            return "";
        }

        // set up the pieces of the edges
        String left = "+-", middle = "-+-", right = "-+", span = "-";
        String leftBar = "| ", middleBar = " | ", rightBar = " |";

        String hiLo = "";

        // calculate how many actual columns we have
        int columns = 0;
        for ( int width : widths )
        {
            if ( width > 0 )
            {
                ++columns;
            }
        }
       
        // if we found out that none of the columns had data, return an empty
        // string.
        if ( columns <= 0 )
        {
            return "";
        }

        // build the top/bottom border using the column count
        int column = 0;
        hiLo += left;
        for ( int i = 0; i < widths.length; ++i )
        {
            for ( int j = 0; j < widths[i]; ++j )
            {
                hiLo += span;
            }
           
            if ( widths[i] > 0 && ++column < columns )
            {
                hiLo += middle;
            }
        }
        hiLo += right;

        // build the output
        String out = hiLo + "\n";
        for ( int i = 0; i < grid.length; ++i )
        {
            // don't process the row if there's isn't any row data
            if ( !rowData[i] )
            {
                continue;
            }
           
            out += leftBar;
            column = 0;
            for ( int j = 0; j < grid[i].length; ++j )
            {
                // do we need to process this column?
                if ( widths[j] <= 0 )
                {
                    continue;
                }

                // what type of data do we have in this cell?
                if ( grid[i][j] == null )
                {
                    out += String.format( "%" + widths[j] + "s", " " );
                }
                else if ( grid[i][j] instanceof Long ||
                    grid[i][j] instanceof Integer ||
                    grid[i][j] instanceof Short ||
                    grid[i][j] instanceof Byte ||
                    grid[i][j] instanceof Float ||
                    grid[i][j] instanceof Double )
                {
                    out += String.format( "%" + widths[j] + "s", grid[i][j] );
                }
                else
                {
                    out += String.format( "%-" + widths[j] + "s", grid[i][j] );
                }

                // increase the column counter to see if we need to skip the
                // middle bar
                if ( ++column < columns )
                {
                    out += middleBar;
                }
            }
            out += rightBar + "\n";
        }
        out += hiLo;

        return out;
    }
}