package com.customer.work.model;

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Reads an .xlsx workbook (created with Excel or LibreOffice) from the classpath
 * and returns all cells as Strings: book > sheets > rows > cells.
 * Restrictions per sheet:
 * - The column count is defined by the first row: it starts with the 1st cell
 *   and ends at the 1st empty cell
 * - Parsing stops at the 1st empty row
 * - Formulas in cells are evaluated
 */
@Component
public class FlowableExcelParser {

    public List<List<List<String>>> parseExcelBookFromResource(String excelResourcePath) {
        InputStream inputStream = getClass().getClassLoader().getResourceAsStream(excelResourcePath);
        if (inputStream == null) {
            throw new IllegalArgumentException("Resource not found: " + excelResourcePath);
        }
        try (inputStream; Workbook workBook = new XSSFWorkbook(inputStream)) {
            return parseBook(workBook);
        } catch (IOException e) {
            throw new UncheckedIOException("Cannot read Excel resource: " + excelResourcePath, e);
        }
    }

    protected List<List<List<String>>> parseBook(Workbook workBook) {
        List<List<List<String>>> book = new ArrayList<>();
        FormulaEvaluator formulaEvaluator = workBook.getCreationHelper().createFormulaEvaluator();
        for (Sheet workSheet : workBook) {
            book.add(parseSheet(workSheet, formulaEvaluator));
        }
        return book;
    }

    protected List<List<String>> parseSheet(Sheet workSheet, FormulaEvaluator formulaEvaluator) {
        List<List<String>> sheet = new ArrayList<>();
        int maxCols = getMaxCols(workSheet.getRow(0));
        if (maxCols == 0) {
            // Sheet without a header row is empty
            return sheet;
        }
        DataFormatter dataFormatter = new DataFormatter();
        int lastRow = workSheet.getLastRowNum();
        // for (Row workRow : workSheet) sometimes skips empty rows, then the first empty row as an exit criterion wouldn't work
        for (int rowIndex = 0; rowIndex <= lastRow; rowIndex++) {
            List<String> row = parseRow(workSheet.getRow(rowIndex), maxCols, formulaEvaluator, dataFormatter);
            if (row == null) {
                // Take all rows until the first empty row
                break;
            }
            sheet.add(row);
        }
        return sheet;
    }

    protected List<String> parseRow(Row workRow, int maxCols, FormulaEvaluator formulaEvaluator, DataFormatter dataFormatter) {
        if (workRow == null) {
            // Row is empty
            return null;
        }
        List<String> row = new ArrayList<>();
        boolean allCellsEmpty = true;
        for (int colIndex = 0; colIndex < maxCols; colIndex++) {
            Cell workCell = workRow.getCell(colIndex);
            String content = dataFormatter.formatCellValue(workCell, formulaEvaluator);
            if (content != null && !content.isEmpty()) {
                allCellsEmpty = false;
            }
            row.add(content);
        }
        if (allCellsEmpty) {
            // Row is empty
            return null;
        }
        return row;
    }

    protected int getMaxCols(Row workRow) {
        if (workRow == null) {
            return 0;
        }
        int maxCol = workRow.getLastCellNum();
        for (int colIndex = 0; colIndex < maxCol; colIndex++) {
            Cell workCell = workRow.getCell(colIndex);
            // Count cells until the first empty cell; cells holding an empty string
            // (formatting leftovers from Excel/LibreOffice) also count as empty
            if (workCell == null || workCell.getCellType() == CellType.BLANK || workCell.getCellType() == CellType._NONE
                    || (workCell.getCellType() == CellType.STRING && workCell.getStringCellValue().isEmpty())) {
                return colIndex;
            }
        }
        return Math.max(maxCol, 0);
    }

}
