/*
 * TableMeta.java
 *
 * Created on February 21, 2006, 3:48 PM
 *
 */
package usda.weru.wmrm;

import java.util.*;
import org.jdom2.input.*;
import org.jdom2.*;

import usda.weru.util.*;
import java.awt.Color;
import com.klg.jclass.table.*;
import de.schlichtherle.truezip.file.TFile;
import java.io.IOException;
import java.io.UTFDataFormatException;
import usda.weru.util.table.ColumnFilter;
import usda.weru.util.table.filters.ColumnIdFilter;

/**
 *
 * @author joelevin
 */
public class TableMeta extends TableDataView {

    private static final long serialVersionUID = 1L;

    //XML Static Strings
    public static final String XML_ID = "id";
    public static final String XML_COLUMNS = "columns";
    public static final String XML_COLUMNGROUP = "columngroup";
    public static final String XML_COLUMN = "column";
    public static final String XML_LABEL = "label";
    public static final String XML_MINWIDTH = "minwidth";
    public static final String XML_MAXWIDTH = "maxwidth";
    public static final String XML_DATAKEY = "datakey";
    public static final String XML_ACTION = "action";
    public static final String XML_UNITS = "units";
    public static final String XML_DISPLAYUNITS = "displayunits";
    public static final String XML_FORMAT = "format";
    public static final String XML_SYSTEM = "system";
    public static final String XML_ADJUST = "adjust";
    public static final String XML_OPERATION = "operation";    //Styles
    public static final String XML_CELLSTYLES = "cellstyles";
    public static final String XML_STYLE = "style";
    public static final String XML_PARENT = "parent";
    public static final String XML_BACKGROUND = "background";
    public static final String XML_BORDER = "border";
    public static final String XML_BORDERSIDES = "bordersides";
    public static final String XML_CELLBORDERCOLOR = "cellbordercolor";
    public static final String XML_CLIPHINTS = "cliphints";
    public static final String XML_DATATYPE = "datatype";
    public static final String XML_EDITABLE = "editable";
    public static final String XML_EDITOR = "editor";
    public static final String XML_FOREGROUND = "foreground";
    public static final String XML_FONT = "font";
    public static final String XML_HORIZONTALALIGNMENT = "horizontalalignment";
    public static final String XML_RENDERER = "renderer";
    public static final String XML_REPEATBACKGROUND = "repeatbackground";
    public static final String XML_REPEATFOREGROUND = "repeatforeground";
    public static final String XML_TRAVERSABLE = "traversable";
    public static final String XML_VERTICALALIGNMENT = "verticalalignment";
    public static final String XML_HEADERSTYLE = "headerstyle";    //Threshold
    public static final String XML_THRESHOLD = "threshold";
    public static final String XML_LOWER = "lower";
    public static final String XML_UPPER = "upper";
    public static final String XML_INCLUSIVE = "inclusive";
    public static final String XML_CHECKZEROES = "checkzeros";
    public static final String XML_TEXT = "text";    //End Strings

    private ColumnGroupMeta c_rootColumnGroup;
    private Hashtable<String, CellStyleMeta> c_styles;
    private SortIconCellRenderer c_headerRenderer = new SortIconCellRenderer();
    //Stored as (row, column)
    private ColumnGroupMeta[][] c_headerMatrix;
    private CellStyleMeta c_defaultCellStyle;
    private CellStyleMeta c_defaultHeaderStyle;
    private String c_unitsSystem = "SI";
    private boolean c_loaded = false;
    private int[] row_map;    //Counter for hidden ids
    private int c_idCounter = 100;    //Sorting variables

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

    public void setUnitsSystem(String system) {
        c_unitsSystem = system;
    }

    public TableMeta(Element root) {
        this();
        init();
        loadFromXmlNode(root);
    }

    public TableMeta(TFile file) {
        this();
        init();
        loadFromFile(file);
    }

    private void init() {
        c_styles = new Hashtable<>();
        c_rootColumnGroup = new ColumnGroupMeta();
    }

    public void loadFromFile(TFile file) {
        try {
            if (!file.exists()) {
                return;
            }
            SAXBuilder builder = new SAXBuilder();
            Document document = builder.build(file);
            Element root = document.getRootElement();
            loadFromXmlNode(root);
        } catch (UTFDataFormatException udfe) {
        } catch (JDOMException | IOException jde) {
        }
    }

    public void loadFromXmlNode(Element root) {

        loadCellStyles(root);

        //Loadcolumns and groups
        Element columns = root.getChild(XML_COLUMNS);
        c_rootColumnGroup = new ColumnGroupMeta(columns);

        c_loaded = true;

    }

    private boolean isLoaded() {
        return c_loaded;
    }

    private void loadCellStyles(Element node) {
        //Load Cell Styles first so references can be made for columns which use a parent style
        Element stylesNode = node.getChild(XML_CELLSTYLES);

        if (stylesNode != null) //No cell styles to load.
        {
            stylesNode.getChildren(XML_STYLE).stream().map(o -> o).map(element -> new CellStyleMeta(element)).forEachOrdered(cellStyle -> {
                c_styles.put(cellStyle.getId(), cellStyle);
            });
        }

        //Load the default cell and header styles
        try {
            c_defaultCellStyle = c_styles.get(node.getChild("defaultcellstyle").getText());
        } catch (Exception e) {
        }

        try {
            c_defaultHeaderStyle = c_styles.get(node.getChild("defaultheaderstyle").getText());
        } catch (Exception e) {
        }
    }

    public Element toXml() {
        Element root = new Element(XML_COLUMNS);

        return root;
    }

    public void applyMetaToTable() {
        if (!isLoaded()) {
            return;
        }

        //Cell Styles and widths
        for (int i = 0; i < getNumColumns(); i++) {

            Vector<ColumnGroupMeta> bottomLayer = c_rootColumnGroup.getBottomColumnGroupVector();
            ColumnMeta column = bottomLayer.elementAt(i).getColumn();

            //Cell Widths
            table.setMaxWidth(i, column.getMaxWidths()[0]);
            table.setMinWidth(i, column.getMinWidths()[0]);

            //Cell Styles
            if (column.getCellStyle() != null) {
                table.setCellStyle(JCTableEnum.ALLCELLS, i, column.getCellStyle());
                //CellStyleModel style = column.getCellStyle();
                //table.setCellStyle(JCTableEnum.ALL, i, style);
            }

            //Header styles
            if (column.getHeaderStyle() != null) {
                if (column.getHeaderStyle().getCellRenderer() == null) {
                    column.getHeaderStyle().setCellRenderer(c_headerRenderer);
                }
                table.setCellStyle(new JCCellRange(getHeaderStarts(), i, getHeaderEnds(), i), column.getHeaderStyle());
            }

        }

        // Way to get the header row to work correctly for now
        table.setMinHeight(0, 100);
        table.setCellStyle(0, 0, c_styles.get("label"));
        
        if (getNumHeaderRows() > 0) {
            table.setRowHidden(-1, true);
            table.setFrozenRows(getNumHeaderRows());
        }

        boolean[][] used = new boolean[getHeaderMatrix().length][getHeaderMatrix()[0].length];

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

    }

    /**
     *
     * @param column which column to sort by
     * @param direction direction to sort (-999 = unsort)
     * @return
     */
    public boolean sortByColumn(int column, int direction) {
        resetRowMap();
        boolean result = false;

        if (column != -1) {
            //LOGGER.warn(column + "   " + direction + "   " + table.getFrozenRows() + "   " + table.getNumRows());

            // this is a terrible hack:
            // first, call the standard sort method. this will properly change some properties
            // of the table, but sort it incorrectly.
            //       table.sortByColumn(column, direction);
            // next, call a modified sort method. this will not change any properties,
            // but will actually sort the table correctly.
            result = usda.weru.wmrm.Sort.sortByColumn(table, column, direction,
                    table.getFrozenRows(), table.getNumRows() - 1, null);

            //Update cell renderers.
            c_headerRenderer.setColumns(column);
            c_headerRenderer.setDirection(direction);
        } else {
            c_headerRenderer.setColumns(null);
            c_headerRenderer.setDirection(null);
        }
        return result;
    }

    private JCCellRange getSpan(int rowIndex, int columnIndex, boolean[][] used) {
        if (used[rowIndex][columnIndex]) {
            return null;
        }
        JCCellRange span = new JCCellRange();
        ColumnGroupMeta group = getHeaderMatrix()[rowIndex][columnIndex];

        int endColumn = columnIndex;
        ColumnGroupMeta test = getHeaderMatrix()[rowIndex][endColumn];

        //How many columns are the same?  Catch if the entire row has the same header.
        while (test == group && endColumn + 1 < used[rowIndex].length) {
            test = getHeaderMatrix()[rowIndex][endColumn + 1];
            if (test == group) {
                endColumn++;
            }
        }

        int endRow = used.length;
        for (int c = columnIndex; c <= endColumn; c++) {
            int r = rowIndex;
            test = getHeaderMatrix()[r][c];
            while (test == group && r + 1 < used.length) {
                test = getHeaderMatrix()[r + 1][c];
                if (test == group) {
                    r++;
                }
            }
            if (r < endRow) {
                endRow = r;
            }
        }
        span.start_column = columnIndex;
        span.start_row = rowIndex;
        span.end_column = endColumn;
        span.end_row = endRow;

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

        return span;
    }

    public void linkTable(JCTable newTable) {
        table = newTable;
        //table.setDataSource(null);
        table.setDataView(this);
        applyMetaToTable();
    }

    public void setDataSource(TableNamedDataModel background) {
        //Wrap the data source
        final TableNamedDataModel c_background = background;
        TableNamedDataModel data = new TableNamedDataModel() {

            @Override
            public void addTableDataListener(JCTableDataListener listener) {
                c_background.addTableDataListener(listener);
            }

            @Override
            public void removeTableDataListener(JCTableDataListener listener) {
                c_background.removeTableDataListener(listener);
            }

            @Override
            public int getNumColumns() {
                int c = c_background.getNumColumns();
                return c;
            }

            @Override
            public int getNumRows() {
                return c_background.getNumRows();
            }

            @Override
            public Object getTableColumnLabel(int columnIndex) {
                return c_background.getTableColumnLabel(columnIndex);
            }

            @Override
            public Object getTableDataItem(int rowIndex, int columnIndex) {
                ColumnMeta column = getColumnMeta(columnIndex);
                return getTableDataItem(rowIndex, column.getDataKey());
            }

            @Override
            public Object getTableDataItem(int rowIndex, String column) {
                int a_rowIndex = rowIndex;
                return c_background.getTableDataItem(a_rowIndex, column);
            }

            @Override
            public Object getTableRowLabel(int rowIndex) {
                int a_rowIndex = rowIndex + getHeaderStarts() - getNumHeaderRows();
                return c_background.getTableRowLabel(a_rowIndex);
            }

            @Override
            public boolean setTableDataItem(Object o, int row, int column) {
                if (o == null) {
                    return false;
                }

                setLabelAt(o.toString(), row, column);

                return true;
            }
        };

        if (dataSource != data) {
            if (dataSource != null) {
                dataSource.removeTableDataListener(this);
            }
            dataSource = data;
            dataSource.addTableDataListener(this);
        }

    }

    private void buildHeaderMatrix() {
        Vector<ColumnGroupMeta> bottomLayer = c_rootColumnGroup.getBottomColumnGroupVector();
        int numberOfRows = c_rootColumnGroup.getDeepestGroup().getDepth() - 1;
        int numberOfColumns = bottomLayer.size();
        c_headerMatrix = new ColumnGroupMeta[numberOfRows][numberOfColumns];
        for (int c = 0; c < numberOfColumns; c++) {
            addGroupToMatrix(bottomLayer.elementAt(c), numberOfRows - 1, c);
        }
    }

    private void addGroupToMatrix(ColumnGroupMeta group, int rowIndex, int columnIndex) {
        //Count the number of child columns.  The group is added to that to cells going to the right to span
        //Over all children.
        int numberOfColumnsToSpan = group.getBottomColumnGroupVector().size();
        if (numberOfColumnsToSpan == 0) {
            numberOfColumnsToSpan = 1;
        }
        int numberOfRowsToSpan = rowIndex - group.getDepth() + 3;
        int a = 0;
        for (int c = 0; c < numberOfColumnsToSpan; c++) {
            for (int r = 0; r < numberOfRowsToSpan; r++) {
                c_headerMatrix[rowIndex - r][columnIndex + c] = group;
            }
        }

        if (group.getParent() != null && group.getParent().getParent() != null) {
            if (group.getParent().getColumnGroups().firstElement() == group) {
                //This is the fist child, so it adds the parent.
                //               System.out.println("Span:"+numberOfRowsToSpan);
                addGroupToMatrix(group.getParent(), rowIndex - numberOfRowsToSpan, columnIndex);
            }
        }

    }

    public void setLabelAt(String label, int rowIndex, int columnIndex) {
        setHeaderAt(label, rowIndex, columnIndex);
    }

    private void setHeaderAt(String label, int rowIndex, int columnIndex) {
        if (c_headerMatrix == null) {
            buildHeaderMatrix();
        }
        c_headerMatrix[rowIndex][columnIndex].setLabel(label);
    }

    public String getLabelAt(int rowIndex, int columnIndex) {
        return getHeaderAt(rowIndex, columnIndex).getLabel();
    }

    private ColumnGroupMeta getHeaderAt(int rowIndex, int columnIndex) {
        if (rowIndex < 0 || columnIndex < 0) {
            return null;
        }
        if (c_headerMatrix == null) {
            buildHeaderMatrix();
        }
        return c_headerMatrix[rowIndex][columnIndex];
    }

    private ColumnGroupMeta[][] getHeaderMatrix() {
        if (c_headerMatrix == null) {
            buildHeaderMatrix();
        }
        return c_headerMatrix;
    }

    @Override
    public Object getObject(int rowIndex, int columnIndex) {
        int a_rowIndex = getDataRow(rowIndex);
        int a_columnIndex = getDataColumn(columnIndex);

        int start = getHeaderStarts();
        int num = getNumHeaderRows();
        if (a_rowIndex < 0) {
            return getTableColumnLabel(a_columnIndex);
        } else if (a_rowIndex < (start + num)) {
            //Custom header cells
            ColumnGroupMeta meta = getHeaderAt(a_rowIndex, a_columnIndex);
            return meta != null ? meta.getLabel() : null;

        } else {
            return getTableDataItem(rowIndex, columnIndex);
        }
    }

    @Override
    public Object getTableDataItem(int rowIndex, int columnIndex) {
        int a_rowIndex = getDataRow(rowIndex) + getHeaderStarts() - getNumHeaderRows();
        int a_columnIndex = getDataColumn(columnIndex);
        //Adjust the row index to account for header rows
        ColumnMeta column = getColumnMeta(a_columnIndex);
        Object value = dataSource.getTableDataItem(a_rowIndex, a_columnIndex);
        value = column.applyMetaToValue(value);
        return value;
    }

    @Override
    public boolean setRowMap(int[] new_map) {
        int rows = getNumRows();

        if (new_map != null && new_map.length != rows) {
            return (false);
        }

        // Cancel edit if in progress
        table.cancelEdit(true);
        needs_row_map = true;
        row_map = new_map != null ? Arrays.copyOf(new_map, new_map.length) : null;
        table.repaint();

        return (true);
    }

    @Override
    public int[] getRowMap() {
        int rows = getNumRows();
        if (row_map == null || row_map.length != rows) {
            resizeRowMap();
        }
        return row_map != null ? Arrays.copyOf(row_map, row_map.length) : null;
    }

    @Override
    public int getDataRow(int row) {
        // Only lookup the row if the mapped rows array is needed
        if (row_map == null) {
            return (row);
        }

        int rows = getNumRows();
        if (row_map.length < rows) {
            resizeRowMap();
        }
        if (row >= 0 && row < rows) {
            return (row_map[row]);
        } else {
            return (JCTableEnum.NOVALUE);
        }
    }
    private boolean needs_row_map;

    @Override
    protected void resizeRowMap() {
        // Don't do anything if the mapped rows array isn't needed
        if (!needs_row_map) {
            return;
        }

        int rows = getNumRows();

        int[] temp = new int[rows];
        for (int i = 0; i < rows; i++) {
            if (row_map == null || i >= row_map.length) {
                temp[i] = i;
            } else {
                temp[i] = row_map[i];
            }
        }
        row_map = temp;
    }

    @Override
    public void resetRowMap() {
        needs_row_map = false;

        if (row_map != null) {
            int rows = getNumRows();
            int[] temp = new int[rows];
            for (int i = 0; i < rows; i++) {
                temp[i] = i;
            }
            setRowMap(temp);
        } else {
            row_map = null;
        }
    }

    @Override
    public Object getTableColumnLabel(int columnIndex) {
        return c_rootColumnGroup.getBottomColumnGroupVector().elementAt(columnIndex).getLabel();
    }

    public int getHeaderStarts() {
        if (c_rootColumnGroup.getDeepestGroup().getDepth() > 1) {
            return 0;
        } else {
            return -1;
        }
    }

    public int getHeaderEnds() {
        return getHeaderStarts() + getNumHeaderRows() - 1;
    }

    public int getNumHeaderRows() {
        return getHeaderMatrix().length;
    }

    //Throws an exception when the config file was not found.
    @Override
    public int getNumColumns() {
        int num = getHeaderMatrix()[0].length;
        return num;
    }

    public ColumnMeta getColumnMeta(int columnIndex) {
        return c_rootColumnGroup.getBottomColumnGroupVector().elementAt(columnIndex).getColumn();
    }

    @Override
    public void dataChanged(JCTableDataEvent de) {
        super.dataChanged(de);
    }

    public void fromXml() {
    }

    @Override
    public int getNumRows() {
        return (dataSource != null ? dataSource.getNumRows() : 0) + getNumHeaderRows();

    }

    @Override
    public void setTable(JCTable jCTable) {
        super.setTable(jCTable);
        applyMetaToTable();
    }
    private int[] c_columnMap;

    @Override
    public synchronized int[] getColumnMap() {
        if (c_columnMap == null) {
            buildColumnMap();
        }
        return c_columnMap != null ? Arrays.copyOf(c_columnMap, c_columnMap.length) : null;
    }

    @Override
    public boolean setColumnMap(int[] map) {
        c_columnMap = map != null ? Arrays.copyOf(map, map.length) : null;
        return (c_columnMap != null);
    }

    private void buildColumnMap() {
        int[] tempMap = new int[getNumColumns()];
        for (int c = 0; c < getNumColumns(); c++) {
            ColumnMeta column = getColumnMeta(c);
            String dataKey = column.getDataKey();
            int dataSourceIndex = findColumnLabel(dataKey);
            if (dataSourceIndex >= 0) {
                tempMap[c] = dataSourceIndex;
            } else {
                tempMap[c] = c;
            }
        }
        setColumnMap(tempMap);
    }

    private int findColumnLabel(String text) {
        for (int c = 0; c < dataSource.getNumColumns(); c++) {
            Object column = dataSource.getTableColumnLabel(c);
            if (text.equals(column)) {
                return c;
            }
        }
        return -1;
    }

    class ColumnGroupMeta {

        private Vector<ColumnGroupMeta> c_childGroups;
        private ColumnMeta c_column;
        private ColumnGroupMeta c_parent;
        private String[] c_labels;
        private String c_label = null;
        boolean c_ignoreColumn = false;
        private CellStyleMeta c_headerStyle;

        private void init() {
            c_childGroups = new Vector<>();
        }

        public ColumnGroupMeta() {
            init();
        }

        public ColumnGroupMeta(Element node) {
            init();
            fromXml(node);
        }

        public CellStyleMeta getHeaderStyle() {
            return (c_headerStyle != null) ? c_headerStyle : c_defaultHeaderStyle;
        }

        private void loadHeaderStyles(Element node) {
            //Load cell styles
            Element styleNode = node.getChild(XML_HEADERSTYLE);
            if (styleNode == null) {
                return;
            }
            String cellId = null;
            cellId = styleNode.getTextTrim();

            if (cellId != null && cellId.length() > 0) {
                c_headerStyle = c_styles.get(cellId);
            } else {
                c_headerStyle = new CellStyleMeta(styleNode);
            }
        }

        public void setParent(ColumnGroupMeta parent) {
            c_parent = parent;
        }

        public void setColumn(ColumnMeta column) {
            setColumn(column, false);
        }

        public void setColumn(ColumnMeta column, boolean ignore) {
            c_ignoreColumn = ignore;
            c_column = column;
        }

        public ColumnGroupMeta getParent() {
            return c_parent;
        }

        @Override
        public String toString() {
            if (c_parent == null) {
                return "CG: Root";
            }
            if (isColumn()) {
                return "CM: " + getLabel();
            }
            return "CG: " + getLabel();
        }

        public void setLabel(String label) {
            c_label = label;
        }

        public String getLabel() {
            String label = null;
            if (c_label != null) {
                label = c_label;
            }
            if (isColumn()) {
                label = getColumn().getLabel();
            }
            return label;
        }

        public boolean isColumn() {
            if (c_ignoreColumn) {
                return false;
            }
            return (c_column != null);
        }

        public void addChild(ColumnGroupMeta group) {
            c_childGroups.add(group);
            group.setParent(this);
        }

        public ColumnMeta getColumn() {
            return c_column;
        }

        public Vector<ColumnGroupMeta> getColumnGroups() {
            return c_childGroups;
        }

        public int getNumberOfColumnsToSpan() {
            return getBottomColumnGroupVector().size();
        }

        public ColumnMeta getColumnAt(int columnIndex) {
            return getBottomColumnGroupVector().elementAt(columnIndex).getColumn();
        }

        public Vector<ColumnGroupMeta> getBottomColumnGroupVector() {
            return buildColumnGroupVector(null);
        }

        public Vector<ColumnGroupMeta> buildColumnGroupVector(Vector<ColumnGroupMeta> children) {
            if (children == null) {
                children = new Vector<>();
            }
            for (ColumnGroupMeta group : c_childGroups) {
                if (group.isColumn()) {
                    //Is a column, add to the vector
                    children.add(group);
                } else {
                    group.buildColumnGroupVector(children);
                }
            }
            return children;
        }

        public void fromXml(Element node) {
            if (XmlHelper.isValid(node) == false) {
                return;
            }
            c_label = node.getChildText(XML_LABEL);
            //loadLabels(node);
            Vector<Element> columnsAndGroups = XmlHelper.getChildren(node, XML_COLUMNGROUP, XML_COLUMN);
            columnsAndGroups.forEach(columnOrGroup -> {
                if (columnOrGroup.getName().equals(XML_COLUMNGROUP)) {
                    //Found another group
                    ColumnGroupMeta group = new ColumnGroupMeta(columnOrGroup);
                    addChild(group);
                } else if (columnOrGroup.getName().equals(XML_COLUMN)) {
                    //This is a column.  Create a group for each label
                    ColumnGroupMeta wrappingGroup = new ColumnGroupMeta();
                    ColumnGroupMeta lastGroup = null;
                    ColumnMeta column = new ColumnMeta(columnOrGroup);
                    if (column.getLabels() != null) {
                        for (int i = 0; i < column.getLabels().length; i++) {
                            ColumnGroupMeta loopGroup = new ColumnGroupMeta();
                            loopGroup.setLabel(column.getLabels()[i]);
                            //This is the first label, so we tie it to the wrapping pointer
                            if (i == 0) {
                                wrappingGroup = loopGroup;
                            } else {
                                lastGroup.addChild(loopGroup);
                            }
                            lastGroup = loopGroup;

                            //This is the column group
                            if (i == column.getLabels().length - 1) {
                                loopGroup.setColumn(column);
                            } else {
                                loopGroup.setColumn(column, true);
                            }
                        }
                    }

                    addChild(wrappingGroup);
                }
            });

            loadHeaderStyles(node);
        }

//        private void loadLabels(Element node) {
//            List<Element> labels = node.getChildren(XML_LABEL);
//            if (labels.size() > 0) {
//                c_labels = new String[labels.size()];
//                int i = 0;
//                for (Object o : labels) {
//                    c_labels[i] = ((Element) o).getText();
//                    i++;
//                }
//            } else {
//                c_labels = null;
//            }
//
//        }

        public String[] getLabels() {
            return c_labels;
        }

        public int getDepth() {
            int depth = 0;
            ColumnGroupMeta group = this;
            while (group != null) {
                depth++;
                group = group.getParent();
            }
            return depth;
        }

        public ColumnGroupMeta getDeepestGroup() {
            ColumnGroupMeta deepest = this;
            if (deepest == null) {
                deepest = this;
            }
            for (ColumnGroupMeta child : c_childGroups) {
                ColumnGroupMeta test = child.getDeepestGroup();
                if (test.getDepth() > deepest.getDepth()) {
                    deepest = test;
                }
            }
            return deepest;
        }
    }

    class ColumnMeta {

        private String[] c_labels;
        private String c_dataKey;
        private String c_action;
        private ConversionUnit c_baseUnits = null;
        private Map<String, ConversionUnit> c_displayUnits;      //System to unit
        private Map<String, String> c_displayUnitsFormats;      //System to format
        private Map<String, DataThreshold> c_thresholds;
        private DataAdjustment[] c_dataAdjustments;
        private CellStyleMeta c_cellStyle;
        private CellStyleMeta c_headerStyle;
        private int[] c_cellMaxWidth;
        private int[] c_cellMinWidth;
        private ColumnFilter c_id;

        private void init() {
            c_displayUnits = new HashMap<>();
            c_displayUnitsFormats = new HashMap<>();
            c_thresholds = new HashMap<>();
            c_labels = new String[0];
        }

        public ColumnMeta(String headerText, String dataKey) {
            init();
            c_labels = new String[1];
            c_labels[0] = headerText;
            c_dataKey = dataKey;
            c_action = "";
        }

        public CellStyleMeta getCellStyle() {
            return (c_cellStyle != null) ? c_cellStyle : c_defaultCellStyle;
        }

        public CellStyleMeta getHeaderStyle() {
            return (c_headerStyle != null) ? c_headerStyle : c_defaultHeaderStyle;
        }

        public int[] getMaxWidths() {
            return c_cellMaxWidth;
        }

        public int[] getMinWidths() {
            return c_cellMinWidth;
        }

        ColumnMeta(Element element) {
            init();
            fromXml(element);
        }

        //Requires more work
        public Element toXml() {
            Element root = new Element(XML_COLUMN);
            Element header = new Element(XML_LABEL);
            Element dataKey = new Element(XML_DATAKEY);
            Element action = new Element(XML_ACTION);

            header.setText(getLabel());
            dataKey.setText(getDataKey());
            action.setText(getAction());

            root.addContent(header);
            root.addContent(dataKey);
            root.addContent(action);

            if (c_baseUnits != null) {
                Element baseUnits = new Element(XML_UNITS);
                baseUnits.setText(c_baseUnits.getName());
                root.addContent(baseUnits);
            }

            return root;
        }

        public void fromXml(Element root) {
            Element baseUnits = root.getChild(XML_UNITS);
            List<Element> displayUnits = root.getChildren(XML_DISPLAYUNITS);

            loadLabels(root);
            c_dataKey = XmlHelper.getText(root.getChild(XML_DATAKEY), "");
            c_action = XmlHelper.getText(root.getChild(XML_ACTION), "");

            try {
                c_baseUnits = ConversionCalculator.getUnitFromTag(baseUnits.getText());
            } catch (Exception e) {
            }

            //Load display units linked to a measurement system (SI, or US, etc...)
            for (Object o : displayUnits) {
                Element unitElement = (Element) o;
                String system = unitElement.getAttribute(XML_SYSTEM).getValue();
                String format = unitElement.getAttributeValue(XML_FORMAT);
                if (unitElement.getText().length() > 0) {
                    try {
                        ConversionUnit unit = ConversionCalculator.getUnitFromTag(unitElement.getText());
                        if (system != null && system.length() > 0 && unit != null) {
                            c_displayUnits.put(system, unit);
                            if (format != null && format.length() > 0) {
                                c_displayUnitsFormats.put(system, format);
                            }
                        }
                    } catch (Exception e) {
                    }
                }

            }
            loadCellStyles(root);
            loadHeaderStyles(root);
            loadDataAdjustments(root);
            loadThresholds(root);
            loadWidths(root);
            loadIds(root);

        }

        private void loadIds(Element node) {
            String id = node.getAttributeValue(XML_ID);
            if (id != null) {
                c_id = new ColumnIdFilter(id);
            }
        }

        private void loadCellStyles(Element node) {
            //Load cell styles
            Element styleNode = node.getChild(XML_STYLE);
            if (styleNode == null) {
                return;
            }
            String cellId = null;
            cellId = styleNode.getTextTrim();

            if (cellId != null && cellId.length() > 0) {
                c_cellStyle = c_styles.get(cellId);
            } else {
                c_cellStyle = new CellStyleMeta(styleNode);
            }
        }

        private void loadHeaderStyles(Element node) {
            //Load cell styles
            Element styleNode = node.getChild(XML_HEADERSTYLE);
            if (styleNode == null) {
                return;
            }
            String cellId = null;
            cellId = styleNode.getTextTrim();

            if (cellId != null && cellId.length() > 0) {
                c_headerStyle = c_styles.get(cellId);
            } else {
                c_headerStyle = new CellStyleMeta(styleNode);
            }
        }

        private void loadDataAdjustments(Element node) {
            //Load adjustments for data
            List<Element> dataAdjustments = node.getChildren(XML_ADJUST);
            c_dataAdjustments = new DataAdjustment[dataAdjustments.size()];
            int i = 0;
            for (Object o : dataAdjustments) {
                Element adjustElement = (Element) o;
                c_dataAdjustments[i] = new DataAdjustment(adjustElement);
                i++;
            }
        }

        private void loadThresholds(Element node) {
            List<Element> thresholds = node.getChildren(XML_THRESHOLD);
            thresholds.stream().map(o -> o).map(thresholdElement -> new DataThreshold(thresholdElement)).forEachOrdered(threshold -> {
                c_thresholds.put(threshold.getSystem(), threshold);
            });
        }

        private void loadLabels(Element node) {
            List<Element> labels = node.getChildren(XML_LABEL);
            if (labels.size() > 0) {
                c_labels = new String[labels.size()];
                int i = 0;
                for (Object o : labels) {
                    c_labels[i] = ((Element) o).getText();
                    i++;
                }
            } else {
                c_labels = null;
            }

        }

        // Loads in values for setting the widths of the cells
        private void loadWidths(Element node) {
            List<Element> maxWidth = XmlHelper.getChildren(node, XML_MAXWIDTH);
            List<Element> minWidth = XmlHelper.getChildren(node, XML_MINWIDTH);
            c_cellMaxWidth = widthArrayLoop(maxWidth, XML_MAXWIDTH);
            c_cellMinWidth = widthArrayLoop(minWidth, XML_MINWIDTH);
        }

        // Loops over the items in the specified Element list to put them into a
        // usable array of ints
        private int[] widthArrayLoop(List<Element> widths, String widthName) {
            int[] returnArray = new int[widths.size()];
            int i = 0;
            for (Element width : widths) {
                if (width.getName().equals(widthName)) {
                    returnArray[i] = Integer.parseInt((width).getText());
                    i++;
                }
            }

            return returnArray;
        }

        public ColumnFilter getId() {
            return c_id;
        }

        public String[] getLabels() {
            return c_labels;
        }

        public String getLabel() {
            String label = c_labels[c_labels.length - 1];
            /* System.out.print("label is - " + label); */
            label = parseSpecialText(label);
            /* System.out.println(" : now label is - " + label); */
            return label;
        }

        public Object applyMetaToValue(Object value) {
            value = adjustData(value);
            if (isUsingUnits() && value instanceof Double) {
                Double dValue = Double.parseDouble(value.toString());

                ConvertedValue cValue = new ConvertedValue();
                cValue.setBaseUnits(getBaseUnits());
                getDisplayUnitsTable().keySet().stream().map(system -> {
                    cValue.addDisplayUnits(system, getDisplayUnits(system));
                    return system;
                }).forEachOrdered(_item -> {
                    cValue.setValue(dValue);
                });
                String format = c_unitsSystem != null ? this.c_displayUnitsFormats.get(c_unitsSystem) : null;
                if (format != null) {
                    cValue.setOutputFormat(format);
                }
                cValue.setDisplaySystem(c_unitsSystem);

                //Is the value beyond any thresholds?
                DataThreshold threshold = c_thresholds.get(c_unitsSystem);
                if (threshold == null) {
                    threshold = c_thresholds.get("DEFAULT");
                }
                if (threshold != null) {
                    if (threshold.excedingThreshold(cValue.getDisplayValue())) {
                        return threshold.getText(cValue.getDisplayValue());
                    }
                }

                return cValue;
            } else {
                return value;
            }

        }

        public String parseSpecialText(String text) {
            if (text == null) {
                return text;
                //Do special stuff
            }
            String oldText = text;
            try {
                text = text.replaceAll("#displayunits_abbreviation", getDisplayUnits(c_unitsSystem).getAbbreviation());
                text = text.replaceAll("#displayunits", getDisplayUnits(c_unitsSystem).getName());
            } catch (Exception e) {
                text = oldText;
            }
            return text;
        }

        public String getDataKey() {
            return c_dataKey;
        }

        public String getAction() {
            return c_action;
        }

        public boolean isUsingUnits() {
            return (c_baseUnits != null);
        }

        public ConversionUnit getBaseUnits() {
            return c_baseUnits;
        }

        public Map<String, ConversionUnit> getDisplayUnitsTable() {
            return c_displayUnits;
        }

        public ConversionUnit getDisplayUnits(String system) {
            return c_displayUnits.get(system);
        }

        @Override
        public String toString() {
            return "CM: " + c_dataKey;
        }

        public Object adjustData(Object value) {
            for (DataAdjustment adjustment : c_dataAdjustments) {
                value = adjustment.adjust(value);
            }
            return value;
        }
    }

    static class DataAdjustment {

        private String c_operation;
        private String c_adjustment;

        public DataAdjustment(String operation, String adjustment) {
            c_operation = operation;
            c_adjustment = adjustment;
        }

        public DataAdjustment(Element node) {
            fromXml(node);
        }

        public Object adjust(Object value) {
            try {
                if (c_operation == null || c_adjustment == null) {
                    return value;
                }
                if (c_operation.equals("multiply")) {
                    double adjustment = Double.parseDouble(c_adjustment);
                    double parsedValue = Double.parseDouble(value.toString());
                    double adjustedValue = parsedValue * adjustment;
                    if (adjustedValue == 0) {
                        adjustedValue = 0;
                    }
                    value = adjustedValue;
                }
                return value;
            } catch (NullPointerException npe) {
                return value;
            }
        }

        public void fromXml(Element root) {
            c_operation = root.getAttribute(XML_OPERATION).getValue();
            c_adjustment = root.getText();
        }

        public Element toXml() {
            Element root = new Element(XML_ADJUST);
            if (c_operation.length() > 0) {
                root.setAttribute(XML_OPERATION, c_operation);
            }
            if (c_adjustment.length() > 0) {
                root.setText(c_adjustment);
            }
            return root;
        }
    }

    static class DataThreshold {

        private String c_system;
        private double c_lower = Double.NaN;
        private boolean c_lowerInclusive = false;
        private boolean c_checkZeros = false;
        private String c_lowerText = null;
        private double c_upper = Double.NaN;
        private boolean c_upperInclusive = false;
        private String c_upperText = null;

        public DataThreshold(Element node) {
            fromXml(node);
        }

        public void fromXml(Element node) {
            loadSystem(node);
            loadLower(node);
            loadUpper(node);
        }

        public void loadSystem(Element node) {
            c_system = node.getAttributeValue(XML_SYSTEM);
        }

        public void loadLower(Element node) {
            Element lower = node.getChild(XML_LOWER);
            if (lower != null) {
                try {
                    c_lower = Double.parseDouble(lower.getValue());
                } catch (NumberFormatException nfe) {
                    c_lower = Double.NaN;
                    return;
                }
                c_lowerInclusive = Boolean.parseBoolean(lower.getAttributeValue(XML_INCLUSIVE));
                c_checkZeros = Boolean.parseBoolean(lower.getAttributeValue(XML_CHECKZEROES));
                c_lowerText = lower.getAttributeValue(XML_TEXT);

            } else {
                c_lower = Double.NaN;
                c_lowerInclusive = false;
                c_lowerText = null;
            }
        }

        public void loadUpper(Element node) {
            Element upper = node.getChild(XML_UPPER);
            if (upper != null) {
                try {
                    c_upper = Double.parseDouble(upper.getValue());
                } catch (NumberFormatException nfe) {
                    c_upper = Double.NaN;
                    return;
                }
                c_upperInclusive = Boolean.parseBoolean(upper.getAttributeValue(XML_INCLUSIVE));
                c_upperText = upper.getAttributeValue(XML_TEXT);
            } else {
                c_upper = Double.NaN;
                c_upperInclusive = false;
                c_upperText = null;
            }
        }

        public boolean excedingThreshold(double value) {
            return outisdeLowerBound(value) || outisdeUpperBound(value);
        }

        public boolean outisdeLowerBound(double value) {
            //Special case for zeros
            if (!c_checkZeros && value == 0) {
                return false;
            }
            if (isLowerBoundInclusive() && value <= c_lower) {
                return true;
            } else {
                return value < c_lower;
            }
        }

        public boolean outisdeUpperBound(double value) {
            if (isUpperBoundInclusive() && value >= c_upper) {
                return true;
            } else {
                return value > c_upper;
            }
        }

        public String getText(double value) {
            if (outisdeLowerBound(value)) {
                return getLowerBoundText();
            } else if (outisdeUpperBound(value)) {
                return this.getUpperBoundText();
            } else {
                return null;
            }
        }

        public String getSystem() {
            if (c_system != null) {
                return c_system;
            } else {
                return "DEFAULT";
            }
        }

        public void setSystem(String system) {
            c_system = system;
        }

        public boolean hasLowerBound() {
            return !Double.isNaN(c_lower);
        }

        public double getLowerBound() {
            return c_lower;
        }

        public void setLowerBound(double bound) {
            c_lower = bound;
        }

        public boolean isLowerBoundInclusive() {
            return c_lowerInclusive;
        }

        public void setLowerBoundInclusive(boolean inclusive) {
            c_lowerInclusive = inclusive;
        }

        public boolean hasLowerBoundText() {
            return c_lowerText != null;
        }

        public String getLowerBoundText() {
            if (hasLowerBoundText()) {
                return c_lowerText;
            } else {
                String symbol = (isLowerBoundInclusive()) ? "<=" : "<";
                return symbol + Double.toString(getLowerBound());

            }
        }

        public void setLowerBoundText(String text) {
            c_lowerText = text;
        }

        public boolean hasUpperBound() {
            return !Double.isNaN(c_upper);
        }

        public double getUpperBound() {
            return c_upper;
        }

        public void setUpperBound(double bound) {
            c_upper = bound;
        }

        public boolean isUpperBoundInclusive() {
            return c_upperInclusive;
        }

        public void setUpperBoundInclusive(boolean inclusive) {
            c_upperInclusive = inclusive;
        }

        public boolean hasUpperBoundText() {
            return c_upperText != null;
        }

        public String getUpperBoundText() {
            if (hasUpperBoundText()) {
                return c_upperText;
            } else {
                String symbol = (isUpperBoundInclusive()) ? ">=" : ">";
                return symbol + Double.toString(getUpperBound());

            }
        }

        public void setUpperBoundText(String text) {
            c_upperText = text;
        }
    }

    class CellStyleMeta extends com.klg.jclass.table.JCCellStyle {

        private static final long serialVersionUID = 1L;

        private String c_id;

        public CellStyleMeta(Element node) {
            fromXml(node);
        }

        public String getId() {
            return c_id;
        }

        public void fromXml(Element node) {
            loadId(node);
            loadParent(node);
            loadTraversable(node);
            loadBackgroundColors(node);
            loadForegroundColors(node);
            loadBorders(node);
            loadAlignments(node);

        }

        private void loadId(Element node) {
            //Get Id                
            if (XmlHelper.hasContent(node.getAttributeValue(XML_ID))) {
                c_id = node.getAttributeValue(XML_ID);
            } else {
                //Get Id from counter
                c_id = Integer.toString(c_idCounter++);
            }
        }

        private void loadParent(Element node) {
            //Set Parent Style
            if (XmlHelper.hasContent(node.getAttribute(XML_PARENT))) {
                CellStyleMeta parentStyle = c_styles.get(node.getAttribute(XML_PARENT).getValue());
                //Has the parent been loaded?
                if (parentStyle != null) {
                    setParentStyle(parentStyle);
                }
            }
        }

        private void loadTraversable(Element node) {
            String traversableText = node.getChildTextTrim(XML_TRAVERSABLE);
            if (traversableText != null) {
                String[] falseStrings = {"false", "no", "0"};
                for (String test : falseStrings) {
                    if (traversableText.equalsIgnoreCase(test)) {
                        setTraversable(false);
                    }
                    break;
                }
            } else {
                setTraversable(true);
            }
        }

        private void loadBackgroundColors(Element node) {
            List<Element> backgroundColors = node.getChildren(XML_BACKGROUND);
            //Set the background color to the first color
            if (backgroundColors.size() == 1) {
                Color color = XmlHelper.parseColor(backgroundColors.get(0));
                setBackground(color);
                setRepeatBackground(JCTableEnum.REPEAT_NONE);
            }
            if (backgroundColors.size() > 1) {
                //Might be a repeat policy
                try {

                    //We have a valid policy, set the repeating colors
                    setRepeatBackgroundColors(XmlHelper.parseColorList(backgroundColors));

                    //Convert policy to JC Constant Ennum
                    int backgroundRepeatPolicy = XmlHelper.getJCTableEnum(node.getChildTextTrim(XML_REPEATBACKGROUND));
                    setRepeatBackground(backgroundRepeatPolicy);
                } catch (IllegalArgumentException | NullPointerException iae) {
                }

            }
        }

        private void loadForegroundColors(Element node) {
            List<Element> foregroundColors = node.getChildren(XML_FOREGROUND);
            //Set the background color to the first color
            if (foregroundColors.size() == 1) {
                Color color = XmlHelper.parseColor(foregroundColors.get(0));
                setForeground(color);
                setRepeatForeground(JCTableEnum.REPEAT_NONE);
            }
            if (foregroundColors.size() > 1) {
                //Might be a repeat policy
                try {
                    //We have a valid policy, set the repeating colors
                    setRepeatForegroundColors(XmlHelper.parseColorList(foregroundColors));

                    //Convert policy to JC Constant Ennum
                    int foregroundRepeatPolicy = XmlHelper.getJCTableEnum(node.getChildTextTrim(XML_REPEATFOREGROUND));
                    setRepeatForeground(foregroundRepeatPolicy);

                } catch (IllegalArgumentException | NullPointerException iae) {
                }

            }
        }

        private void loadBorders(Element node) {
            int borderPolicy = XmlHelper.getJCTableEnum(node.getChildTextTrim(XML_BORDER));
            setCellBorder(new JCCellBorder(borderPolicy));
        }

        private void loadAlignments(Element node) {
            int horizontalPolicy = XmlHelper.getJCTableEnum(node.getChildTextTrim(XML_HORIZONTALALIGNMENT),
                    getHorizontalAlignment());
            setHorizontalAlignment(horizontalPolicy);

            int verticalPolicy = XmlHelper.getJCTableEnum(node.getChildTextTrim(XML_VERTICALALIGNMENT), getVerticalAlignment());
            setVerticalAlignment(verticalPolicy);
        }
    }

    public static interface TableNamedDataModel extends EditableTableDataModel {

        public Object getTableDataItem(int rowIndex, String column);
    }
}
