/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2008 jOpenDocument, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU
 * General Public License Version 3 only ("GPL").  
 * You may not use this file except in compliance with the License. 
 * You can obtain a copy of the License at http://www.gnu.org/licenses/gpl-3.0.html
 * See the License for the specific language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 * 
 */

package org.jopendocument.dom.spreadsheet;

import org.jopendocument.dom.NS;
import org.jopendocument.util.CollectionUtils;

import java.awt.Point;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;

import org.jdom.Attribute;
import org.jdom.Element;
import org.jdom.Namespace;

/**
 * A single sheet in a spreadsheet.
 * 
 * @author Sylvain
 */
public class Sheet extends CalcNode {

    static Element createEmpty(NS ns) {
        return new Element("table", ns.getTABLE());
    }

    private final SpreadSheet parent;
    private final List<Row> rows;
    private final List<Column> cols;

    @SuppressWarnings("unchecked")
    Sheet(SpreadSheet parent, Element local) {
        super(local);
        this.parent = parent;

        this.rows = (List<Row>) flatten(false);
        this.cols = (List<Column>) flatten(true);
    }

    @SuppressWarnings("unchecked")
    private List<? extends CalcNode> flatten(boolean col) {
        final String childName = col ? "column" : "row";
        final List<Element> children = this.getElement().getChildren("table-" + childName, getTABLE());
        // not final, since iter.add() does not work repeatedly (it returns the added items), and
        // thus we must recreate an iterator each time
        ListIterator<Element> iter = children.listIterator();
        while (iter.hasNext()) {
            final Element row = iter.next();
            final Attribute repeatedAttr = row.getAttribute("number-" + childName + "s-repeated", getTABLE());
            if (repeatedAttr != null) {
                row.removeAttribute(repeatedAttr);
                final int index = iter.previousIndex();
                int repeated = Integer.parseInt(repeatedAttr.getValue());
                if (repeated > 60000) {
                    repeated = 10;
                }
                // -1 : we keep the original row
                for (int i = 0; i < repeated - 1; i++) {
                    final Element clone = (Element) row.clone();
                    iter.add(clone);
                }
                // restart after the added rows
                iter = children.listIterator(index + repeated);
            }
        }

        final List<CalcNode> res = new ArrayList<CalcNode>(children.size());
        for (final Element clone : children) {
            // have to cast otherwise javac complains !!!
            res.add(col ? new Column(clone) : (CalcNode) new Row(this, clone));
        }
        return res;
    }

    public Object getPrintRanges() {
        return this.getElement().getAttributeValue("print-ranges", this.getTABLE());
    }

    public void setPrintRanges(String s) {
        this.getElement().setAttribute("print-ranges", s, this.getTABLE());
    }

    public void removePrintRanges() {
        this.getElement().removeAttribute("print-ranges", this.getTABLE());
    }

    @SuppressWarnings("unchecked")
    public synchronized void duplicateFirstRows(int nbFirstRows, int nbDuplicate) {
        final List<Element> children = this.getElement().getChildren("table-row", getTABLE());
        // clone xml elements and add them to our tree
        final List<Element> clones = new ArrayList<Element>(nbFirstRows * nbDuplicate);
        for (int i = 0; i < nbDuplicate; i++) {
            for (int l = 0; l < nbFirstRows; l++) {
                // final Element r = (Element) this.rows.get(l);
                final Element r = this.rows.get(l).getElement();
                clones.add((Element) r.clone());
            }
        }
        children.addAll(nbFirstRows, clones);

        // synchronize our rows with our new tree
        this.rows.clear();
        for (final Element clone : children) {
            // have to cast otherwise javac complains !!!
            this.rows.add(new Row(this, clone));
        }
    }

    @SuppressWarnings("unchecked")
    public synchronized void insertDuplicatedRows(int rowDuplicated, int nbDuplicate) {
        final List<Element> children = this.getElement().getChildren("table-row", getTABLE());
        // clone xml elements and add them to our tree
        final List<Element> clones = new ArrayList<Element>(nbDuplicate);
        for (int i = 0; i < nbDuplicate; i++) {

            // final Element r = (Element) this.rows.get(l);
            final Element r = this.rows.get(rowDuplicated).getElement();
            clones.add((Element) r.clone());

        }
        children.addAll(rowDuplicated, clones);

        // synchronize our rows with our new tree
        this.rows.clear();
        for (final Element clone : children) {
            // have to cast otherwise javac complains !!!
            this.rows.add(new Row(this, clone));
        }
    }

    public final SpreadSheet getSpreadSheet() {
        return this.parent;
    }

    protected final Namespace getTABLE() {
        return this.getSpreadSheet().getNS().getTABLE();
    }

    public Point resolveHint(String ref) {
        if (isCellRef(ref)) {
            return resolve(ref);
        } else
            throw new IllegalArgumentException(ref + " is not a cell ref, if it's a named range, you must use it on a SpreadSheet.");
    }

    // *** set cell

    public boolean isCellValid(int x, int y) {
        if (x > this.getColumnCount())
            return false;
        else if (y > this.getRowCount())
            return false;
        else
            return this.getImmutableCellAt(x, y).isValid();
    }

    public MutableCell getCellAt(int x, int y) {
        return this.getRow(y).getMutableCellAt(x);
    }

    public MutableCell getCellAt(String ref) {
        final Point p = resolveHint(ref);
        return this.getCellAt(p.x, p.y);
    }

    /**
     * Sets the value at the specified coordinates.
     * 
     * @param val the new value, <code>null</code> will be treated as "".
     * @param x the column.
     * @param y the row.
     */
    public void setValueAt(Object val, int x, int y) {
        if (val == null)
            val = "";
        // ne pas casser les repeated pour rien
        if (!val.equals(this.getValueAt(x, y)))
            this.getCellAt(x, y).setValue(val);
    }

    // *** get cell

    protected final Cell getImmutableCellAt(int x, int y) {
        return this.getRow(y).getCellAt(x);
    }

    protected final Cell getImmutableCellAt(String ref) {
        final Point p = resolveHint(ref);
        return this.getImmutableCellAt(p.x, p.y);
    }

    /**
     * @param row la ligne (0 a lineCount-1)
     * @param column la colonnee (0 a colonneCount-1)
     * @return la valeur de la cellule spécifiée.
     */
    public Object getValueAt(int column, int row) {
        return this.getImmutableCellAt(column, row).getValue();
    }

    public Object getStyleAt(int column, int row) {
        return this.getImmutableCellAt(column, row).getStyle();
    }

    /**
     * Retourne la valeur de la cellule spécifiée.
     * 
     * @param ref une référence de la forme "A3".
     * @return la valeur de la cellule spécifiée.
     */
    public Object getValueAt(String ref) {
        return this.getImmutableCellAt(ref).getValue();
    }

    // *** get count

    private Row getRow(int index) {
        return this.rows.get(index);
    }

    private Column getCol(int i) {
        return this.cols.get(i);
    }

    public int getRowCount() {
        return this.rows.size();
    }

    public int getColumnCount() {
        return this.cols.size();
    }

    // *** set count

    /**
     * Assure that this sheet as at least newSize columns.
     * 
     * @param newSize the new column count.
     */
    public void setColumnCount(int newSize) {
        this.setColumnCount(newSize, -1);
    }

    public void ensureColumnCount(int newSize) {
        if (newSize > this.getColumnCount())
            this.setColumnCount(newSize);
    }

    /**
     * Changes the column count. If newSize is less than getColumnCount() extra cells will be choped
     * off. Otherwise empty cells will be created.
     * 
     * @param newSize the new column count.
     * @param colIndex the index of the column to be copied, -1 for empty column (ie default style).
     */
    @SuppressWarnings("unchecked")
    public void setColumnCount(int newSize, int colIndex) {
        final Element elemToClone = colIndex < 0 ? Column.createEmpty(getSpreadSheet().getNS()) : getCol(colIndex).getElement();
        final int toGrow = newSize - this.getColumnCount();
        if (toGrow < 0) {
            CollectionUtils.delete(this.cols, newSize);
        } else {
            // row & col are at the same level, so filter
            final List<Element> children = this.getElement().getChildren(elemToClone.getName(), elemToClone.getNamespace());
            for (int i = 0; i < toGrow; i++) {
                final Element newElem = (Element) elemToClone.clone();
                children.add(newElem);
                this.cols.add(new Column(newElem));
            }
        }
        for (final Row r : this.rows) {
            r.columnCountChanged();
        }
    }

    public void ensureRowCount(int newSize) {
        if (newSize > this.getRowCount())
            this.setRowCount(newSize);
    }

    public void setRowCount(int newSize) {
        this.setRowCount(newSize, -1);
    }

    @SuppressWarnings("unchecked")
    public void setRowCount(int newSize, int rowIndex) {
        final Element elemToClone;
        if (rowIndex < 0) {
            elemToClone = Row.createEmpty(this.getSpreadSheet().getNS());
            // each row MUST have the same number of columns
            elemToClone.addContent(Cell.createEmpty(this.getSpreadSheet().getNS(), this.getColumnCount()));
        } else
            elemToClone = getRow(rowIndex).getElement();
        final int toGrow = newSize - this.getRowCount();
        if (toGrow < 0) {
            CollectionUtils.delete(this.rows, newSize);
        } else {
            // row & col are at the same level, so filter
            final List<Element> children = this.getElement().getChildren(elemToClone.getName(), elemToClone.getNamespace());
            for (int i = 0; i < toGrow; i++) {
                final Element newElem = (Element) elemToClone.clone();
                children.add(newElem);
                this.rows.add(new Row(this, newElem));
            }
        }
    }

    // *** table models

    public TableModel getTableModel(final int column, final int row) {
        return new SheetTableModel(row, column);
    }

    public TableModel getTableModel(final int column, final int row, int lastCol, int lastRow) {
        return new SheetTableModel(row, column, lastRow, lastCol);
    }

    public TableModel getMutableTableModel(final int column, final int row) {
        return new MutableTableModel(row, column);
    }

    public void merge(TableModel t, final int column, final int row) {
        this.merge(t, column, row, false);
    }

    /**
     * Merges t into this sheet at the specified point.
     * 
     * @param t the data to be merged.
     * @param column the columnn t will be merged at.
     * @param row the row t will be merged at.
     * @param includeColNames if <code>true</code> the column names of t will also be merged.
     */
    public void merge(TableModel t, final int column, final int row, final boolean includeColNames) {
        final int offset = (includeColNames ? 1 : 0);
        // the columns must be first, see section 8.1.1 of v1.1
        this.ensureColumnCount(column + t.getColumnCount());
        this.ensureRowCount(row + t.getRowCount() + offset);
        final TableModel thisModel = this.getMutableTableModel(column, row);
        if (includeColNames) {
            for (int x = 0; x < t.getColumnCount(); x++) {
                thisModel.setValueAt(t.getColumnName(x), 0, x);
            }
        }
        for (int y = 0; y < t.getRowCount(); y++) {
            for (int x = 0; x < t.getColumnCount(); x++) {
                final Object value = t.getValueAt(y, x);
                thisModel.setValueAt(value, y + offset, x);
            }
        }
    }

    private class SheetTableModel extends AbstractTableModel {
        protected final int row;
        protected final int column;
        protected final int lastRow;
        protected final int lastCol;

        private SheetTableModel(int row, int column) {
            this(row, column, Sheet.this.getRowCount(), Sheet.this.getColumnCount());
        }

        /**
         * Creates a new instance.
         * 
         * @param row the first row, inclusive.
         * @param column the first column, inclusive.
         * @param lastRow the last row, exclusive.
         * @param lastCol the last column, exclusive.
         */
        private SheetTableModel(int row, int column, int lastRow, int lastCol) {
            super();
            this.row = row;
            this.column = column;
            this.lastRow = lastRow;
            this.lastCol = lastCol;
        }

        public int getColumnCount() {
            return this.lastCol - this.column;
        }

        public int getRowCount() {
            return this.lastRow - this.row;
        }

        public Object getValueAt(int rowIndex, int columnIndex) {
            return Sheet.this.getValueAt(this.column + columnIndex, this.row + rowIndex);
        }
    }

    private final class MutableTableModel extends SheetTableModel {

        private MutableTableModel(int row, int column) {
            super(row, column);
        }

        public void setValueAt(Object obj, int rowIndex, int columnIndex) {
            Sheet.this.setValueAt(obj, this.column + columnIndex, this.row + rowIndex);
        }
    }

    // *** static

    private static final Pattern REF_PATTERN = Pattern.compile("([\\p{Alpha}]+)([\\p{Digit}]+)");

    static final boolean isCellRef(String ref) {
        return REF_PATTERN.matcher(ref).matches();
    }

    // "AA34" => (26,33)
    static final Point resolve(String ref) {
        final Matcher matcher = REF_PATTERN.matcher(ref);
        if (!matcher.matches())
            throw new IllegalArgumentException(ref + " illegal");
        final String letters = matcher.group(1);
        final String digits = matcher.group(2);
        return new Point(toInt(letters), Integer.parseInt(digits) - 1);
    }

    // "AA" => 26
    static final int toInt(String col) {
        if (col.length() < 1)
            throw new IllegalArgumentException("x cannot be empty");
        col = col.toUpperCase();

        int x = 0;
        for (int i = 0; i < col.length(); i++) {
            x = x * 26 + (col.charAt(i) - 'A' + 1);
        }

        // zero based
        return x - 1;
    }

}