package taro.spreadsheet.model;

import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.Drawing;
import org.apache.poi.ss.usermodel.Picture;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;

import java.util.Map;

import static com.google.common.collect.Maps.newHashMap;
import static taro.spreadsheet.model.SpreadsheetCellStyle.DEFAULT;

public class SpreadsheetTab {

    private SpreadsheetWorkbook workbook;
    private XSSFSheet sheet;
    private Map<String, SpreadsheetCell> cells = newHashMap();
    private Drawing drawing;

    private int highestModifiedCol = -1;
    private int highestModifiedRow = -1;

    SpreadsheetTab(SpreadsheetWorkbook workbook, String title) {
        this.workbook = workbook;
        this.sheet = workbook.getPoiWorkbook().createSheet(title);
    }

    SpreadsheetTab(SpreadsheetWorkbook workbook, XSSFSheet sheet) {
        this.workbook = workbook;
        this.sheet = sheet;
    }

    public static String getCellAddress(int row, int col) {
        return CellReference.convertNumToColString(col) + (row+1);
    }

    public void setValue(String cellAddress, Object content) {
        setValue(cellAddress, content, null);
    }

    public void setValue(String cellAddress, Object content, SpreadsheetCellStyle style) {
        CellReference cellReference = new CellReference(cellAddress);
        setValue(cellReference.getRow(), cellReference.getCol(), content, style);
    }

    public void setValue(int row, int col, Object content) {
        setValue(row, col, content, null);
    }

    public void setValue(int row, int col, Object content, SpreadsheetCellStyle style) {
        SpreadsheetCell cell = getOrCreateCell(row, col);
        cell.setValue(content);
        if (style != null) {
            cell.setStyle(style);
        }
        recordCellModified(row, col);
    }

    public void setStyle(String cellAddress, SpreadsheetCellStyle style) {
        CellReference cellReference = new CellReference(cellAddress);
        setStyle(cellReference.getRow(), cellReference.getCol(), style);
    }

    public void setStyle(String firstCell, String lastCell, SpreadsheetCellStyle style) {
        CellReference firstReference = new CellReference(firstCell);
        CellReference lastReference = new CellReference(lastCell);
        setStyle(firstReference.getRow(), lastReference.getRow(), firstReference.getCol(), lastReference.getCol(), style);
    }

    public void setStyle(int row, int col, SpreadsheetCellStyle style) {
        getOrCreateCell(row, col).setStyle(style);
    }

    public void setStyle(int firstRow, int lastRow, int firstCol, int lastCol, SpreadsheetCellStyle style) {
        for (int row = firstRow; row <= lastRow; row++) {
            for (int col = firstCol; col <= lastCol; col++) {
                getOrCreateCell(row, col).setStyle(style);
            }
        }
    }

    public XSSFSheet getPoiSheet() {
        return sheet;
    }

    public SpreadsheetCell getCell(String cellAddress) {
        CellReference cellReference = new CellReference(cellAddress);
        return getCell(cellReference.getRow(), cellReference.getCol());
    }

    public SpreadsheetCell getCell(int row, int col) {
        String address = getCellAddress(row, col);
        SpreadsheetCell cell = cells.get(address);
        return cell;
    }

    public SpreadsheetCell getOrCreateCell(String cellAddress) {
        CellReference cellReference = new CellReference(cellAddress);
        return getOrCreateCell(cellReference.getRow(), cellReference.getCol());
    }

    public SpreadsheetCell getOrCreateCell(int row, int col) {
        SpreadsheetCell cell = getCell(row,col);
        if (cell == null) {
            cell = new SpreadsheetCell(this, getOrCreatePoiCell(row, col));
            String address = getCellAddress(row, col);
            cells.put(address, cell);
        }
        return cell;
    }

    private XSSFCell getOrCreatePoiCell(int rowNum, int col) {
        XSSFRow row = getOrCreatePoiRow(rowNum);
        XSSFCell cell = row.getCell(col);
        if (cell == null) {
            cell = row.createCell(col);
        }
        return cell;
    }

    private XSSFRow getOrCreatePoiRow(int rowNum) {
        XSSFRow row = sheet.getRow(rowNum);
        if (row == null) {
            row = sheet.createRow(rowNum);
        }
        return row;
    }

    public void mergeCells(String firstCell, String lastCell, Object content, SpreadsheetCellStyle style) {
        CellReference firstReference = new CellReference(firstCell);
        CellReference lastReference = new CellReference(lastCell);
        mergeCells(firstReference.getRow(), lastReference.getRow(), firstReference.getCol(), lastReference.getCol(), content, style);
    }

    public void mergeCells(int firstRow, int lastRow, int firstCol, int lastCol, Object content, SpreadsheetCellStyle style) {
        setValue(firstRow, firstCol, content);
        for (int col = firstCol; col <= lastCol; col++) {
            for (int row = firstRow; row <= lastRow; row++) {
                setStyle(row, col, style);
            }
        }
        sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol));
    }

    /**
     * In twips (1/20th of a point)
     */
    public int getRowHeight(int row) {
        return sheet.getRow(row).getHeight();
    }

    /**
     * In twips (1/20th of a point)
     */
    public void setRowHeight(int row, int twips) {
        sheet.getRow(row).setHeight((short)twips);
    }

    /**
     * In (1/256th of a character width)
     */
    public int getColWidth(int col) {
        return sheet.getColumnWidth(col);
    }

    /**
     * In (1/256th of a character width)
     */
    public void setColWidth(int col, int twips) {
        sheet.setColumnWidth(col, twips);
    }

    public void autosizeRowsAndCols() {
        autosizeCols();
        autosizeRows();
    }

    public void autosizeRows() {
        for (int row = 0; row <= highestModifiedRow; row++) {
            autoSizeRow(row);
        }
    }

    public void autosizeCols() {
        for (int col = 0; col <= highestModifiedCol; col++) {
            sheet.autoSizeColumn(col, true);
        }
    }

    public void autoSizeRow(int row) {
        float tallestCell = -1;
        for (int col = 0; col <= highestModifiedCol; col++) {
            SpreadsheetCell cell = getOrCreateCell(row, col);
            int fontSize = cell.getFontSizeInPoints();
            XSSFCell poiCell = cell.getPoiCell();
            if (poiCell.getCellType() == CellType.STRING) {
                String value = poiCell.getStringCellValue();
                int numLines = 1;
                for (int i = 0; i < value.length(); i++) {
                    if (value.charAt(i) == '\n') numLines++;
                }
                float cellHeight = computeRowHeightInPoints(fontSize, numLines);
                if (cellHeight > tallestCell) {
                    tallestCell = cellHeight;
                }
            }
        }

        float defaultRowHeightInPoints = sheet.getDefaultRowHeightInPoints();
        float rowHeight = tallestCell;
        if (rowHeight < defaultRowHeightInPoints+1) {
            rowHeight = -1;    // resets to the default
        }

        sheet.getRow(row).setHeightInPoints(rowHeight);
    }

    public float computeRowHeightInPoints(int fontSizeInPoints, int numLines) {
        // a crude approximation of what excel does
        float lineHeightInPoints = 1.3f * fontSizeInPoints;
        float rowHeightInPoints = lineHeightInPoints * numLines;
        rowHeightInPoints = Math.round(rowHeightInPoints * 4) / 4f;        // round to the nearest 0.25

        // Don't shrink rows to fit the font, only grow them
        float defaultRowHeightInPoints = sheet.getDefaultRowHeightInPoints();
        if (rowHeightInPoints < defaultRowHeightInPoints + 1) {
            rowHeightInPoints = defaultRowHeightInPoints;
        }
        return rowHeightInPoints;
    }

    public void addSpacer() {
        sheet.setColumnWidth(0, 768);
    }


    private void recordCellModified(int row, int col) {
        if (col > highestModifiedCol) {
            highestModifiedCol = col;
        }
        if (row > highestModifiedRow) {
            highestModifiedRow = row;
        }
    }

    public void printDown(String cellAddress, SpreadsheetCellStyle style, String... values) {
        CellReference cellReference = new CellReference(cellAddress);
        printDown(cellReference.getRow(), cellReference.getCol(), style, values);
    }

    public void printAcross(String cellAddress, SpreadsheetCellStyle style, String... values) {
        CellReference cellReference = new CellReference(cellAddress);
        printAcross(cellReference.getRow(), cellReference.getCol(), style, values);
    }

    /**
     * Returns the index of the next row after the last one written
     */
    public int printDown(int row, int col, SpreadsheetCellStyle style, Object... values) {
        for (int i = 0; i < values.length; i++) {
            setValue(row + i, col, values[i], style);
        }
        return row + values.length;
    }

    /**
     * Returns the index of the next col after the last one written.
     */
    public int printAcross(int row, int col, SpreadsheetCellStyle style, Object... values) {
        for (int i = 0; i < values.length; i++) {
            setValue(row, col + i, values[i], style);
        }
        return col + values.length;
    }

    public void setSurroundBorder(String firstCell, String lastCell, BorderStyle border) {
        CellReference firstReference = new CellReference(firstCell);
        CellReference lastReference = new CellReference(lastCell);
        setSurroundBorder(firstReference.getRow(), lastReference.getRow(), firstReference.getCol(), lastReference.getCol(), border);
    }

    public void setSurroundBorder(int firstRow, int lastRow, int firstCol, int lastCol, BorderStyle border) {
        setTopBorder(firstRow, firstCol, lastCol, border);
        setBottomBorder(lastRow, firstCol, lastCol, border);
        setLeftBorder(firstRow, lastRow, firstCol, border);
        setRightBorder(firstRow, lastRow, lastCol, border);
    }

    public void setRightBorder(int firstRow, int lastRow, int col, BorderStyle border) {
        for (int row = firstRow; row <= lastRow; row++) {
            getOrCreateCell(row, col).applyStyle(DEFAULT.withRightBorder(border));
        }
    }

    public void setLeftBorder(int firstRow, int lastRow, int col, BorderStyle border) {
        for (int row = firstRow; row <= lastRow; row++) {
            getOrCreateCell(row, col).applyStyle(DEFAULT.withLeftBorder(border));
        }
    }

    public void setTopBorder(int row, int firstCol, int lastCol, BorderStyle border) {
        for (int col = firstCol; col <= lastCol; col++) {
            getOrCreateCell(row, col).applyStyle(DEFAULT.withTopBorder(border));
        }
    }

    public void setBottomBorder(int row, int firstCol, int lastCol, BorderStyle border) {
        for (int col = firstCol; col <= lastCol; col++) {
            getOrCreateCell(row, col).applyStyle(DEFAULT.withBottomBorder(border));
        }
    }

    public CellStyle registerStyle(SpreadsheetCellStyle style) {
        return workbook.registerStyle(style);
    }

    public void addPicture(String cellAddress, byte[] bytes, int pictureType) {
        CellReference cellRef = new CellReference(cellAddress);
        addPicture(cellRef.getRow(), cellRef.getCol(), bytes, pictureType);
    }

    public void addPicture(int row, int col, byte[] bytes, int pictureType) {
        if (drawing == null) {
            drawing = sheet.createDrawingPatriarch();
        }

        int pictureIndex = workbook.getPoiWorkbook().addPicture(bytes, pictureType);
        //add a picture shape
        ClientAnchor anchor = workbook.getPoiWorkbook().getCreationHelper().createClientAnchor();
        //set top-left corner of the picture,
        //subsequent call of Picture#resize() will operate relative to it
        anchor.setCol1(col);
        anchor.setRow1(row);

        Picture pict = drawing.createPicture(anchor, pictureIndex);
        //auto-size picture relative to its top-left corner
        pict.resize();
    }

}