package com.artfess.easyExcel.util.excel;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.artfess.base.annotation.Excel;
import com.artfess.easyExcel.annotaion.ExcelSelected;
import com.artfess.easyExcel.handler.SelectedSheetWriteHandler;
import com.artfess.easyExcel.util.limiter.SlidingWindow;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.function.UnaryOperator;

@Slf4j
public class XExcelUtil {
    public static final Integer EXCEL_SHEET_ROW_MAX_SIZE = 1000001; // excel sheet最大行数(算标题)
    private static final long DEF_PAGE_SIZE = 1000; // 默认页大小
    private static final int DEF_PARALLEL_NUM = Math.min(Runtime.getRuntime().availableProcessors(), 3);
    private HttpServletResponse httpServletResponse;
    private boolean parallel;
    private long pageSize = DEF_PAGE_SIZE;
    private int parallelNum = DEF_PARALLEL_NUM;
    private String fileName;

    private XExcelUtil() {
    }

    public static XExcelUtil download(HttpServletResponse response, String fileNamePrefix) throws UnsupportedEncodingException {
        XExcelUtil excelUtil = new XExcelUtil();
        excelUtil.httpServletResponse = response;
        SimpleDateFormat date_sdf = new SimpleDateFormat();
        String format = date_sdf.format(new Date());
        excelUtil.fileName = fileNamePrefix + "_" + format + ".xlsx";
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String downloadFileName = URLEncoder.encode(excelUtil.fileName, "UTF-8").replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + downloadFileName);
        return excelUtil;
    }

    public XExcelUtil parallel() {
        this.parallel = true;
        return this;
    }

    public XExcelUtil parallel(int parallelNum) {
        this.parallel = true;
        this.parallelNum = parallelNum;
        return this;
    }

    public XExcelUtil pageSize(int pageSize) {
        this.pageSize = pageSize;
        return this;
    }

    @SneakyThrows
    public <T> void pageExcelWriter(Class<T> head, UnaryOperator<IPage<T>> pageFunction) {
        if (parallel) {
            pageExcelWriterParallel(httpServletResponse.getOutputStream(), fileName, head, parallelNum, pageSize, pageFunction);
        } else {
            pageExcelWriter(httpServletResponse.getOutputStream(), fileName, head, pageSize, pageFunction);
        }
    }

    private static <T> void pageExcelWriter(OutputStream outputStream, String fileName, Class<T> head, long pageSize, UnaryOperator<IPage<T>> pageFunction) {
        long start = System.currentTimeMillis();
        Map<Integer, ExcelSelectedResolve> selectedMap = resolveSelectedAnnotation(head);
        log.debug("fileName:{}, excel writer start", fileName);
        try (ExcelWriter excelWriter = EasyExcel.write(outputStream, head).build()) {
            IPage<T> page = null;
            final WriteSheet writeSheet = EasyExcel.writerSheet(0, "Sheet0")
                    .registerWriteHandler(new SelectedSheetWriteHandler(selectedMap))
                    .registerWriteHandler(ExcelUtil.buildCellStyle())
                    .head(ExcelUtil.getExcelImportHead(head))
                    .build();
            do {
                long pageSearchStartTime = System.currentTimeMillis();
                page = pageFunction.apply(page == null ? new Page<>(1, pageSize) : new Page<>(page.getCurrent() + 1, page.getSize(), page.getTotal(), false)); // 分页查询
                long pageExcelWriteStartTime = System.currentTimeMillis();
                writeSheet.setSheetNo((int) (page.getCurrent() * page.getSize() / EXCEL_SHEET_ROW_MAX_SIZE));
                excelWriter.write(page.getRecords(), writeSheet); // excel写入数据
                log.debug("fileName:{}, total:{}, pageSize:{}, totalPage:{}, pageNo:{}, sheetNo:{}, pageSearchTime:{}ms, pageExcelWriterTime:{}ms", fileName, page.getTotal(), page.getSize(), page.getPages(), page.getCurrent(), writeSheet.getSheetNo(), pageExcelWriteStartTime - pageSearchStartTime, System.currentTimeMillis() - pageExcelWriteStartTime);
            } while (page.getCurrent() < page.getPages()); // 是否还有下一页
        }
        log.debug("fileName:{}, excel writer done, totalTime:{}ms", fileName, System.currentTimeMillis() - start);
    }

    private static <T> void pageExcelWriterParallel(OutputStream outputStream, String fileName, Class<T> head, int parallelNum, long pageSize, UnaryOperator<IPage<T>> pageFunction) throws ExecutionException, InterruptedException {
        long start = System.currentTimeMillis();
        Map<Integer, ExcelSelectedResolve> selectedMap = resolveSelectedAnnotation(head);
        log.debug("fileName:{}, excel writer start", fileName);
        try (ExcelWriter excelWriter = EasyExcel.write(outputStream, head).build()) {
            final WriteSheet writeSheet = EasyExcel.writerSheet(0, "Sheet0")
                    .registerWriteHandler(new SelectedSheetWriteHandler(selectedMap))
                    .registerWriteHandler(ExcelUtil.buildCellStyle())
                    .head(ExcelUtil.getExcelImportHead(head))
                    .build();
            IPage<T> basePage = pageFunction.apply(new Page<>(1, 0)).setSize(pageSize); // XXX 查总条数+总页数
            log.debug("fileName:{}, total:{}, pageSize:{}, totalPage:{}", fileName, basePage.getTotal(), basePage.getSize(), basePage.getPages());
            SlidingWindow.create(IPage.class, parallelNum, basePage.getPages()).sendWindow(pageNo -> {
                long pageSearchStartTime = System.currentTimeMillis();
                IPage<T> page = pageFunction.apply(new Page<>(pageNo, basePage.getSize(), basePage.getTotal(), false));
                log.debug("fileName:{}, [读]pageNo:{}, total:{}, pageSize:{}, totalPage:{}, pageSearchTime:{}ms", fileName, page.getCurrent(), page.getTotal(), page.getSize(), page.getPages(), (System.currentTimeMillis() - pageSearchStartTime));
                return page;
            }).receiveWindow(page -> {
                long pageWriteStartTime = System.currentTimeMillis();
                writeSheet.setSheetNo((int) (page.getCurrent() * page.getSize() / EXCEL_SHEET_ROW_MAX_SIZE));
                writeSheet.setSheetName("Sheet" + writeSheet.getSheetNo());
                excelWriter.write(page.getRecords(), writeSheet);
                log.debug("fileName:{}, [写]pageNo:{}, total:{}, pageSize:{}, totalPage:{}, sheetNo:{}, pageWriteTime:{}ms", fileName, page.getCurrent(), page.getTotal(), page.getSize(), page.getPages(), writeSheet.getSheetNo(), (System.currentTimeMillis() - pageWriteStartTime));
            }).start();
        }
        log.debug("fileName:{}, excel writer done, totalTime:{}ms", fileName, System.currentTimeMillis() - start);
    }

    /**
     * 解析表头类中的下拉注解
     *
     * @param head 表头类
     * @param <T>  泛型
     * @return Map<下拉框列索引, 下拉框内容> map
     */
    private static <T> Map<Integer, ExcelSelectedResolve> resolveSelectedAnnotation(Class<T> head) {
        Map<Integer, ExcelSelectedResolve> selectedMap = new HashMap<>();

        // getDeclaredFields(): 返回全部声明的属性；getFields(): 返回public类型的属性
        Field[] fields = head.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            // 解析注解信息
            ExcelSelected selected = field.getAnnotation(ExcelSelected.class);
            ExcelProperty property = field.getAnnotation(ExcelProperty.class);
            if (selected != null) {
                ExcelSelectedResolve excelSelectedResolve = new ExcelSelectedResolve();
                String[] source = excelSelectedResolve.resolveSelectedSource(selected);
                if (source != null && source.length > 0) {
                    excelSelectedResolve.setSource(source);
                    excelSelectedResolve.setFirstRow(selected.firstRow());
                    excelSelectedResolve.setLastRow(selected.lastRow());
                    if (property != null && property.index() >= 0) {
                        selectedMap.put(property.index(), excelSelectedResolve);
                    } else {
                        selectedMap.put(i, excelSelectedResolve);
                    }
                }
            }
        }

        return selectedMap;
    }

}
