/*
 * WepsTableMeta.java
 *
 * Created on June 8, 2006, 12:16 PM
 *
 */
package usda.weru.util.table;

import com.klg.jclass.table.JCCellRange;
import com.klg.jclass.table.JCTable;
import de.schlichtherle.truezip.file.TFile;
import java.io.Serializable;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jdom2.Element;

/**
 * Contains configuration information for a WepsTable object.
 * @author Joseph Levin
 */
public class WepsTableMeta implements XmlObject, Serializable {
    private static final long serialVersionUID = 1L;

    private static int c_anonymousId = 100;

    List<String> boldedColumns;
    
	/**
	 *
	 */
	protected List<Column> c_columns;

	/**
	 *
	 */
	protected transient Label[][] c_labelMatrix;

	/**
	 *
	 */
	protected WepsTableMeta c_parent;    //Inheritable values.

	/**
	 *
	 */
	protected Hashtable<String, CellStyle> c_cellStyleMap;

	/**
	 *
	 */
	protected Hashtable<String, Column> c_columnMap;

	/**
	 *
	 */
	protected int c_frozenColumns = WepsTableEnum.NO_VALUE;

	/**
	 *
	 */
	protected int c_frozenRows = WepsTableEnum.NO_VALUE;

	/**
	 *
	 */
	protected int c_columnLabelDisplay = 1;

	/**
	 *
	 */
	protected int c_rowLabelDisplay = WepsTableEnum.NO_VALUE;

	/**
	 *
	 */
	protected String c_defaultCellStyleId;

	/**
	 *
	 */
	protected String c_defaultLabelStyleId;

	/**
	 *
	 */
	protected CellStyle c_defaultCellStyle;

	/**
	 *
	 */
	protected CellStyle c_defaultLabelStyle;

    /**
     * Creates a new instance of WepsTableMeta
     */
    public WepsTableMeta() {
        init();
    }

    /**
     * Initialize data structures that will hold information about the table.
     */
    public void init() {
        c_cellStyleMap = new Hashtable<String, CellStyle>();
        c_columnMap = new Hashtable<String, Column>();
        c_columns = new Vector<Column>();
    }

    /**
     * Load table configuration information from an org.jdom2 XML node.
     * @param node An org.jdom2.Element containing configuration information for the table.
     */
    @Override
    public void fromXml(Element node) {
        init();
        loadParent(node);
        loadCellStyles(node);
        loadColumnStyles(node);
        loadColumns(node);
        loadRowLabelDisplay(node);
        loadColumnLabelDisplay(node);
        loadFrozenRows(node);
        loadFrozenColumns(node);
    }

    /**
     * Apply the current configuration settings to the specified WepsTable.
     * @param table The usda.weru.util.table.WepsTable to apply the current configuration to.
     */
    public void applyToTable(WepsTable table) {
        table.setRepaintEnabled(false);
        //Frozen rows/columns
        int frozenRows = getFrozenRows();
        if (frozenRows >= 0) {
            table.setFrozenRows(frozenRows);
        }

        int frozenColumns = getFrozenColumns();
        if (frozenColumns >= 0) {
            table.setFrozenColumns(frozenColumns);
        }

        //Label Display
        table.setRowLabelDisplay(isRowLabelDisplayed());
        table.setColumnLabelDisplay(isColumnLabelDisplayed());

        //Default styles
        CellStyle defaultCellStyle = getDefaultCellStyle();
        if (defaultCellStyle != null) {
            table.setDefaultCellStyle(defaultCellStyle);
        }

        CellStyle defaultLabelStyle = getDefaultLabelStyle();
        if (defaultLabelStyle != null) {
            table.setDefaultLabelStyle(defaultLabelStyle);
        }


        //Set column widths
        for (int c = 0; c < getColumnCount(); c++) {
            Column column = getColumn(c);
            table.setPixelWidth(c, column.getWidth());

            int min = column.getMinWidth();
            if (min != WepsTableEnum.NO_VALUE) {
                table.setMinWidth(c, min);
            }

            int max = column.getMaxWidth();
            if (max != WepsTableEnum.NO_VALUE) {
                table.setMaxWidth(c, max);
            }

            if (column.isHidden() || !column.isVisible()) {
                table.setColumnHidden(c, true);
            }
        }

        //Span columns for headers.
        table.clearSpannedRanges();
        if (isColumnLabelDisplayed()) {
            spanLabels(table);
        }

        //Lable Row Sizes.
        for (int r = -1; r < getLabelRowCount(); r++) {
            table.setPixelHeight(r, 20);
        }
        table.setRepaintEnabled(true);

    }

    /**
     * Returns the CellStyle of the given name.
     * If there is no style by that name it will check the parent's list of styles.
     * Returns <B>null</B> if it still can't be found.
     * @param id The id of the CellStyle.
     * @return The CellStyle that was found or <B>null</B> if one could not be found.
     */
    public CellStyle getCellStyle(String id) {
        if (id == null) {
            return null;
        //Inherit styles from the parent.
        }
        CellStyle style = c_cellStyleMap.get(id);
        if (style != null) {
            return style;
        } else if (c_parent != null) {
            return c_parent.getCellStyle(id);
        } else {
            return null;
        }
    }

    /**
     * Returns all CellStyles available.
     * @return A Collection of all available CellStyles.
     */
    public Collection<CellStyle> getCellStyles() {
        return c_cellStyleMap.values();
    }

    /**
     * Adds the given CellStyle to the list of available CellStyles.
     * @param style The style to be added to the list.
     */
    public void putCellStyle(CellStyle style) {
        if (style.isAnonymous()) {
            return;        //No reason to store an anonymous cellstyle.
        }
        c_cellStyleMap.put(style.getId(), style);
    }

    /**
     * Returns the specified Column.  Checks the parent if the column can't be found.
     * Returns <B>null</B> if it still can't be found.
     * @param id The name of the desired Column.
     * @return The Column found or null if no column found.
     */
    public Column getColumn(String id) {
        if (id == null) {
            return null;
        //Inherit columns from the parent.
        }
        Column column = c_columnMap.get(id);
        if (column != null) {
            return column;
        } else if (c_parent != null) {
            return c_parent.getColumn(id);
        } else {
            return null;
        }
    }

    /**
     * Adds the given Column to the list of Columns
     * @param column The Column to be added.
     */
    public void putColumn(Column column) {
        if (column.isAnonymous()) {
            return;       //No reason to store an anonymous column for lookup
        }
        c_columnMap.put(column.getId(), column);
    }

    /**
     * Returns a unique ID.
     * @return A unique ID.
     */
    public String getAnonymousId() {
        return Integer.toString(c_anonymousId++);
    }

    /**
     * Load configuration settings from an XML file.
     * @param file The file containing configuration settings for the table.
     */
    public void fromFile(TFile file) {
        Element root = Helper.getRootNode(file);
        //TODO: Notify of fail.
        if (root == null) {
            return;
        }
        fromXml(root);
    }

    private void loadParent(Element node) {
        String parentPath = node.getAttributeValue(WepsTableEnum.XML_parent);
        if (parentPath == null) {
            return;
        }
        TFile parentFile = new TFile(new TFile(parentPath).getAbsoluteFile());
        WepsTableMeta parent = new WepsTableMeta();
        parent.fromFile(parentFile);
        setParentMeta(parent);
    }

    private void setParentMeta(WepsTableMeta meta) {
        c_parent = meta;
    }

    @SuppressWarnings("unchecked")
    private void loadCellStyles(Element node) {
        //Load the defined cell styles
        Element stylesNode = node.getChild(WepsTableEnum.XML_cellstyles);

        if (stylesNode != null) //No cell styles to load.
        {
            for (Element element : stylesNode.getChildren(WepsTableEnum.XML_style)) {
                CellStyle cellStyle = new CellStyle(this);
                cellStyle.fromXml(element);
                putCellStyle(cellStyle);
            }
        }

        //Load the default cell and label styles
        c_defaultCellStyleId = node.getChildText(WepsTableEnum.XML_defaultcellstyle);
        c_defaultCellStyle = getCellStyle(getDefaultCellStyleId());

        c_defaultLabelStyleId = node.getChildText(WepsTableEnum.XML_defaultlabelstyle);
        c_defaultLabelStyle = getCellStyle(getDefaultLabelStyleId());

        if (c_parent != null) {
            for (CellStyle style : c_parent.getCellStyles()) {
                style.setMeta(this);
                style.refreshParentStyle();
            }
        }
    }

    /**
     * Return the default CellStyle ID.  If there is no default specified, it checks for the parents default CellStyle ID.
     * @return The ID of the default CellStyle.
     */
    public String getDefaultCellStyleId() {
        if (c_defaultCellStyleId == null && c_parent != null) {
            return c_parent.getDefaultCellStyleId();
        } else {
            return c_defaultCellStyleId;
        }
    }

    /**
     * Return the default CellStyle.  If there is no default specified, it checks for the parents default CellStyle.
     * @return The default CellStyle.
     */
    public CellStyle getDefaultCellStyle() {
        if (c_defaultCellStyle == null && c_parent != null) {
            return c_parent.getDefaultCellStyle();
        } else {
            return c_defaultCellStyle;
        }
    }

    /**
     * Return the default label CellStyle ID.  If there is no default specified,
     * it checks for the parents default label CellStyle ID.
     * @return The default label CellStyle ID.
     */
    public String getDefaultLabelStyleId() {
        if (c_defaultLabelStyleId == null && c_parent != null) {
            return c_parent.getDefaultLabelStyleId();
        } else {
            return c_defaultLabelStyleId;
        }
    }

    /**
     * Return the default label CellStyle.  If there is no default specified, it checks for the parents default label CellStyle.
     * @return The default label CellStyle.
     */
    public CellStyle getDefaultLabelStyle() {
        if (c_defaultLabelStyle == null && c_parent != null) {
            return c_parent.getDefaultLabelStyle();
        } else {
            return c_defaultLabelStyle;
        }
    }

    private void loadColumnStyles(Element node) {
        Element columnStylesNode = node.getChild(WepsTableEnum.XML_columnstyles);
        if (columnStylesNode == null) {
            return;
        }
        List<Element> children = columnStylesNode.getChildren(WepsTableEnum.XML_column);
        for (Element columnNode : children) {
            Column column = new Column(this, false);
            column.fromXml(columnNode);
            putColumn(column);
        }
    }

    private void loadColumns(Element node) {
        Element columnsNode = node.getChild(WepsTableEnum.XML_columns);
        if (columnsNode != null) {
            ColumnGroup columns = new ColumnGroup(this);
            columns.fromXml(columnsNode);
            boldedColumns = columns.getBoldedColumns();
            buildLabelMatrix();
        }
    }

    private void buildLabelMatrix() {
        int numberOfColumns = c_columns.size();
        int numberOfRows = 0;
        for (Column column : c_columns) {
            int depth = column.depthInHeader();
            if (depth > numberOfRows) {
                numberOfRows = depth;
            }
        }
//        numberOfRows = numberOfRows;
        c_labelMatrix = new Label[numberOfRows][numberOfColumns];
        for (int c = 0; c < numberOfColumns; c++) {
            addToLabelMatrix(getColumn(c), numberOfRows - 1, c);
        }
    }

    private void addToLabelMatrix(ColumnGroupOrColumn group, int rowIndex, int columnIndex) {
        //printLabelMatrix();

        int numberOfColumnsToSpan = group.getLabel().getSpanColumns();
        //Is the column spanning set on the label?        
        if (numberOfColumnsToSpan <= 0) {
            //Determine the number of columns to span.
            //This is the number of columns at the bottom of the tree which share this parent.
            //Should always be one for a column, they have no children.
            numberOfColumnsToSpan = group.bottomBreadth();
        }

        int numberOfRowsToSpan = group.getLabel().getSpanRows();
        //Is the row spanning set on the label?
        if (numberOfRowsToSpan <= 0) {
            //Determine the number of rows to span.
            //span = rows - depthInHeader + 1
            int depth = group.depthInHeader() - 1;      //We subtract 1 because the top level parent is a root container.
            numberOfRowsToSpan = rowIndex - depth + 1;
        }

        for (int c = 0; c < numberOfColumnsToSpan; c++) {
            for (int r = 0; r < numberOfRowsToSpan; r++) {
                if (c_labelMatrix[rowIndex - r][columnIndex + c] == null) {
                    c_labelMatrix[rowIndex - r][columnIndex + c] = group.getLabel();
                }
            }
        }

        if (group.depthInHeader() > 1) {     //Has a parent and grandparent.          
            if (group.getParentGroup().isFirstChild(group)) {
                //This is the fist child, so it adds the parent.
                addToLabelMatrix(group.getParentGroup(), rowIndex - numberOfRowsToSpan, columnIndex);
            }
        }

    }

    /**
     * 
     * @param table The WepsTable to be queried.
     * @param rowIndex The row index of the Cell to be queried.
     * @param columnIndex The column index of the cell to be queried.
     * @return 
     */
    public Object getColumnLabel(WepsTable table, int rowIndex, int columnIndex) {
        if (rowIndex < 0 || columnIndex < 0) {
            return null;
        }
        Label label = getLabelMatrix()[rowIndex][columnIndex];
        if (label == null) {
            return null;
        }
        Object value = label.getObject(table, rowIndex, columnIndex);
        if (value instanceof String) {
            value = parse((String) value, table, rowIndex, columnIndex);
        }
        return value;
    }

	/**
	 *
	 * @param table
	 */
	public void spanLabels(JCTable table) {
        //Num rows = 2 ---> freeze 
        //TODO: Adjust frozen rows to the user's number of frozen rows.
        //This will need to be handled by overiding setFrozenRows in WepsTable.
        boolean[][] used = null;
        try {
            used = new boolean[getLabelMatrix().length][getLabelMatrix()[0].length];
        } catch (ArrayIndexOutOfBoundsException aioobe) {
            //TODO: Notify of fail.
            return;
        }

        for (int r = 0; r < getLabelMatrix().length; r++) {
            for (int c = 0; c < getLabelMatrix()[r].length; c++) {
                if (!used[r][c]) {
                    JCCellRange span = getSpan(r, c, used);
                    if (span != null) {
                        table.addSpannedRange(span);
                    }
                }
            }
        }
    }

    private JCCellRange getSpan(int rowIndex, int columnIndex, boolean[][] used) {
        if (used[rowIndex][columnIndex]) {
            return null;
        }
        JCCellRange span = new JCCellRange();
        Label[][] labelMatrix = getLabelMatrix();
        Label label = labelMatrix[rowIndex][columnIndex];

        int endColumn = columnIndex;
        Label test = labelMatrix[rowIndex][endColumn];

        //How many columns are the same?  Catch if the entire row has the same header.
        for (int cdx = columnIndex; cdx < used[rowIndex].length; cdx++) {
            test = labelMatrix[rowIndex][cdx];
            if (test != label) {
                break;
            }
            endColumn = cdx;
        }

        int endRow = 0;
        for (int rdx = rowIndex; rdx < used.length; rdx++) {
            test = labelMatrix[rdx][columnIndex];
            if (test != label) {
                break;
            }
            endRow = rdx;
            for (int cdx = columnIndex; cdx < endColumn; cdx++) {
                used[rdx][cdx] = false;
            }
        }

        span.start_column = columnIndex;
        span.start_row = rowIndex - 1;
        span.end_column = endColumn;
        span.end_row = endRow - 1;

        for (int ir = span.start_row + 1; ir <= span.end_row + 1; ir++) {
            for (int ic = span.start_column; ic <= span.end_column; ic++) {
                used[ir][ic] = true;
            }
        }

        return span;
    }

    /**
     * 
     * @return 
     */
    public Label[][] getLabelMatrix() {
        //TODO: Add more smarts to the tests, ie: is the number of columns correct...
        if (c_labelMatrix == null) {
            buildLabelMatrix();
        }
        return c_labelMatrix.clone();
    }

    private void loadRowLabelDisplay(Element node) {
        String rowLabelDisplayText = node.getChildTextTrim(WepsTableEnum.XML_rowlabeldisplay);
        if (rowLabelDisplayText == null) {
            return;
        }
        boolean rowLabelDisplay = Helper.isTrue(rowLabelDisplayText);
        if (rowLabelDisplay) {
            c_rowLabelDisplay = 1;
        }
    }

    /**
     * 
     * @return 
     */
    public boolean isRowLabelDisplayed() {
        if (c_rowLabelDisplay == WepsTableEnum.NO_VALUE && c_parent != null) {
            return c_parent.isRowLabelDisplayed();
        } else {
            return (c_rowLabelDisplay == 1);
        }
    }

    private void loadColumnLabelDisplay(Element node) {
        String columnLabelDisplayText = node.getChildTextTrim(WepsTableEnum.XML_columnlabeldisplay);
        if (columnLabelDisplayText == null) {
            return;
        }
        boolean columnLabelDisplay = Helper.isTrue(columnLabelDisplayText);
        if (columnLabelDisplay) {
            c_columnLabelDisplay = 1;
        }
    }

    /**
     * 
     * @return 
     */
    public boolean isColumnLabelDisplayed() {
        if (c_columnLabelDisplay == WepsTableEnum.NO_VALUE && c_parent != null) {
            return c_parent.isColumnLabelDisplayed();
        } else {
            return (c_columnLabelDisplay == 1);
        }
    }

    private void loadFrozenRows(Element node) {
        String frozenRowsText = node.getChildTextTrim(WepsTableEnum.XML_frozenrows);
        try {
            int frozenRows = Integer.parseInt(frozenRowsText);
            c_frozenRows = frozenRows;
        } catch (NumberFormatException nfe) {
            return;
        }
    }

    /**
     * Return the number of rows to be reserved as header rows.
     * @return The number of rows used as header rows.
     */
    public int getFrozenRows() {
        if (c_frozenRows == WepsTableEnum.NO_VALUE && c_parent != null) {
            return c_parent.getFrozenRows();
        } else if (c_frozenRows > 0) {
            return c_frozenRows;
        } else {
            return 0;
        }
    }

    private void loadFrozenColumns(Element node) {
        String frozenColumnsText = node.getChildTextTrim(WepsTableEnum.XML_frozencolumns);
        try {
            int frozenColumns = Integer.parseInt(frozenColumnsText);
            c_frozenColumns = frozenColumns;
        } catch (NumberFormatException nfe) {
        }
    }

    /**
     * Return the number of columns to be reserved as row headers.
     * @return The number of columns used as header rows.
     */
    public int getFrozenColumns() {
        if (c_frozenColumns == WepsTableEnum.NO_VALUE && c_parent != null) {
            return c_parent.getFrozenColumns();
        } else if (c_frozenColumns > 0) {
            return c_frozenColumns;
        } else {
            return 0;
        }
    }

    /**
     * Returns the list of Columns available to the table.
     * @return The list of columns available to the table.
     */
    public Column[] getColumns() {
        Column[] temp = new Column[0];
        return c_columns.toArray(temp);
    }

    /**
     * Returns the number of Columns available to the table.
     * @return The number of columns available to the table.
     */
    public int getColumnCount() {
        if (c_columns == null) {
            return 0;
        }
        int count = c_columns.size();
        return count;
    }

    /**
     * Add the specified Column to the list of available Columns.
     * @param column The Column to be added.
     */
    public void addColumn(Column column) {
        c_columns.add(column);
        putColumn(column);
    }

    /**
     * Return the Column at the specified index in the table.
     * @param columnIndex The index of the desired Column.
     * @return The Column at the specified index.
     */
    public Column getColumn(int columnIndex) {
        try {
            return c_columns.get(columnIndex);
        } catch (IndexOutOfBoundsException ioobe) {
            return null;
        }
    }

    /**
     * Return the number of rows used as column headers.
     * @return The number or rows used as column headers.
     */
    public int getLabelRowCount() {
        if (isColumnLabelDisplayed() == false) {
            return 0;
        }
        int count = getLabelMatrix().length;
        return count;
    }

    /**
     * Evaluate the supplied boolean expression.
     * @param expression The expression to be evaluated.
     * @param table 
     * @param rowIndex 
     * @param columnIndex 
     * @return The result of the expression.
     */
    public boolean evaluate(String expression, WepsTable table, int rowIndex, int columnIndex) {
        //Very very basic, only supports left of == equals right of == in a string comparison.
        int operatorIndex = expression.indexOf("==");
        if (operatorIndex < 0) {
            return false;
        }
        String left = expression.substring(0, operatorIndex);
        String right = expression.substring(operatorIndex + 2);

        if (left == null || right == null) {
            return false;
        }
        left = parse(left, table, rowIndex, columnIndex).toString();
        right = parse(right, table, rowIndex, columnIndex).toString();

        return left.equals(right);
    }    //Regex =  (?:\$\{([.[^\}]]*)\.([.[^\}]]*)\})|(?:\$\{([.[^\}]]*)\})    
    //This will match ${name.field}, name is in group 1 and field is in group 2, group 3 is a field without a name, ${field}
    private static final String parsePatternString =
            "(?:\\$\\{([.[^\\}]]*)\\.([.[^\\}]]*)\\})|(?:\\$\\{([.[^\\}]]*)\\})";
    private static final Pattern parsePattern = Pattern.compile(parsePatternString);
    //Regex For Events =  (?:\$X\{([.[^\}]]*)\.([.[^\}]]*)\})|(?:\$X\{([.[^\}]]*)\})
    private static final String parseXPatternString =
            "(?:\\$X\\{([.[^\\}]]*)\\.([.[^\\}]]*)\\})|(?:\\$X\\{([.[^\\}]]*)\\})";
    private static final Pattern parseXPattern = Pattern.compile(parseXPatternString);

    /**
     * 
     * @param text 
     * @param table 
     * @param rowIndex 
     * @param columnIndex 
     * @return 
     */
    public Object parse(String text, WepsTable table, int rowIndex, int columnIndex) {
        Object value;
        value = parseSpecial(text, table, rowIndex, columnIndex);
        value = parseExternal(value.toString(), table, rowIndex, columnIndex);
        value = parseInternal(value.toString(), table, rowIndex, columnIndex);
        return value;
    }

    private Object parseSpecial(String text, WepsTable table, int rowIndex, int columnIndex) {
        text = text.replace("${column.index}", String.valueOf(columnIndex));
        return text;
    }

    private Object parseExternal(String text, WepsTable table, int rowIndex, int columnIndex) {
        if (text == null) {
            return null;
        }
        Object value = text;
        if (text.indexOf("X") > 0) {
            int a = 0;
        }
        Matcher matcher = parseXPattern.matcher(text);
        while (matcher.find()) {
            String expression = matcher.group();
            String loneField = matcher.group(3);
            String field;
            String name;
            if (loneField != null && loneField.length() > 0) {
                name = null;
                field = loneField;
            } else {
                name = matcher.group(1);
                field = matcher.group(2);
            }
            ParseEvent event = new ParseEvent(text, table, rowIndex, columnIndex, expression, name, field);
            table.fireParseEvent(event);
            if (event.getParsed() != null) {
                text = text.replace(expression, event.getParsed().toString());
            }

            value = text;
        }
        return value;
    }

    private Object parseInternal(String text, WepsTable table, int rowIndex, int columnIndex) {
        if (text == null) {
            return null;
        }
        Matcher matcher = parsePattern.matcher(text);
        while (matcher.find()) {
            String expression = matcher.group();
            String loneField = matcher.group(3);
            Column column = null;
            String field;
            String name = null;
            if (loneField != null && loneField.length() > 0) {
                //Assume the current column.
                column = getColumn(columnIndex);
                field = loneField;
            } else {
                name = matcher.group(1);
                column = c_columnMap.get(name);
                field = matcher.group(2);
            }


            if (column != null) {
                ParseEvent event = new ParseEvent(text, table, rowIndex, columnIndex, expression, name, field);
                column.parse(event);
                Object value = event.getParsed();
                if (value == null) {
                    value = "#N/A#";
                }
                text = text.replace(expression, value.toString());
            }
        }
        return text;
    }
    
    public boolean columnIsBold(String text)
    {
        return boldedColumns.contains(text);
    }
}
