/*
 * Copyright 2009 Lari Natri <lari.natri@gmail.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 * 
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


package compid.report;


import compid.report.ReportRuntimeException.ExceptionType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.swing.table.AbstractTableModel;

/**
 *
 * @author Lari Natri
 */
public class ReportPageModel extends AbstractTableModel {
    
    private final String title;
    private final String subTitle;
    private final ArrayList<String> colNames;
    private final ArrayList<ArrayList<Object>> data;
    private boolean isFinalized;

    /**
     * Constructs an empty ReportPageModel.
     * @param title Title
     * @param subTitle Secondary title
     */
    public ReportPageModel(String title, String subTitle) {
        super();
        this.title = title==null ? "" : title;
        this.subTitle = subTitle==null ? "" : subTitle;
        this.isFinalized = false;
        this.colNames = new ArrayList<String>();
        this.data = new ArrayList<ArrayList<Object>>();
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
    }


    /**
     * Adds a row with given values to the end of the report. If cell count in
     * row given as parameter is bigger than current amount of columns in this
     * report, columns with auto-generated names will be added (with null
     * values for all other rows).
     * @param cellValues values for cells
     * @throws compid.report.ReportRuntimeException Thrown, if report is
     *         already finalized and modifications are not allowed.
     */
    public synchronized void addRow(List<Object> cellValues)
            throws ReportRuntimeException
    {
        this.insertRow(this.data.size(), cellValues);
    }

    /**
     * Inserts a row with given values to the report. If cell count in
     * row given as parameter is bigger than current amount of columns in this
     * report, columns with auto-generated names will be added (with null
     * values for all other rows).
     * @param beforeRow Row index before which the new row will be inserted,
     *        must be: 0 <= beforeRow <= getRowCount()
     * @param cellValues values for cells
     * @throws IndexOutOfBoundsException Thrown, if parameter beforeRow < 0 ||
     *         beforeRow > getRowCount()
     * @throws compid.report.ReportRuntimeException Thrown, if report is
     *         already finalized and modifications are not allowed.
     */
    public synchronized void insertRow(int beforeRow, List<Object> cellValues)
            throws IndexOutOfBoundsException, ReportRuntimeException
    {
        if (this.isFinalized) {
            throw new ReportRuntimeException(ExceptionType.REPORTISFINAL,
                                      "Cannot insert a row.");
        }

        ArrayList<Object> newRow;
        if (cellValues==null) {
            int colCount = this.colNames.size();
            newRow = new ArrayList<Object>(colCount);
            for (int c=0; c<colCount; ++c) {
                newRow.add(null);
            }
        } else {
            newRow = new ArrayList<Object>(cellValues);
            if (newRow.size()>colNames.size()) {
                this.addColumn(null);
            } else while (newRow.size()<colNames.size()) {
                newRow.add(null);
            }
        }

        assert newRow.size()==colNames.size();

        this.data.add(beforeRow, newRow);

        this.fireTableRowsInserted(beforeRow, beforeRow);

    }

    /**
     * Adds a new column as the last one.
     * @param colName Name for the new column
     * @throws compid.report.ReportRuntimeException Thrown, if report is
     *         already finalized and modifications are not allowed.
     */
    public synchronized void addColumn(String colName) 
            throws ReportRuntimeException
    {
        this.insertColumn(colNames.size(), colName);
    }

    /**
     * Adds a new column to a specified location.
     * @param beforeCol Row index before which the new row will be inserted,
     *        must be: 0 <= beforeCol <= getColumnCount()
     * @param colName Name for the new column
     * @throws IndexOutOfBoundsException Thrown, if parameter beforeRow < 0 ||
     *         beforeCol > getColumnCount()
     * @throws compid.report.ReportRuntimeException Thrown, if report is
     *         already finalized and modifications are not allowed.
     */
    public synchronized void insertColumn(int beforeCol, String colName)
            throws IndexOutOfBoundsException, ReportRuntimeException
    {
        if (this.isFinalized) {
            throw new ReportRuntimeException(ExceptionType.REPORTISFINAL,
                    "Cannot insert a column.");
        }
        if (colName==null) colName = "";
        while (colNames.contains(colName)) { // no duplicate column names
            colName += "_";
            // TODO: name looper-counter-thingy
        }
        colNames.add(beforeCol, colName); // throws IndexOutOfboundsException

        for (ArrayList<Object> row : this.data) { // adds null cell to each row
            row.add(beforeCol,null);
        }

        this.fireTableStructureChanged();

    }

    /**
     * Sets the report as finalized. New data cannot be added after this.
     */
    public synchronized void setPageIsFinal() {
        this.isFinalized = true;
    }

    /**
     * Sorts the report with the default comparator
     * @param col Column index to sort by
     * @param ascending if true, order will be ascending; descending otherwise.
     * @throws compid.report.ReportRuntimeException Thrown, if this report
     *         hasn't been finalized. Finalization is required before sorting
     *         is allowed.
     */
    public synchronized void sort(int col, boolean ascending)
            throws ReportRuntimeException
    {
        if (this.isFinalized==false) {
            throw new ReportRuntimeException(ExceptionType.REPORTISFINAL,
                    "Sorting is allowed only with finalized reports.");
        }
        if (ascending) {
            Collections.<ArrayList<Object>>sort(this.data,
                    new CellComparator(col));
        } else {
            Collections.<ArrayList<Object>>sort(this.data,
                    Collections.reverseOrder(new CellComparator(col)));
        }

        this.fireTableDataChanged();
    }


    @Override
    public synchronized String getColumnName(int i)
            throws IndexOutOfBoundsException
    {
        return colNames.get(i);
    }

    @Override
    public synchronized int getRowCount() {
        return this.data.size();
    }

    @Override
    public synchronized int getColumnCount() {
        return this.colNames.size();
    }

    @Override
    public synchronized Object getValueAt(int row, int column) {
        if (data.size()<1 || colNames.size()<1) {
            throw new IndexOutOfBoundsException("ReportModel is empty");
        }
        return this.data.get(row).get(column);
    }

    /**
     * Returns the title of this report.
     */
    public String getTitle() {
        return this.title;
    }

    /** Returns the subtitle of this report. */
    public synchronized String getSubTitle() {
        return this.subTitle;
    }

    /** Returns true, if report is already finalized, false otherwise. */
    public synchronized boolean isFinal() {
        return this.isFinalized;
    }


    /**
     * Default comparator used in sorting of finalized reports.
     */
    private class CellComparator implements Comparator<ArrayList<Object>> {

        private final int colIndex;

        /**
         * Construts comparator.
         * @param colIndex Index of the column to be compared.
         */
        public CellComparator(int colIndex) {
            this.colIndex = colIndex;
        }

        @Override
        public int compare(ArrayList<Object> row1, ArrayList<Object> row2)
        {
            boolean isNumber1 = true;
            boolean isNumber2 = true;
            String s1 = row1.get(colIndex).toString();
            String s2 = row2.get(colIndex).toString();
            int firstSpaceIndex = s1.indexOf(' ');
            if (firstSpaceIndex>0) s1 = s1.substring(0,firstSpaceIndex);
            firstSpaceIndex = s2.indexOf(' ');
            if (firstSpaceIndex>0) s2 = s2.substring(0,firstSpaceIndex);

            double num1 = 0;
            double num2 = 0;
            try {
                num1 = Double.parseDouble(s1);
            } catch (NumberFormatException ex) {
                isNumber1 = false;
            }
            try {
                num2 = Double.parseDouble(s2);
            } catch (NumberFormatException ex) {
                isNumber2 = false;
            }
            if (isNumber1 && isNumber2) {
                if (num2>num1) return -1;
                if (num1>num2) return 1;
                return 0;
            }
            if (isNumber1) return 1;
            if (isNumber2) return -1;

            // neither is a number

            return row1.get(colIndex).toString().compareTo(row2.get(colIndex).toString());
        }

    } // inner class CellComparator

   
}
