package org.jboss.seam.excel.jxl;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.List;

import javax.faces.component.UIComponent;
import javax.imageio.ImageIO;

import jxl.CellView;
import jxl.Workbook;
import jxl.WorkbookSettings;
import jxl.write.WritableCell;
import jxl.write.WritableHyperlink;
import jxl.write.WritableImage;
import jxl.write.WritableSheet;
import jxl.write.WritableWorkbook;
import jxl.write.WriteException;

import org.jboss.seam.core.Interpolator;
import org.jboss.seam.document.DocumentData;
import org.jboss.seam.document.DocumentData.DocumentType;
import org.jboss.seam.excel.ExcelWorkbook;
import org.jboss.seam.excel.ExcelWorkbookException;
import org.jboss.seam.excel.WorksheetItem;
import org.jboss.seam.excel.ui.UICell;
import org.jboss.seam.excel.ui.UIColumn;
import org.jboss.seam.excel.ui.UIHyperlink;
import org.jboss.seam.excel.ui.UIImage;
import org.jboss.seam.excel.ui.UILink;
import org.jboss.seam.excel.ui.UIWorkbook;
import org.jboss.seam.excel.ui.UIWorksheet;
import org.jboss.seam.excel.ui.command.Command;
import org.jboss.seam.excel.ui.command.UIGroupColumns;
import org.jboss.seam.excel.ui.command.UIGroupRows;
import org.jboss.seam.excel.ui.command.UIMergeCells;
import org.jboss.seam.excel.ui.command.UIRowPageBreak;
import org.jboss.seam.log.Log;
import org.jboss.seam.log.Logging;

/**
 * Class that encapsulates the JExcelApi Workbook and Worksheet concepts and
 * internal state
 * 
 * @author Nicklas Karlsson (nickarls@gmail.com)
 * @author Daniel Roth (danielc.roth@gmail.com)
 */
public class JXLExcelWorkbook implements ExcelWorkbook {
    private static final int CELL_DEFAULT_HEIGHT = 17;
    private static final int CELL_DEFAULT_WIDTH = 64;

    private Log log = Logging.getLog(getClass());

    // The maximum number of columns allowed by the Excel specification
    private static final int MAX_COLUMNS = 255;

    // The maximum number of columns allowed by the Excel specification. This
    // will be worked around in future versions of this class by automatically
    // creating new sheets
    private static final int MAX_ROWS = 65535;

    // The default worksheet naming base
    private static final String DEFAULT_WORKSHEET_NAME = "Sheet{0}";

    // The temporary array of data which represents the binary worksheet. This
    // will be passed on to the DocumentStore
    private ByteArrayOutputStream byteStream = new ByteArrayOutputStream();

    // The JExcelAPI abstraction of a workbook. There will only be one per
    // instance of this class
    private WritableWorkbook workbook;

    // The JExcelAPI abstraction of a worksheet. This also represents the
    // current
    // worksheet begin worked on
    private WritableSheet worksheet;

    // The row index to start from. Used for placing a data block at another
    // location than the default top-left (0, 0)
    private int startRowIndex = 0;

    // The current index of the row being worked on (the row where the next cell
    // will be added)
    private int currentRowIndex = 0;

    // The column index to start from. Used for placing a data block at another
    // location than the default top-left (0, 0)
    private int startColumnIndex = 0;

    // The current index of the column being worked on (the column where the
    // next
    // cell will be added)
    private int currentColumnIndex = 0;

    /*
     * The current index of the worksheet being worked on. It's not that
     * important right now (we are moving forward linearly, but later when be
     * support worksheets with more that 65k rows we have to keep track on where
     * we are because starting with the next column could mean jumping back
     * several worksheets. For this we will also require some sort of low- and
     * high-indexes for the current worksheet when we add support for multiple
     * user-defined worksheets in the workbook.
     */
    private int currentWorksheetIndex = 0;

    /**
     * The maximum row index we have seen. Used for determining where to place
     * the worksheet footer (if any)
     */
    private int maxRowIndex;

    private JXLHelper jxlHelper = new JXLHelper();

    /**
     * Moves the row pointer to the next row. Used internally when adding data
     * 
     */
    private void nextRow() {
        if (log.isTraceEnabled()) {
            log.trace("Moving from row #0 to #1", currentRowIndex,
                    currentRowIndex + 1);
        }
        currentRowIndex++;
        if (currentRowIndex >= MAX_ROWS) {
            throw new ExcelWorkbookException(Interpolator.instance()
                    .interpolate("Excel only supports {0} rows", MAX_ROWS));
        }
    }

    /**
     * Moves the internal column pointer to the next column, called by the tag
     * to indicate that a new column has been started. If the pointer exceeds
     * the maximum allowed, throws an exception. Resets the styles and row
     * indexes etc.
     * 
     */
    public void nextColumn() {
        if (log.isTraceEnabled()) {
            log.trace("Moving from column #0 to #1", currentColumnIndex,
                    currentColumnIndex + 1);
        }
        currentColumnIndex++;
        if (currentColumnIndex > MAX_COLUMNS) {
            throw new ExcelWorkbookException(Interpolator.instance()
                    .interpolate("Excel doesn't support more than {0} columns",
                            MAX_COLUMNS));
        }
        if (currentRowIndex > maxRowIndex) {
            maxRowIndex = currentRowIndex;
        }
        currentRowIndex = startRowIndex;
    }

    /**
     * Checks if the workbook contains a sheet
     * 
     * @param name
     *            The name to look for
     * @return true if found, false otherwise
     */
    private boolean workbookContainsSheet(String name) {
        if (log.isTraceEnabled()) {
            log.trace("Checking if workbook contains sheet named #0", name);
        }
        if (workbook == null) {
            throw new ExcelWorkbookException(
                    "Can't search for sheets before creating a workbook");
        }
        boolean found = false;
        for (String sheetName : workbook.getSheetNames()) {
            if (sheetName.equalsIgnoreCase(name)) {
                return true;
            }
        }
        if (log.isTraceEnabled()) {
            log.trace("Result: #0", found);
        }
        return found;
    }

    /**
     * Creates a new worksheet (or selects one if it exists) in the workbook.
     * Will require a rework for auto-renaming when support for auto-adding of
     * new worksheets if there are more than 65k rows. Resets the internal state
     * (row- and column indexes, current styles etc)
     * 
     * @param uiWorksheet
     *            The worksheet to create or select in the workbook
     */
    public void createOrSelectWorksheet(UIWorksheet uiWorksheet) {
        if (workbook == null) {
            throw new ExcelWorkbookException(
                    "You cannot create a worksheet before creating a workbook");
        }
        if (log.isDebugEnabled()) {
            log
                    .debug(
                            "Creating worksheet named #0 starting at column #1 and row #2",
                            uiWorksheet.getName(),
                            uiWorksheet.getStartColumn(), uiWorksheet
                                    .getStartRow());
        }
        if (workbookContainsSheet(uiWorksheet.getName())) {
            if (log.isTraceEnabled()) {
                log.trace("Sheet found, selecting");
            }
            worksheet = workbook.getSheet(uiWorksheet.getName());
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Sheet not found, creating");
            }
            String name = uiWorksheet.getName() != null ? uiWorksheet.getName()
                    : Interpolator.instance().interpolate(
                            DEFAULT_WORKSHEET_NAME, currentWorksheetIndex + 1);
            worksheet = workbook.createSheet(name, currentWorksheetIndex);
        }

        jxlHelper.applyWorksheetSettings(worksheet, uiWorksheet);
        currentWorksheetIndex++;
        startColumnIndex = uiWorksheet.getStartColumn() == null ? 0
                : uiWorksheet.getStartColumn();
        currentColumnIndex = startColumnIndex;
        startRowIndex = uiWorksheet.getStartRow() == null ? 0 : uiWorksheet
                .getStartRow();
        currentRowIndex = startRowIndex;
        maxRowIndex = currentRowIndex;
    }

    /**
     * Creates and adds a data cell to the worksheet using the data cell format.
     * If the cell format is null, initializes the cell format. Finally moves
     * the internal pointer to the next row.
     * 
     * @param uiCell
     *            The cell to be created and added to the workbook
     * @param the
     *            type (header or data) of the cell
     */
    private void addCell(UICell uiCell) {
        if (log.isTraceEnabled()) {
            log.trace("Adding a cell with data #1 at column #2 and row #3",
                    uiCell.getValue(), currentColumnIndex, currentRowIndex);
        }
        if (worksheet == null) {
            throw new ExcelWorkbookException(
                    "Can't add cells before creating worksheet");
        }

        // Determine where to really place the cell
        int useRow = uiCell.getRow() != null ? uiCell.getRow()
                : currentRowIndex;
        int useColumn = uiCell.getColumn() != null ? uiCell.getColumn()
                : currentColumnIndex;

        CellInfo cellInfo = jxlHelper.getCellInfo(uiCell);
        WritableCell cell = JXLHelper.createCell(useColumn, useRow, cellInfo
                .getCellType(), uiCell.getValue(), cellInfo.getCellFormat());
        if (cellInfo.getCellFeatures() != null) {
            cell.setCellFeatures(cellInfo.getCellFeatures());
        }
        try {
            worksheet.addCell(cell);
        } catch (WriteException e) {
            throw new ExcelWorkbookException("Could not add cell", e);
        }
        // Only increase row if cell had no explicit placing
        if (uiCell.getColumn() == null && uiCell.getRow() == null) {
            nextRow();
        }
    }

    /**
     * Returns the binary data from the internal representation of the workbook
     * 
     * @return the data
     * @throws ExcelWorkbookException
     *             If there is a problem producing the binary data
     */
    public byte[] getBytes() {
        if (log.isTraceEnabled()) {
            log.trace("Returning bytes from workbook");
        }
        if (workbook == null) {
            throw new ExcelWorkbookException(
                    "You can't get workbook data before creating a workbook");
        }
        // You will get an IndexOutOfBoundException if trying to write a
        // workbook
        // without sheets,
        // creating a dummy. Could also throw an exception...
        if (workbook.getSheets().length == 0) {
            if (log.isTraceEnabled()) {
                log.trace("Creating dummy sheet");
            }
            workbook.createSheet("dummy", 0);
        }
        try {
            workbook.write();
            workbook.close();
        } catch (WriteException e) {
            throw new ExcelWorkbookException(
                    "There was an exception writing the workbook", e);
        } catch (IOException e) {
            throw new ExcelWorkbookException(
                    "There was an exception closing the workbook", e);
        }
        return byteStream.toByteArray();
    }

    /**
     * Intitializes a new workbook. Must be called first. Not that pretty but
     * the API has different constructors for all permutations of workbook
     * settings and template usage
     * 
     * @param uiWorkbook
     *            UIn Workbook to create
     * @throws ExcelWorkbookException
     *             if there were any errors creating the workbook
     */
    public void createWorkbook(UIWorkbook uiWorkbook) {
        String urlString = uiWorkbook.getTemplateURI();
        InputStream templateStream = null;
        if (urlString != null) {
            try {
                if (urlString.indexOf("://") < 0) {
                    templateStream = getClass().getResourceAsStream(urlString);
                } else {
                    templateStream = new URL(urlString).openStream();
                }
            } catch (Exception e) {
                throw new ExcelWorkbookException(
                        "Could not handle template URI", e);
            }
        }
        WorkbookSettings workbookSettings = null;
        if (uiWorkbook.hasSettings()) {
            workbookSettings = jxlHelper.createWorkbookSettings(uiWorkbook);
        }
        if (log.isDebugEnabled()) {
            log.debug("Creating workbook with creation type #0", uiWorkbook
                    .getCreationType());
        }
        // The joys of multiple constructors and no setters...
        try {
            switch (uiWorkbook.getCreationType()) {
            case WITH_SETTNGS_AND_TEMPLATE:
                workbook = Workbook.createWorkbook(byteStream, Workbook
                        .getWorkbook(templateStream), workbookSettings);
                break;
            case WITH_SETTINGS_WITHOUT_TEMPLATE:
                workbook = Workbook
                        .createWorkbook(byteStream, workbookSettings);
                break;
            case WITHOUT_SETTINGS_WITH_TEMPLATE:
                workbook = Workbook.createWorkbook(byteStream, Workbook
                        .getWorkbook(templateStream));
                break;
            case WITHOUT_SETTINGS_OR_TEMPLATE:
                workbook = Workbook.createWorkbook(byteStream);
                break;
            }
        } catch (Exception e) {
            throw new ExcelWorkbookException("Could not create workbook", e);
        }
        if (uiWorkbook.getWorkbookProtected() != null) {
            workbook.setProtected(uiWorkbook.getWorkbookProtected());
        }
        currentWorksheetIndex = workbook.getNumberOfSheets();
    }

    /**
     * Gets the document type of the data for the DocumentStore
     * 
     * @return the document type (Excel workbook)
     */
    public DocumentType getDocumentType() {
        return new DocumentData.DocumentType("xls", "application/vnd.ms-excel");
    }

    /**
     * Applies column settings for the current column
     * 
     * @param uiColumn
     *            the UI column to inspect for settings
     */
    public void applyColumnSettings(UIColumn uiColumn) {
        if (worksheet == null) {
            throw new ExcelWorkbookException(
                    "You can't set column settings before creating a worksheet");
        }
        jxlHelper.applyColumnSettings(uiColumn, worksheet, currentColumnIndex);
    }

    /**
     * Adds an image to the worksheet. First converts it to PNG since it's what
     * the library wants. If starting rows or columns are given, uses them,
     * otherwise uses the current indexes. If column- and rowspannings are
     * given, uses them, otherwise tries to determine them from the image
     * dimensions and default cell dimensions.
     * 
     * @param uiImage
     *            The image to add
     */
    private void addImage(UIImage uiImage) {
        if (worksheet == null) {
            throw new ExcelWorkbookException(
                    "Can't add an image before creating a worksheet");
        }

        BufferedImage image = null;
        ByteArrayOutputStream pngStream = null;
        try {
            image = ImageIO.read(new URI(uiImage.getURI()).toURL());
            pngStream = new ByteArrayOutputStream();
            ImageIO.write(image, "PNG", pngStream);
        } catch (Exception e) {
            throw new ExcelWorkbookException("Could not load or process image",
                    e);
        }

        int useStartColumn = uiImage.getStartColumn() == null ? currentColumnIndex
                : uiImage.getStartRow();
        int useStartRow = uiImage.getStartRow() == null ? currentRowIndex
                : uiImage.getStartRow();
        double estimatedRowSpan = (double) image.getHeight()
                / (double) CELL_DEFAULT_HEIGHT;
        double estimatedColSpan = (double) image.getWidth()
                / (double) CELL_DEFAULT_WIDTH;
        double useColumnSpan = uiImage.getColumnSpan() == null ? estimatedRowSpan
                : uiImage.getColumnSpan();
        double useRowSpan = uiImage.getRowSpan() == null ? estimatedColSpan
                : uiImage.getRowSpan();

        worksheet.addImage(new WritableImage(useStartColumn, useStartRow,
                useColumnSpan, useRowSpan, pngStream.toByteArray()));
    }

    /**
     * Creates a hyperlink to an URL in the worksheet
     * 
     * @param column
     *            The target column of the link (if null, defaults to current
     *            column)
     * @param row
     *            The target row of the link (if null, defaults to current row)
     * @param url
     *            The target URL
     */
    private void addHyperlink(UIHyperlink uiHyperlink) {
        if (worksheet == null) {
            throw new ExcelWorkbookException(
                    "Can't add a hyperlink before creating a worksheet");
        }

        int useStartColumn = uiHyperlink.getStartColumn() == null ? currentColumnIndex
                : uiHyperlink.getStartColumn();
        int useStartRow = uiHyperlink.getStartRow() == null ? currentRowIndex
                : uiHyperlink.getStartRow();
        int useEndColumn = uiHyperlink.getEndColumn() == null ? useStartColumn
                : uiHyperlink.getEndColumn();
        int useEndRow = uiHyperlink.getEndRow() == null ? useStartRow
                : uiHyperlink.getEndRow();
        String useDescription = uiHyperlink.getDescription() == null ? uiHyperlink
                .getURL()
                : uiHyperlink.getDescription();
        URL useURL = null;

        try {
            useURL = new URL(uiHyperlink.getURL());
        } catch (MalformedURLException e) {
            throw new ExcelWorkbookException("Bad url", e);
        }
        try {
            worksheet.addHyperlink(new WritableHyperlink(useStartColumn,
                    useStartRow, useEndColumn, useEndRow, useURL,
                    useDescription));
        } catch (Exception e) {
            throw new ExcelWorkbookException("Could not add hyperlink", e);
        }
    }

    /**
     * Adds an item (cell, image, hyperlink) to add to the worksheet
     * 
     * @param item
     *            The item to add
     */
    public void addItem(WorksheetItem item) {
        if (!((UIComponent) item).isRendered()) {
            return;
        }
        if (item.getValue() == null || "".equals(item.getValue())) {
            if (item.getColumn() == null && item.getRow() == null) {
                nextRow();
            }
            return;
        }
        switch (item.getItemType()) {
        case cell:
            addCell((UICell) item);
            break;
        case hyperlink:
            addHyperlink((UIHyperlink) item);
            break;
        case image:
            addImage((UIImage) item);
            break;
        default:
            throw new ExcelWorkbookException(Interpolator.instance()
                    .interpolate("Unknown item type {0}", item.getItemType()));
        }
    }

    /**
     * Executes a command for a worksheet
     * 
     * @param command
     *            The command to execute
     */
    public void executeCommand(Command command) {
        switch (command.getCommandType()) {
        case merge_cells:
            mergeCells((UIMergeCells) command);
            break;
        case group_columns:
            groupColumns((UIGroupColumns) command);
            break;
        case group_rows:
            groupRows((UIGroupRows) command);
            break;
        case add_row_pagebreak:
            addRowPageBreak((UIRowPageBreak) command);
            break;
        default:
            throw new ExcelWorkbookException(
                    Interpolator.instance().interpolate("Unknown command #0",
                            command.getCommandType()));
        }
    }

    /**
     * Adds a row page break to the worksheet
     * 
     * @param command
     *            the page break command to interpret
     */
    private void addRowPageBreak(UIRowPageBreak command) {
        if (log.isTraceEnabled()) {
            log.trace("Adding row page break #0", command);
        }
        if (worksheet == null) {
            throw new ExcelWorkbookException(
                    "Can't add row page breaks before creating a worksheet");
        }
        int useRow = command.getRow() != null ? command.getRow()
                : currentRowIndex;
        worksheet.addRowPageBreak(useRow);
    }

    /**
     * Groups worksheet rows
     * 
     * @param command
     *            The group command to interpret
     */
    private void groupRows(UIGroupRows command) {
        if (log.isTraceEnabled()) {
            log.trace("Grouping rows #0", command);
        }
        if (worksheet == null) {
            throw new ExcelWorkbookException(
                    "Can't group rows before creating a worksheet");
        }
        if (command.getStartRow() == null || command.getEndRow() == null) {
            throw new ExcelWorkbookException(
                    "Must define starting and ending rows when grouping rows");
        }
        boolean collapse = command.getCollapse() == null ? false : command
                .getCollapse();
        try {
            worksheet.setRowGroup(command.getStartRow(), command.getEndRow(),
                    collapse);
        } catch (Exception e) {
            throw new ExcelWorkbookException("Could not group columns", e);
        }
    }

    /**
     * Groups columns in the worksheet
     * 
     * @param command
     *            The group command to interpret
     */
    private void groupColumns(UIGroupColumns command) {
        if (log.isTraceEnabled()) {
            log.trace("Grouping columns #0", command);
        }
        if (worksheet == null) {
            throw new ExcelWorkbookException(
                    "Can't group columns before creating a worksheet");
        }
        if (command.getStartColumn() == null || command.getEndColumn() == null) {
            throw new ExcelWorkbookException(
                    "Must define starting and ending columns when grouping columns");
        }
        // JExcelAPI bug workaround
        for (int i = command.getStartColumn(); i <= command.getEndColumn(); i++) {
            worksheet.setColumnView(i, new CellView());
        }
        boolean collapse = command.getCollapse() == null ? false : command
                .getCollapse();
        try {
            worksheet.setColumnGroup(command.getStartColumn(), command
                    .getEndColumn(), collapse);
        } catch (Exception e) {
            throw new ExcelWorkbookException("Could not group columns", e);
        }
    }

    /**
     * Merge cells in the worksheet
     * 
     * @param command
     *            The merge command to interpret
     */
    private void mergeCells(UIMergeCells command) {
        if (log.isTraceEnabled()) {
            log.trace("Merging cells #0", command);
        }
        if (worksheet == null) {
            throw new ExcelWorkbookException(
                    "Can't merge cells before creating a worksheet");
        }
        if (command.getStartColumn() == null || command.getStartRow() == null
                || command.getEndColumn() == null
                || command.getEndRow() == null) {
            throw new ExcelWorkbookException(
                    "All start/end columns/rows must be set when merging cells");
        }
        try {
            worksheet
                    .mergeCells(command.getStartColumn(),
                            command.getStartRow(), command.getEndColumn(),
                            command.getEndRow());
        } catch (Exception e) {
            throw new ExcelWorkbookException("Couldn't merge cells", e);
        }
    }

    /**
     * Places an item in the worksheet footer
     * 
     * @param item
     *            The item to add
     * @param colspan
     *            The number of columns to span
     */
    public void addWorksheetFooter(WorksheetItem item, int colspan) {
        currentColumnIndex = startColumnIndex;
        currentRowIndex = maxRowIndex;
        UIMergeCells mergeCommand = new UIMergeCells();
        mergeCommand.setStartColumn(currentColumnIndex);
        mergeCommand.setStartRow(currentRowIndex);
        mergeCommand.setEndColumn(currentColumnIndex + colspan - 1);
        mergeCommand.setEndRow(currentRowIndex);
        executeCommand(mergeCommand);
        addItem(item);
    }

    /**
     * Places an item in the worksheet header
     * 
     * @param item
     *            The item to add
     * @param colspan
     *            The number of columns to span
     */
    public void addWorksheetHeader(WorksheetItem item, int colspan) {
        UIMergeCells mergeCommand = new UIMergeCells();
        mergeCommand.setStartColumn(currentColumnIndex);
        mergeCommand.setStartRow(currentRowIndex);
        mergeCommand.setEndColumn(currentColumnIndex + colspan - 1);
        mergeCommand.setEndRow(currentRowIndex);
        executeCommand(mergeCommand);
        addItem(item);
        startRowIndex++;
    }

    /**
     * Sets stylesheets for the workbook
     * 
     * @param stylesheets
     *            The stylesheet to register
     */
    public void setStylesheets(List<UILink> stylesheets) {
        try {
            jxlHelper.setStylesheets(stylesheets);
        } catch (Exception e) {
            throw new ExcelWorkbookException("Could not parse stylesheet", e);
        }
    }

}
