/*************************************************************************
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * Copyright 2000, 2010 Oracle and/or its affiliates.
 *
 * OpenOffice.org - a multi-platform office productivity suite
 *
 * This file is part of OpenOffice.org.
 *
 * OpenOffice.org is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * OpenOffice.org 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 version 3 for more details
 * (a copy is included in the LICENSE file that accompanied this code).
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with OpenOffice.org.  If not, see
 * <http://www.openoffice.org/license.html>
 * for a copy of the LGPLv3 License.
 *
 ************************************************************************/

package org.openoffice.xmerge.converter.xml.sxc.minicalc;

import jmc.Workbook;
import jmc.Worksheet;
import jmc.CellAttributes;
import jmc.CellDescriptor;
import jmc.JMCconstants;
import jmc.JMCException;

import java.awt.Color;

import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import org.openoffice.xmerge.converter.palm.Record;
import org.openoffice.xmerge.util.Debug;
import org.openoffice.xmerge.util.IntArrayList;

import org.openoffice.xmerge.converter.xml.sxc.SpreadsheetEncoder;
import org.openoffice.xmerge.converter.xml.sxc.Format;
import org.openoffice.xmerge.converter.xml.OfficeConstants;

/**
 *  This class is used by {@link
 *  org.openoffice.xmerge.converter.xml.sxc.SxcDocumentSerializerImpl
 * SxcDocumentSerializerImpl} to encode the MiniCalc format.
 *
 *  @author   Paul Rank
 */
final class MinicalcEncoder extends SpreadsheetEncoder {

    /** MiniCalc WorkBook to store sheets. */
    private Workbook wb;

    /** MiniCalc sheet - only one sheet can be open at a time. */
    private Worksheet ws;

    /**
     *  Estimate of the number of Palm pixels per character.  Used for
     *  estimating the width of a cell on a Palm device.
     */
    private final static int pixelsPerChar = 6;

    /**
     *  The minimum width (in pixels) that we allow a column to be set to
     *  on a Palm device.
     */
    private final static int minWidth = 10;

    /**
     *  The maximum width (in pixels) that we allow a column to be set to
     *  on a Palm device.
     */
    private final static int maxWidth = 80;


    /**
     *  Constructor creates a MiniCalc WorkBook.
     *
     *  @param  log       Log object for logging.
     *  @param  name      The name of the WorkBook.
     *  @param  password  The password for the WorkBook.
     *
     *  @throws  IOException  If any I/O error occurs.
     */
    MinicalcEncoder(String name, String password) throws IOException {

        super(name, password);

        try {
            wb = new Workbook(name, password);
        }
        catch (JMCException e) {
            Debug.log(Debug.ERROR, "new Workbook threw exception:" + e.getMessage());
            throw new IOException(e.getMessage());
        }
    }


    /**
     *  This method creates a WorkSheet belonging to the
     *  WorkBook.
     *
     *  @param  sheetName  The name of the WorkSheet.
     *
     *  @throws  IOException  If any I/O error occurs.
     */
    public void createWorksheet(String sheetName) throws IOException {

        try {
            ws = wb.createWorksheet(sheetName);
        }
        catch (JMCException e) {
            Debug.log(Debug.ERROR, "wb.createWorksheet threw exception:" + e.getMessage());
            throw new IOException(e.getMessage());
        }
    }


    /**
     *  This method gets the number of sheets in the WorkBook.
     *
     *  @return  The number of sheets in the WorkBook.
     */
    public int getNumberOfSheets() {

        int numSheets = wb.getNumberOfSheets();
        return numSheets;
    }


    /**
     *  This method encodes the MiniCalc WorkBook information
     *  into an palm <code>Record</code> array in MiniCalc
     *  database format.
     *
     *  @return  Array of <code>Record</code> holding MiniCalc
     *           contents.
     *
     *  @throws  IOException  If any I/O error occurs.
     */
    public Record[] getRecords(int sheetID) throws IOException {

        // Get the WorkSheet for the input sheetID
        ws = wb.getWorksheet(sheetID);

        // Need to call ws.initWrite() before we start querying the WorkSheet
        try {
            ws.initWrite();
        }
        catch (JMCException e) {
        Debug.log(Debug.ERROR, "ws.initWrite in getRecords:" + e.getMessage());
            throw new IOException(e.getMessage());
        }

        // Get the number of records in the WorkSheet
        int numRecords = ws.getNumberOfRecords();

        // Create the Record array
        Record[] allRecords = new Record[numRecords];


        // Get each record from the WorkSheet and store in allRecords[]
        try {
            for (int i = 0; i < allRecords.length; i++) {

                ByteArrayOutputStream bos = new ByteArrayOutputStream();

                int length = ws.writeNextRecord(bos);

                byte cBytes[] = bos.toByteArray();

                allRecords[i] = new Record(cBytes);
            }
        }
        catch (Exception e) {
            Debug.log(Debug.ERROR, "ws.writeNextRecord in getRecords:" + e.getMessage());
            throw new IOException(e.getMessage());
        }

        return allRecords;
    }


    /**
     *  A cell reference in a StarOffice formula looks like
     *  [.C2] (for cell C2).  MiniCalc is expecting cell references
     *  to look like C2.  This method strips out the braces and
     *  the period.
     *
     *  @param  formula  A StarOffice formula <code>String</code>.
     *
     *  @return  A MiniCalc formula <code>String</code>.
     */
    protected String parseFormula(String formula) {

        StringBuffer inFormula = new StringBuffer(formula);
        StringBuffer outFormula = new StringBuffer();

        boolean inBrace = false;
        boolean firstCharAfterBrace = false;
        boolean firstCharAfterColon = false;

        int len = inFormula.length();

        for (int in = 0; in < len; in++) {
            switch (inFormula.charAt(in)) {
            case '[':
                // We are now inside a StarOffice cell reference.
                // We also need to strip out the '['
                inBrace = true;

                // If the next character is a '.', we want to strip it out
                firstCharAfterBrace = true;
                break;

            case ']':
                // We are exiting a StarOffice cell reference
                // We are stripping out the ']'
                inBrace = false;
                break;

            case ':':
                // We have a cell range reference.
                // May need to strip out the leading '.'
                if (inBrace)
                    firstCharAfterColon = true;
                outFormula.append(inFormula.charAt(in));
                break;

            case '.':
                if (inBrace == true) {
                    if (firstCharAfterBrace == false &&
                            firstCharAfterColon == false) {
                        // Not the first character after the open brace.
                        // We have hit a separator between a sheet reference
                        // and a cell reference.  MiniCalc uses a ! as
                        // this type of separator.
                        outFormula.append('!');
                    }
                    else {
                        firstCharAfterBrace = false;
                        firstCharAfterColon = false;
                        // Since we are in a StarOffice cell reference,
                        // and we are the first character, we need to
                        // strip out the '.'
                    }
                    break;
                } else {
                    // We hit valid data, lets add it to the formula string
                    outFormula.append(inFormula.charAt(in));
                    break;
                }

            case ';':
                // StarOffice XML format uses ';' as a separator.  MiniCalc (and
                // many spreadsheets) use ',' as a separator instead.
                outFormula.append(',');
                break;

            default:
                // We hit valid data, lets add it to the formula string
                outFormula.append(inFormula.charAt(in));

                // Need to make sure that firstCharAfterBrace is not true.
                firstCharAfterBrace = false;
                break;
            }
        }

        return outFormula.toString();
    }

    /**
     *  Add a cell to the current WorkSheet.
     *
     *  @param   row             The row number of the cell.
     *  @param   column          The column number of the cell.
     *  @param   fmt             The <code>Format</code> object describing
     *                           the appearance of this cell.
     *  @param   cellContents    The text or formula of the cell's contents.
     *
     *  @throws  IOException  If any I/O error occurs.
     */
    public void addCell(int row, int column, Format fmt, String cellContents) throws IOException {

        CellAttributes ca = new CellAttributes(getFormat(fmt),
                                                fmt.getForeground(),
                                                fmt.getBackground());
           if (cellContents.startsWith("=")) {
                cellContents = parseFormula(cellContents);
                Debug.log(Debug.INFO, "YAHOO Found Formula" + cellContents);
        }

        CellDescriptor cellDes = new CellDescriptor(row, column, ca, cellContents);

        try {
            ws.putCell(cellDes);
        }
        catch (JMCException jmce) {
            Debug.log(Debug.ERROR, "ws.putCell threw exception: " + jmce.getMessage());
            throw new IOException(jmce.getMessage());
        }
    }


    /**
     *  Set the width of the columns in the WorkBook.
     *
     *  @param  columnWidths  An <code>IntArrayList</code> of column
     *                        widths.
     */
    public void setColumnWidths(IntArrayList columnWidths) throws IOException {
        // Get the number of columns
        int numColumns = columnWidths.size();

        // Return if there are no columns in the listr
        if (numColumns == 0) {
            return;
        }

        // Need to set the FORM_FLAGS_NONDEFAULT flag for the column widths
        // to be used in MiniCalc
        long format = JMCconstants.FORM_FLAGS_NONDEFAULT;

        CellAttributes ca = new CellAttributes(format);

        try {
            for (int i = 0; i < numColumns; i++) {
                // Get the column width in Palm pixels
                int width = columnWidths.get(i) * pixelsPerChar;

                // Check limits on column width
                if (width < minWidth) {
                    width = minWidth;
                } else if (width > maxWidth) {
                    width = maxWidth;
                }

                // Add the column descriptor to the WorkSheet
                ws.putColumn(i + 1, width, ca);
            }
        }
        catch (JMCException jmce) {
            Debug.log(Debug.ERROR, "ws.putColumn threw exception: " + jmce.getMessage());
            throw new IOException(jmce.getMessage());
        }
    }


    /**
     *  This method sets the format of a cell to <i>string</i>.
     *
     *  @param  format  The cell format-may already contain display info,
     *                  such as alignment or font type.
     *
     *  @return  The updated format of the cell.
     */
    private long setFormatString(long format) {

        format = clearCellFormatType(format);

        // Set format to generic, since MiniCalc does not have a string type.
        format = format | JMCconstants.FF_FORMAT_GENERIC;

        return format;
    }


    /**
     *  This method sets the format of a cell to <i>floating point</i>.
     *
     *  @param  format         The cell format.  May already contain
     *                         display info, such as alignment or
     *                         font type.
     *  @param  decimalPlaces  The number of decimal places to
     *                         set in the floating point number.
     *
     *  @return  The updated format of the cell.
     */
    private long setFormatFloat(long format, int decimalPlaces) {

        format = clearCellFormatType(format);

        // Set format to floating point with correct number of decimal places
        format = format | JMCconstants.FF_FORMAT_DECIMAL | decimalPlaces;

        return format;
    }


    /**
     *  This method sets the format of a cell to <i>time</i>.
     *
     *  @param  format  The cell format-may already contain display info,
     *                  such as alignment or font type.
     *
     *  @return  The updated format of the cell.
     */
    private long setFormatTime(long format) {

        format = clearCellFormatType(format);

        // Set format to time.
        format = format | JMCconstants.FF_FORMAT_TIME;

        return format;
    }


    /**
     *  This method sets the format of a cell to <i>date</i>.
     *
     *  @param  format  The cell format-may already contain display info,
     *                  such as alignment or font type.
     *
     *  @return  The updated format of the cell.
     */
    private long setFormatDate(long format) {

        format = clearCellFormatType(format);

        // Set format to date.
        format = format | JMCconstants.FF_FORMAT_DATE;

        return format;
    }


    /**
     *  This method sets the format of a cell to <i>currency</i>.
     *
     *  @param  format         The cell format-may already contain
     *                         display info, such as alignment or
     *                         font type.
     *  @param  decimalPlaces  The number of decimal places to set.
     *
     *  @return  The updated format of the cell.
     */
    private long setFormatCurrency(long format, int decimalPlaces) {

        format = clearCellFormatType(format);

        // Set format to Currency with correct number of decimal places
        format = format | JMCconstants.FF_FORMAT_CURRENCY | decimalPlaces;

        return format;
    }


    /**
     *  This method sets the format of a cell to <i>boolean</i>.
     *
     *  @param  format  The cell format-may already contain display info,
     *                  such as alignment or font type.
     *
     *  @return  The updated format of the cell.
     */
    private long setFormatBoolean(long format) {

        format = clearCellFormatType(format);

        // Set format to generic, since MiniCalc does not have a Boolean type.
        format = format | JMCconstants.FF_FORMAT_GENERIC;

        return format;
    }


    /**
     *  This method sets the format of a cell to <i>percent</i>.
     *
     *  @param  format         The cell format-may already contain
     *                         display info, such as alignment or
     *                         font type.
     *  @param  decimalPlaces  The number of decimal places to set.
     *
     *  @return  The updated format of the cell.
     */
    private long setFormatPercent(long format, int decimalPlaces) {

        format = clearCellFormatType(format);

        // Set format to Percent with correct number of decimal places
        format = format | JMCconstants.FF_FORMAT_PERCENT | decimalPlaces;

        return format;
    }


    /**
     *  This method clears out the format bits associated with
     *  the type of data (<i>float</i>, <i>time</i>, etc...) in
     *  a cell.
     *
     *  @param  format  The original format for the cell.
     *
     *  @return  The updated cell format with the bits associated
     *           with the type of data (float, time, etc...)
     *           zeroed out.
     */
    private long clearCellFormatType(long format) {

        // First 4 bits are for the number of decimal places
        // bits 5-8 are for the data format (float, time, etc...)

        // Clear out first 8 bits
        format = format & 0xFFFFFFFFFFFFFF00L;

        return format;
    }


    /**
     *  Set a cell's formatting options via a separately create
     *  <code>Format</code> object.
     *
     *  @param  row     The row number of the cell to be changed.
     *  @param  column  The column number of the cell to be changed.
     *  @param  fmt     Object containing formatting settings for
     *                 this cell.
     */
    public void setCellFormat(int row, int column, Format fmt) {
    }


    /**
     *  Get the names of the sheets in the WorkBook.
     *
     *  @param  sheet  The required sheet.
     */
    public String getSheetName(int sheet) {
        return wb.getWorksheet(sheet).getName();
    }


    /*
     *  This method returns a MiniCalc style format from the
     *  <code>Format</code> object.
     */
    private long getFormat(Format fmt)
    {
        String category = fmt.getCategory();

        if (category.equalsIgnoreCase(OfficeConstants.CELLTYPE_BOOLEAN)) {
            return setFormatBoolean(0);
        }
        else if (category.equalsIgnoreCase(OfficeConstants.CELLTYPE_CURRENCY)) {
            return setFormatCurrency(0, fmt.getDecimalPlaces());
        }
        else if (category.equalsIgnoreCase(OfficeConstants.CELLTYPE_DATE)) {
            return setFormatDate(0);
        }
        else if (category.equalsIgnoreCase(OfficeConstants.CELLTYPE_FLOAT)) {
            return setFormatFloat(0, fmt.getDecimalPlaces());
        }
        else if (category.equalsIgnoreCase(OfficeConstants.CELLTYPE_PERCENT)) {
            return setFormatPercent(0, fmt.getDecimalPlaces());
        }
        else if (category.equalsIgnoreCase(OfficeConstants.CELLTYPE_STRING)) {
            return setFormatString(0);
        }
        else if (category.equalsIgnoreCase(OfficeConstants.CELLTYPE_TIME)) {
            return setFormatTime(0);
        }
        else {
            // Should never get here, but just in case
            System.out.println("XXXXX Formatting information not found");
            return 0;
        }
    }
}

