/*
 * Copyright (c) 京北方信息技术股份有限公司 Corporation 2021 . All rights reserved.
 *
 */
package net.northking.cctp.common.db.excel;

import net.northking.cctp.common.db.BasicService;
import net.northking.cctp.common.db.Pagination;
import net.northking.cctp.common.exception.ErrorEnum;
import net.northking.cctp.common.exception.PlatformRuntimeException;
import net.northking.cctp.common.http.QueryByPage;
import net.northking.cctp.common.util.DateUtil;
import net.northking.cctp.common.util.EntityUtils;
import net.northking.cctp.common.util.UUIDUtil;
import org.apache.poi.hssf.usermodel.HSSFDateUtil;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;

import javax.mail.internet.MimeUtility;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

/**
 * 通用Excel服务
 *
 * @author : xiusong.xie
 * @version : 1.0
 */
public abstract class AbstractExcelService<T> implements ExcelService
{
  /**
   * 日志
   */
  private final static Logger logger = LoggerFactory.getLogger(AbstractExcelService.class);

  /**
   * 分批次获取数据，写入Excel，批次数量
   */
  private static final int BATCH_SIZE = 50;

  /**
   * Excel文件后缀名
   */
  private static final String EXCEL_POSTFIX = ".xlsx";
  /**
   * 默认 Excel Sheet 名称
   */
  private static final String SHEET_NAME = "CCTP-云测平台";

  /**
   * 指定导出Excel的表头
   *
   * @return Excel的表头
   */
  public abstract ExcelHeader[] excelHeaders();

  /**
   * 获取数据持久层服务
   *
   * @return 数据持久层服务
   */
  public abstract BasicService<T> getService();

  /**
   * 检查导入Excel文件的表头
   *
   * @param headerRow 表头
   */
  protected void checkHeaders(Row headerRow)
  {
    Iterator<Cell> cellIterator = headerRow.cellIterator();
    int cellNum = 0;
    while (cellIterator.hasNext())
    {
      ExcelHeader header = excelHeaders()[cellNum];
      if (header == null)
      {
        throw new PlatformRuntimeException(ErrorEnum.EXCEL_IMPORT);
      }
      Cell cell = cellIterator.next();
      String cellValue = cell.getStringCellValue();
      if (header.getName().equals(cellValue) || header.getTitle().equals(cellValue))
      {
        logger.debug("Excel导入,检查表头定义：第{}列 =>{}，与导入文件的表头相符。", cellNum + 1, header);
      } else
      {
        logger.error("Excel导入,检查表头定义：第{}列 =>{}，与导入文件的表头{} 不符。", cellNum + 1, header, cellValue);
        throw new PlatformRuntimeException(ErrorEnum.EXCEL_IMPORT);
      }
      cellNum++;
    }
  }

  public int importExcel(MultipartFile file)
  {
    File tempFile = this.createImportTempFile();
    try
    {
      file.transferTo(tempFile.getAbsoluteFile());
    } catch (IOException e)
    {
      logger.error("导入Excel异常", e);
      throw new PlatformRuntimeException(ErrorEnum.EXCEL_IMPORT);
    }
    int rowNum = 0; // 行号
    List<T> records = new ArrayList<>();
    try (
        XSSFWorkbook workbook = new XSSFWorkbook(tempFile)
    )
    {
      XSSFSheet sheet = workbook.getSheetAt(0);
      Iterator<Row> rowIterator = sheet.rowIterator();
      while (rowIterator.hasNext())
      {
        Row row = rowIterator.next();
        rowNum = row.getRowNum();
        if (rowNum == 0)
        {
          checkHeaders(row);
        } else
        {
          records.add(readExcelRow(row));
          if (records.size() >= BATCH_SIZE)
          {
            this.insertBatch(records);
          }
        }
      }
      if (records.size() > 0)
      {
        this.insertBatch(records);
      }
    } catch (Exception e)
    {
      throw new PlatformRuntimeException(ErrorEnum.EXCEL_IMPORT, e);
    }
    return rowNum;
  }

  private void insertBatch(List<T> records)
  {
    List<T> batch = this.beforeInsertBatch(records);
    getService().insertByBatch(batch);
    batch.clear();
    records.clear();
  }

  /**
   * Excel导入，入库之前的处理
   *
   * @param records 从Excel读取到的记录
   * @return 待入库的记录
   */
  protected abstract List<T> beforeInsertBatch(List<T> records);

  /**
   * 读取Excel某一行，转化为数据对象
   *
   * @param row 行
   * @return 数据对象
   */
  private T readExcelRow(Row row) throws ParseException
  {
    T data = getService().createEntity();
    Iterator<Cell> cellIterator = row.cellIterator();
    while (cellIterator.hasNext())
    {
      Cell cell = cellIterator.next();
      ExcelHeader header = excelHeaders()[cell.getColumnIndex()];
      try
      {
        readExcelCell(cell, header, data);
      } catch (Throwable e)
      {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("导入Excel文件失败。");
        stringBuilder.append("异常发生位置：第").append(row.getRowNum()).append("行，第").append(cell.getColumnIndex() + 1).append("列。");
        stringBuilder.append("表头信息：" + header.getTitle() + "(" + header.getName() + ")");
        logger.error(stringBuilder.toString());
        throw e;
      }
    }
    return data;
  }

  /**
   * 读取Excel某一个单元格，转化为数据对象的一个属性值
   *
   * @param cell   单元格
   * @param header 表头定义
   * @param data   数据对象
   */
  private void readExcelCell(Cell cell, ExcelHeader header, T data) throws ParseException
  {
    switch (cell.getCellTypeEnum())
    {
      case BLANK:
        break;
      case BOOLEAN:
        EntityUtils.setFieldValue(data, header.getName(), cell.getBooleanCellValue());
        break;
      case NUMERIC: // 单元格如果是日期，POI的类型是NUMERIC
        if (HSSFDateUtil.isCellDateFormatted(cell))
        {
          EntityUtils.setFieldValue(data, header.getName(), cell.getDateCellValue());
        } else
        {
          EntityUtils.setFieldValue(data, header.getName(), cell.getNumericCellValue());
        }
        break;
      case STRING:
        EntityUtils.setFieldValue(data, header.getName(), cell.getStringCellValue());
        break;
      default:
        throw new PlatformRuntimeException(ErrorEnum.EXCEL_IMPORT);
    }
  }

  /**
   * 将数据写入单元格
   *
   * @param cell   单元格
   * @param header 表头定义
   * @param data   数据
   */
  protected void writeExcelCell(Cell cell, ExcelHeader header, T data)
  {
    Object cellValue = EntityUtils.getFieldValue(data, header.getName());
    if (cellValue == null)
    {
      cell.setCellType(CellType.BLANK);
    } else
    {
      if (header.getType().isAssignableFrom(Boolean.class))
      {
        cell.setCellValue((Boolean) cellValue);
      } else if (header.getType().isAssignableFrom(CharSequence.class)
          || header.getType().isAssignableFrom(Number.class)
          || header.getType().isAssignableFrom(BigDecimal.class))
      {
        cell.setCellValue(cellValue.toString());
      } else if (header.getType().isAssignableFrom(Date.class))
      {
        cell.setCellValue(DateUtil.toYearMonthDay((Date) cellValue));
      } else
      {
        throw new PlatformRuntimeException(ErrorEnum.EXCEL_EXPORT);
      }
    }
  }

  /**
   * 编码文件名，中文的文件名需要特殊处理
   *
   * @param fileName 文件名
   * @return 编码后的文件名
   */
  private String encodeFileName(String fileName)
  {
    String postfix = DateUtil.toYearMonthDay(new Date());
    String downloadName = fileName + "." + postfix;
    try
    {
      downloadName = MimeUtility.encodeWord(downloadName);
    } catch (UnsupportedEncodingException e)
    {
      throw new PlatformRuntimeException(ErrorEnum.UNSUPPORTED_ENCODING, e);
    }
    return downloadName;
  }

  public File exportExcel(QueryByPage<?> queryByPage)
  {
    queryByPage.setPageSize(BATCH_SIZE);
    File temp = this.createExportTempFile();
    int rowNum = 0; // 行号
    int cellNum = 0; // 列号
    T data = null;
    try (
        SXSSFWorkbook workbook = new SXSSFWorkbook(createWorkbook(temp), BATCH_SIZE);
        BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(temp));
    )
    {
      SXSSFSheet sheet = workbook.getSheetAt(0);
      ExcelHeader[] headers = excelHeaders();
      SXSSFRow row; // 行对象
      SXSSFCell cell; // 单元格对象
      // create row
      row = sheet.createRow(rowNum++);
      // write header
      CellStyle headerStyle = workbook.createCellStyle();
      for (ExcelHeader header : headers)
      {
        cell = row.createCell(cellNum++);
        cell.setCellStyle(headerStyle);
        cell.setCellValue(header.getTitle());
      }
      // write data
      Pagination<T> page;
      do
      {
        page = getService().pagingQuery(queryByPage);
        for (T record : page.getRecords())
        {
          data = this.beforeWriteExcel(record);
          row = sheet.createRow(rowNum++);
          for (cellNum = 0; cellNum < headers.length; cellNum++)
          {
            cell = row.createCell(cellNum);
            this.writeExcelCell(cell, headers[cellNum], data);
          }
        }
        queryByPage.nextPage();
      } while (page.getPageNo() < page.getLastPageNo());

      workbook.write(outputStream);
      outputStream.flush();
      workbook.dispose();
    } catch (IOException e)
    {
      StringBuilder stringBuilder = new StringBuilder();
      stringBuilder.append("导出Excel文件失败。查询条件：").append(queryByPage.getQuery()).append("\r\n");
      stringBuilder.append("异常发生位置：第").append(rowNum + 1).append("行，第").append(cellNum + 1).append("列。");
      stringBuilder.append("该行数据=").append(data);
      logger.error(stringBuilder.toString());
      throw new PlatformRuntimeException(ErrorEnum.EXCEL_IMPORT, e);
    }
    return temp;
  }

  /**
   * 写入Excel之前的处理<br>
   * 数据的特殊处理在此处加工处理
   *
   * @param record 来自数据库的记录
   * @return 待写入Excel的记录
   */
  protected abstract T beforeWriteExcel(T record);


  private XSSFWorkbook createWorkbook(File file) throws IOException
  {
    try (
        FileOutputStream fos = new FileOutputStream(file);
        XSSFWorkbook workbook = new XSSFWorkbook()
    )
    {

      workbook.createSheet(SHEET_NAME);
      workbook.write(fos);
      return workbook;
    }
  }

  /**
   * 创建临时文件，例如： ./temp/{type}/2020/03/09/{UUID}.xlsx
   *
   * @return ./temp/{type}/2020/03/09/{UUID}.xlsx
   */
  private File createTempFile(String type)
  {
    String path = DateUtil.createTodayDir() + UUIDUtil.create32UUID() + EXCEL_POSTFIX;
    File file = new File("temp/" + type + path);
    file.getParentFile().mkdirs();
    return file;
  }

  /**
   * 创建导出的Excel临时文件，例如： ./temp/{type}/2020/03/09/{UUID}.xlsx
   *
   * @return 导出的Excel临时文件
   */
  private File createExportTempFile()
  {
    return createTempFile("exp");
  }

  /**
   * 创建导入的临时文件，例如： ./temp/{type}/2020/03/09/{UUID}.xlsx
   *
   * @return 导入的临时文件
   */
  private File createImportTempFile()
  {
    return createTempFile("imp");
  }

  @Override
  public void exportExcel(HttpServletResponse response, QueryByPage<?> queryByPage, String fileName)
  {
    File file = this.exportExcel(queryByPage);
    this.downloadExcel(response, fileName, file);
  }

  public void downloadTemplate(HttpServletResponse response, String fileName)
  {
    this.preDownloadExcel(response, fileName);
    try (
        OutputStream outputStream = response.getOutputStream();
        XSSFWorkbook workbook = new XSSFWorkbook();
    )
    {
      Sheet sheet = workbook.createSheet(SHEET_NAME);
      CellStyle cellStyle = workbook.createCellStyle();
      Row row = sheet.createRow(0);
      int idx = 0;
      for (ExcelHeader header : excelHeaders())
      {
        Cell cell = row.createCell(idx++);
        cell.setCellValue(header.getTitle());
        cell.setCellStyle(cellStyle);
      }
      workbook.write(outputStream);
    } catch (Exception e)
    {
      throw new PlatformRuntimeException(ErrorEnum.EXCEL_EXPORT, e);
    }
  }

  /**
   * 下载Excel
   *
   * @param response 应答
   * @param fileName 文件名
   * @param file     文件
   */
  private void downloadExcel(HttpServletResponse response, String fileName, File file)
  {
    this.preDownloadExcel(response, fileName);
    byte[] buff = new byte[1024];
    int len;
    try (
        OutputStream outputStream = response.getOutputStream();
        BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file))
    )
    {
      while ((len = inputStream.read(buff)) > 0)
      {
        outputStream.write(buff, 0, len);
        outputStream.flush();
      }

    } catch (IOException e)
    {
      throw new PlatformRuntimeException(ErrorEnum.EXCEL_EXPORT, e);
    }
  }

  private void preDownloadExcel(HttpServletResponse response, String fileName)
  {
    String downloadName = encodeFileName(fileName);
    response.reset();
    response.setStatus(HttpStatus.OK.value());
    response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attchement;filname=" + downloadName + ".xlsx");
  }
}
