/*********************************************************************
 *
 *      Copyright (C) 2002 Andrew Khan
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ***************************************************************************/

package org.xbib.datastructures.xslx.jxl.write.biff;

import org.xbib.datastructures.xslx.jxl.biff.BuiltInName;
import org.xbib.datastructures.xslx.jxl.biff.IntegerHelper;
import org.xbib.datastructures.xslx.jxl.biff.StringHelper;
import org.xbib.datastructures.xslx.jxl.biff.Type;
import org.xbib.datastructures.xslx.jxl.biff.WritableRecordData;
import org.xbib.datastructures.xslx.jxl.common.Logger;

/**
 * A name record.  Simply takes the binary data from the name
 * record read in
 */
class NameRecord extends WritableRecordData {
    // Constants which refer to the parse tokens after the string
    private static final int cellReference = 0x3a;
    private static final int areaReference = 0x3b;
    private static final int subExpression = 0x29;
    private static final int union = 0x10;
    // An empty range
    private static final NameRange EMPTY_RANGE = new NameRange(0, 0, 0, 0, 0);
    // The logger
    private static final Logger logger = Logger.getLogger(NameRecord.class);
    /**
     * The binary data for output to file
     */
    private byte[] data;
    /**
     * The name
     */
    private String name;
    /**
     * The built in name
     */
    private BuiltInName builtInName;
    /**
     * The index into the name table
     */
    private final int index;
    /**
     * The 0-based index sheet reference for a record name
     * 0 is for a global reference
     */
    private int sheetRef = 0;
    /**
     * Modified flag
     */
    private boolean modified;
    /**
     * The ranges covered by this name
     */
    private NameRange[] ranges;

    /**
     * Constructor - used when copying sheets
     *
     * @param index the index into the name table
     */
    public NameRecord(org.xbib.datastructures.xslx.jxl.read.biff.NameRecord sr, int ind) {
        super(Type.NAME);

        data = sr.getData();
        name = sr.getName();
        sheetRef = sr.getSheetRef();
        index = ind;
        modified = false;

        // Copy the ranges
        org.xbib.datastructures.xslx.jxl.read.biff.NameRecord.NameRange[] r = sr.getRanges();
        ranges = new NameRange[r.length];
        for (int i = 0; i < ranges.length; i++) {
            ranges[i] = new NameRange(r[i]);
        }
    }

    /**
     * Create a new name record with the given information.
     *
     * @param theName     Name to be created.
     * @param theIndex    Index of this name.
     * @param extSheet    External sheet index this name refers to.
     * @param theStartRow First row this name refers to.
     * @param theEndRow   Last row this name refers to.
     * @param theStartCol First column this name refers to.
     * @param theEndCol   Last column this name refers to.
     * @param global      TRUE if this is a global name
     */
    NameRecord(String theName,
               int theIndex,
               int extSheet,
               int theStartRow,
               int theEndRow,
               int theStartCol,
               int theEndCol,
               boolean global) {
        super(Type.NAME);

        name = theName;
        index = theIndex;
        sheetRef = global ? 0 : index + 1; // 0 indicates a global name, otherwise
        // the 1-based index of the sheet

        ranges = new NameRange[1];
        ranges[0] = new NameRange(extSheet,
                theStartRow,
                theEndRow,
                theStartCol,
                theEndCol);
        modified = true;
    }

    /**
     * Create a new name record with the given information.
     *
     * @param theName     Name to be created.
     * @param theIndex    Index of this name.
     * @param extSheet    External sheet index this name refers to.
     * @param theStartRow First row this name refers to.
     * @param theEndRow   Last row this name refers to.
     * @param theStartCol First column this name refers to.
     * @param theEndCol   Last column this name refers to.
     * @param global      TRUE if this is a global name
     */
    NameRecord(BuiltInName theName,
               int theIndex,
               int extSheet,
               int theStartRow,
               int theEndRow,
               int theStartCol,
               int theEndCol,
               boolean global) {
        super(Type.NAME);

        builtInName = theName;
        index = theIndex;
        sheetRef = global ? 0 : index + 1; // 0 indicates a global name, otherwise
        // the 1-based index of the sheet

        ranges = new NameRange[1];
        ranges[0] = new NameRange(extSheet,
                theStartRow,
                theEndRow,
                theStartCol,
                theEndCol);
    }

    /**
     * Create a new name record with the given information for 2-range entities.
     *
     * @param theName      Name to be created.
     * @param theIndex     Index of this name.
     * @param extSheet     External sheet index this name refers to.
     * @param theStartRow  First row this name refers to.
     * @param theEndRow    Last row this name refers to.
     * @param theStartCol  First column this name refers to.
     * @param theEndCol    Last column this name refers to.
     * @param theStartRow2 First row this name refers to (2nd instance).
     * @param theEndRow2   Last row this name refers to (2nd instance).
     * @param theStartCol2 First column this name refers to (2nd instance).
     * @param theEndCol2   Last column this name refers to (2nd instance).
     * @param global       TRUE if this is a global name
     */
    NameRecord(BuiltInName theName,
               int theIndex,
               int extSheet,
               int theStartRow,
               int theEndRow,
               int theStartCol,
               int theEndCol,
               int theStartRow2,
               int theEndRow2,
               int theStartCol2,
               int theEndCol2,
               boolean global) {
        super(Type.NAME);

        builtInName = theName;
        index = theIndex;
        sheetRef = global ? 0 : index + 1; // 0 indicates a global name, otherwise
        // the 1-based index of the sheet

        ranges = new NameRange[2];
        ranges[0] = new NameRange(extSheet,
                theStartRow,
                theEndRow,
                theStartCol,
                theEndCol);
        ranges[1] = new NameRange(extSheet,
                theStartRow2,
                theEndRow2,
                theStartCol2,
                theEndCol2);
    }

    /**
     * Gets the binary data for output to file
     *
     * @return the binary data
     */
    public byte[] getData() {
        if (data != null && !modified) {
            // this is a copy
            return data;
        }

        final int NAME_HEADER_LENGTH = 15;
        final byte AREA_RANGE_LENGTH = 11;
        final byte AREA_REFERENCE = 0x3b;

        int detailLength;

        if (ranges.length > 1) {
            detailLength = (ranges.length * AREA_RANGE_LENGTH) + 4;
        } else {
            detailLength = AREA_RANGE_LENGTH;
        }

        int length = NAME_HEADER_LENGTH + detailLength;
        length += builtInName != null ? 1 : name.length();
        data = new byte[length];

        // Options
        int options = 0;

        if (builtInName != null) {
            options |= 0x20;
        }
        IntegerHelper.getTwoBytes(options, data, 0);

        // Keyboard shortcut
        data[2] = 0;

        // Length of the name in chars
        if (builtInName != null) {
            data[3] = (byte) 0x1;
        } else {
            data[3] = (byte) name.length();
        }

        // Size of the definitions
        IntegerHelper.getTwoBytes(detailLength, data, 4);

        // Sheet index
        IntegerHelper.getTwoBytes(sheetRef, data, 6);
        IntegerHelper.getTwoBytes(sheetRef, data, 8);

        // Byte 10-13 are optional lengths [0,0,0,0]
        // Byte 14 is length of name which is not used.

        // The name
        if (builtInName != null) {
            data[15] = (byte) builtInName.getValue();
        } else {
            StringHelper.getBytes(name, data, 15);
        }

        // The actual range definition.
        int pos = builtInName != null ? 16 : name.length() + 15;

        // If there are multiple ranges for the name, we must specify a
        // subExpression type rather than areaReference and then put out
        // multiple areaReference entries with an end byte.
        if (ranges.length > 1) {
            data[pos++] = subExpression;
            // Length of remaining bytes excluding itself
            IntegerHelper.getTwoBytes(detailLength - 3, data, pos);
            pos += 2;
            byte[] rd;
            for (int i = 0; i < ranges.length; i++) {
                data[pos++] = areaReference;
                rd = ranges[i].getData();
                System.arraycopy(rd, 0, data, pos, rd.length);
                pos += rd.length;
            }
            data[pos] = 0x10;
        } else {
            // Range format - area
            data[pos] = areaReference;

            // The range data
            byte[] rd = ranges[0].getData();
            System.arraycopy(rd, 0, data, pos + 1, rd.length);
        }

        return data;
    }

    /**
     * Accessor for the name
     *
     * @return the name
     */
    public String getName() {
        return name;
    }

    /**
     * Accessor for the index of this name in the name table
     *
     * @return the index of this name in the name table
     */
    public int getIndex() {
        return index;
    }

    /**
     * The 0-based index sheet reference for a record name
     * 0 is for a global reference
     *
     * @return the sheet reference for name formula
     */
    public int getSheetRef() {
        return sheetRef;
    }

    /**
     * Set the index sheet reference for a record name
     * 0 is for a global reference
     */
    public void setSheetRef(int i) {
        sheetRef = i;
        IntegerHelper.getTwoBytes(sheetRef, data, 8);
    }

    /**
     * Gets the array of ranges for this name
     *
     * @return the ranges
     */
    public NameRange[] getRanges() {
        return ranges;
    }

    /**
     * Called when a row is inserted on the
     *
     * @param sheetIndex the sheet index on which the column was inserted
     * @param row        the column number which was inserted
     */
    void rowInserted(int sheetIndex, int row) {
        for (int i = 0; i < ranges.length; i++) {
            if (sheetIndex != ranges[i].getExternalSheet()) {
                continue; // shame on me - this is no better than a goto
            }

            if (row <= ranges[i].getFirstRow()) {
                ranges[i].incrementFirstRow();
                modified = true;
            }

            if (row <= ranges[i].getLastRow()) {
                ranges[i].incrementLastRow();
                modified = true;
            }
        }
    }

    /**
     * Called when a row is removed on the worksheet
     *
     * @param sheetIndex the sheet index on which the column was inserted
     * @param row        the column number which was inserted
     * @reeturn TRUE if the name is to be removed entirely, FALSE otherwise
     */
    boolean rowRemoved(int sheetIndex, int row) {
        for (int i = 0; i < ranges.length; i++) {
            if (sheetIndex != ranges[i].getExternalSheet()) {
                continue; // shame on me - this is no better than a goto
            }

            if (row == ranges[i].getFirstRow() && row == ranges[i].getLastRow()) {
                // remove the range
                ranges[i] = EMPTY_RANGE;
            }

            if (row < ranges[i].getFirstRow() && row > 0) {
                ranges[i].decrementFirstRow();
                modified = true;
            }

            if (row <= ranges[i].getLastRow()) {
                ranges[i].decrementLastRow();
                modified = true;
            }
        }

        // If all ranges are empty, then remove the name
        int emptyRanges = 0;
        for (int i = 0; i < ranges.length; i++) {
            if (ranges[i] == EMPTY_RANGE) {
                emptyRanges++;
            }
        }

        if (emptyRanges == ranges.length) {
            return true;
        }

        // otherwise just remove the empty ones
        NameRange[] newRanges = new NameRange[ranges.length - emptyRanges];
        for (int i = 0; i < ranges.length; i++) {
            if (ranges[i] != EMPTY_RANGE) {
                newRanges[i] = ranges[i];
            }
        }

        ranges = newRanges;

        return false;
    }

    /**
     * Called when a row is removed on the worksheet
     *
     * @param sheetIndex the sheet index on which the column was inserted
     * @param row        the column number which was inserted
     * @reeturn TRUE if the name is to be removed entirely, FALSE otherwise
     */
    boolean columnRemoved(int sheetIndex, int col) {
        for (int i = 0; i < ranges.length; i++) {
            if (sheetIndex != ranges[i].getExternalSheet()) {
                continue; // shame on me - this is no better than a goto
            }

            if (col == ranges[i].getFirstColumn() && col ==
                    ranges[i].getLastColumn()) {
                // remove the range
                ranges[i] = EMPTY_RANGE;
            }

            if (col < ranges[i].getFirstColumn() && col > 0) {
                ranges[i].decrementFirstColumn();
                modified = true;
            }

            if (col <= ranges[i].getLastColumn()) {
                ranges[i].decrementLastColumn();
                modified = true;
            }
        }

        // If all ranges are empty, then remove the name
        int emptyRanges = 0;
        for (int i = 0; i < ranges.length; i++) {
            if (ranges[i] == EMPTY_RANGE) {
                emptyRanges++;
            }
        }

        if (emptyRanges == ranges.length) {
            return true;
        }

        // otherwise just remove the empty ones
        NameRange[] newRanges = new NameRange[ranges.length - emptyRanges];
        for (int i = 0; i < ranges.length; i++) {
            if (ranges[i] != EMPTY_RANGE) {
                newRanges[i] = ranges[i];
            }
        }

        ranges = newRanges;

        return false;
    }

    /**
     * Called when a row is inserted on the
     *
     * @param sheetIndex the sheet index on which the column was inserted
     * @param col        the column number which was inserted
     */
    void columnInserted(int sheetIndex, int col) {
        for (int i = 0; i < ranges.length; i++) {
            if (sheetIndex != ranges[i].getExternalSheet()) {
                continue; // shame on me - this is no better than a goto
            }

            if (col <= ranges[i].getFirstColumn()) {
                ranges[i].incrementFirstColumn();
                modified = true;
            }

            if (col <= ranges[i].getLastColumn()) {
                ranges[i].incrementLastColumn();
                modified = true;
            }
        }
    }

    /**
     * A nested class to hold range information
     */
    static class NameRange {
        private int columnFirst;
        private int rowFirst;
        private int columnLast;
        private int rowLast;
        private final int externalSheet;

        NameRange(org.xbib.datastructures.xslx.jxl.read.biff.NameRecord.NameRange nr) {
            columnFirst = nr.getFirstColumn();
            rowFirst = nr.getFirstRow();
            columnLast = nr.getLastColumn();
            rowLast = nr.getLastRow();
            externalSheet = nr.getExternalSheet();
        }

        /**
         * Create a new range for the name record.
         */
        NameRange(int extSheet,
                  int theStartRow,
                  int theEndRow,
                  int theStartCol,
                  int theEndCol) {
            columnFirst = theStartCol;
            rowFirst = theStartRow;
            columnLast = theEndCol;
            rowLast = theEndRow;
            externalSheet = extSheet;
        }

        int getFirstColumn() {
            return columnFirst;
        }

        int getFirstRow() {
            return rowFirst;
        }

        int getLastColumn() {
            return columnLast;
        }

        int getLastRow() {
            return rowLast;
        }

        int getExternalSheet() {
            return externalSheet;
        }

        void incrementFirstRow() {
            rowFirst++;
        }

        void incrementLastRow() {
            rowLast++;
        }

        void decrementFirstRow() {
            rowFirst--;
        }

        void decrementLastRow() {
            rowLast--;
        }

        void incrementFirstColumn() {
            columnFirst++;
        }

        void incrementLastColumn() {
            columnLast++;
        }

        void decrementFirstColumn() {
            columnFirst--;
        }

        void decrementLastColumn() {
            columnLast--;
        }

        byte[] getData() {
            byte[] d = new byte[10];

            // Sheet index
            IntegerHelper.getTwoBytes(externalSheet, d, 0);

            // Starting row
            IntegerHelper.getTwoBytes(rowFirst, d, 2);

            // End row
            IntegerHelper.getTwoBytes(rowLast, d, 4);

            // Start column
            IntegerHelper.getTwoBytes(columnFirst & 0xff, d, 6);

            // End columns
            IntegerHelper.getTwoBytes(columnLast & 0xff, d, 8);

            return d;
        }
    }

}

