/*
 *  Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
 *  <p>
 *  Licensed under the Apache License, Version 2.0.
 */
package com.easyagents.core.file2text.extractor.impl;

import com.easyagents.core.file2text.DocumentReadErrorCode;
import com.easyagents.core.file2text.DocumentReadException;
import com.easyagents.core.file2text.DocumentReadSupport;
import com.easyagents.core.file2text.LightweightDocumentReadRequest;
import com.easyagents.core.file2text.LightweightDocumentReadResult;
import com.easyagents.core.file2text.extractor.FileExtractor;
import com.easyagents.core.file2text.source.DocumentSource;
import org.apache.poi.hssf.eventusermodel.FormatTrackingHSSFListener;
import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
import org.apache.poi.hssf.eventusermodel.HSSFListener;
import org.apache.poi.hssf.eventusermodel.HSSFRequest;
import org.apache.poi.hssf.record.BOFRecord;
import org.apache.poi.hssf.record.BoolErrRecord;
import org.apache.poi.hssf.record.BoundSheetRecord;
import org.apache.poi.hssf.record.FormulaRecord;
import org.apache.poi.hssf.record.LabelRecord;
import org.apache.poi.hssf.record.LabelSSTRecord;
import org.apache.poi.hssf.record.NumberRecord;
import org.apache.poi.hssf.record.Record;
import org.apache.poi.hssf.record.SSTRecord;
import org.apache.poi.hssf.record.StringRecord;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.util.CellReference;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * 基于 HSSF Event API 的 XLS 文档提取器。
 */
public class XlsExtractor implements FileExtractor {

    private static final Set<String> MIME_TYPES = Set.of(
            "application/vnd.ms-excel",
            "application/msexcel",
            "application/x-msexcel");

    /**
     * 判断是否支持 XLS。
     *
     * @param source 文档来源
     * @return 是否支持
     */
    @Override
    public boolean supports(DocumentSource source) {
        if (source.getMimeType() != null && MIME_TYPES.contains(source.getMimeType().toLowerCase(Locale.ROOT))) {
            return true;
        }
        String name = source.getFileName();
        return name != null && (name.toLowerCase(Locale.ROOT).endsWith(".xls")
                || name.toLowerCase(Locale.ROOT).endsWith(".xlt"));
    }

    /**
     * 提取兼容纯文本。
     *
     * @param source 文档来源
     * @return 文本
     * @throws IOException 文档 I/O 失败
     */
    @Override
    public String extractText(DocumentSource source) throws IOException {
        return read(new LightweightDocumentReadRequest(source)).getText();
    }

    /**
     * 使用 HSSF 事件模型按工作表和行读取 XLS。
     *
     * @param request 读取请求
     * @return 表格结构化结果
     * @throws IOException 文档 I/O 失败
     */
    @Override
    public LightweightDocumentReadResult read(LightweightDocumentReadRequest request) throws IOException {
        DocumentSource source = request.getSource();
        try (InputStream input = source.openStream();
             POIFSFileSystem fileSystem = new POIFSFileSystem(input)) {
            XlsListener listener = new XlsListener(request);
            FormatTrackingHSSFListener formatter = new FormatTrackingHSSFListener(listener);
            listener.setFormatter(formatter);
            HSSFRequest hssfRequest = new HSSFRequest();
            hssfRequest.addListenerForAllRecords(formatter);
            new HSSFEventFactory().processWorkbookEvents(hssfRequest, fileSystem);
            listener.finish();
            return DocumentReadSupport.result(source,
                    SpreadsheetReadSupport.toSegments(listener.sheets()), request);
        } catch (StructureLimitRuntimeException error) {
            throw new DocumentReadException(DocumentReadErrorCode.DOCUMENT_STRUCTURE_LIMIT_EXCEEDED,
                    error.getMessage(), error);
        } catch (DocumentReadException error) {
            throw error;
        } catch (Exception error) {
            throw new DocumentReadException(DocumentReadErrorCode.DOCUMENT_CORRUPTED,
                    "Failed to extract XLS", error);
        }
    }

    /**
     * 获取读取器优先级。
     *
     * @return 优先级
     */
    @Override
    public int getOrder() {
        return 10;
    }

    /**
     * HSSF 二进制记录监听器。
     */
    private static final class XlsListener implements HSSFListener {

        private final LightweightDocumentReadRequest request;
        private final List<BoundSheetRecord> boundSheets = new ArrayList<>();
        private final List<SpreadsheetReadSupport.SheetRows> sheets = new ArrayList<>();
        private final List<SpreadsheetReadSupport.RowText> currentRows = new ArrayList<>();
        private final Map<Integer, String> currentCells = new LinkedHashMap<>();
        private FormatTrackingHSSFListener formatter;
        private SSTRecord sharedStrings;
        private int sheetIndex;
        private int currentRow = -1;
        private int nonEmptyCells;
        private int pendingFormulaRow = -1;
        private int pendingFormulaColumn = -1;

        private XlsListener(LightweightDocumentReadRequest request) {
            this.request = request;
        }

        private void setFormatter(FormatTrackingHSSFListener formatter) {
            this.formatter = formatter;
        }

        /**
         * 处理一个 HSSF 记录。
         *
         * @param record 工作簿记录
         */
        @Override
        public void processRecord(Record record) {
            checkCancelled();
            if (record instanceof BoundSheetRecord boundSheet) {
                boundSheets.add(boundSheet);
            } else if (record instanceof SSTRecord sstRecord) {
                sharedStrings = sstRecord;
            } else if (record instanceof BOFRecord bofRecord
                    && bofRecord.getType() == BOFRecord.TYPE_WORKSHEET) {
                startSheet();
            } else if (record instanceof LabelSSTRecord label) {
                String value = sharedStrings == null ? "" : sharedStrings.getString(label.getSSTIndex()).toString();
                putCell(label.getRow(), label.getColumn(), value);
            } else if (record instanceof LabelRecord label) {
                putCell(label.getRow(), label.getColumn(), label.getValue());
            } else if (record instanceof NumberRecord number) {
                putCell(number.getRow(), number.getColumn(), formatter.formatNumberDateCell(number));
            } else if (record instanceof FormulaRecord formula) {
                if (formula.hasCachedResultString()) {
                    pendingFormulaRow = formula.getRow();
                    pendingFormulaColumn = formula.getColumn();
                } else {
                    putCell(formula.getRow(), formula.getColumn(), formatter.formatNumberDateCell(formula));
                }
            } else if (record instanceof StringRecord string && pendingFormulaRow >= 0) {
                putCell(pendingFormulaRow, pendingFormulaColumn, string.getString());
                pendingFormulaRow = -1;
                pendingFormulaColumn = -1;
            } else if (record instanceof BoolErrRecord boolError && boolError.isBoolean()) {
                putCell(boolError.getRow(), boolError.getColumn(),
                        Boolean.toString(boolError.getBooleanValue()));
            }
        }

        private void startSheet() {
            flushSheet();
            sheetIndex++;
            if (sheetIndex > request.getMaxSheets()) {
                throw new StructureLimitRuntimeException(
                        "Sheet count exceeds " + request.getMaxSheets());
            }
        }

        private void putCell(int row, int column, String value) {
            if (value == null || value.isBlank()) {
                return;
            }
            if (currentRow >= 0 && row != currentRow) {
                flushRow();
            }
            currentRow = row;
            nonEmptyCells++;
            if (nonEmptyCells > request.getMaxNonEmptyCells()) {
                throw new StructureLimitRuntimeException(
                        "Non-empty cell count exceeds " + request.getMaxNonEmptyCells());
            }
            currentCells.put(column,
                    CellReference.convertNumToColString(column) + (row + 1) + "=" + value.trim());
        }

        private void flushRow() {
            if (!currentCells.isEmpty() && currentRow >= 0) {
                String text = String.join(" | ", currentCells.values());
                currentRows.add(new SpreadsheetReadSupport.RowText(currentRow + 1, text));
            }
            currentCells.clear();
            currentRow = -1;
        }

        private void flushSheet() {
            flushRow();
            if (sheetIndex <= 0) {
                return;
            }
            String name = sheetIndex <= boundSheets.size()
                    ? boundSheets.get(sheetIndex - 1).getSheetname()
                    : "Sheet " + sheetIndex;
            sheets.add(new SpreadsheetReadSupport.SheetRows(
                    sheetIndex, name, new ArrayList<>(currentRows)));
            currentRows.clear();
        }

        private void finish() {
            flushSheet();
        }

        private void checkCancelled() {
            request.checkCancelled();
        }

        private List<SpreadsheetReadSupport.SheetRows> sheets() {
            return sheets;
        }
    }

    /**
     * HSSF 回调中传递读取中止或结构上限异常。
     */
    private static final class StructureLimitRuntimeException extends RuntimeException {

        private StructureLimitRuntimeException(String message) {
            super(message);
        }
    }
}
