/*
 * Copyright (c) 2024 Hutool Team and hutool.cn
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.dromara.hutool.poi.excel.writer;

import org.apache.poi.common.usermodel.Hyperlink;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellReference;
import org.dromara.hutool.core.bean.BeanUtil;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.collection.ListUtil;
import org.dromara.hutool.core.map.MapUtil;
import org.dromara.hutool.core.map.multi.Table;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.poi.excel.RowGroup;
import org.dromara.hutool.poi.excel.RowUtil;
import org.dromara.hutool.poi.excel.cell.CellRangeUtil;
import org.dromara.hutool.poi.excel.cell.CellUtil;
import org.dromara.hutool.poi.excel.cell.editors.CellEditor;
import org.dromara.hutool.poi.excel.style.StyleSet;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Sheet数据写出器<br>
 * 此对象只封装将数据写出到Sheet中，并不刷新到文件
 *
 * @author looly
 * @since 6.0.0
 */
public class SheetDataWriter {

	private final Sheet sheet;
	private final ExcelWriteConfig config;
	private StyleSet styleSet;

	/**
	 * 标题项对应列号缓存，每次写标题更新此缓存<br>
	 * 此缓存用于用户多次write时，寻找标题位置
	 */
	private Map<String, Integer> headerLocationCache;
	/**
	 * 当前行，用于标记初始可写数据的行和部分写完后当前的行
	 */
	private final AtomicInteger currentRow;

	/**
	 * 构造
	 *
	 * @param sheet    {@link Sheet}
	 * @param config   Excel配置
	 * @param styleSet 样式表
	 */
	public SheetDataWriter(final Sheet sheet, final ExcelWriteConfig config, final StyleSet styleSet) {
		this.sheet = sheet;
		this.config = config;
		this.styleSet = styleSet;
		this.currentRow = new AtomicInteger(0);
	}

	/**
	 * 设置样式表
	 *
	 * @param styleSet 样式表
	 * @return this
	 */
	public SheetDataWriter setStyleSet(final StyleSet styleSet) {
		this.styleSet = styleSet;
		return this;
	}

	/**
	 * 设置标题位置映射缓存
	 *
	 * @param headerLocationCache 标题位置映射缓存，key为表明名，value为列号
	 * @return this
	 */
	public SheetDataWriter setHeaderLocationCache(final Map<String, Integer> headerLocationCache) {
		this.headerLocationCache = headerLocationCache;
		return this;
	}

	/**
	 * 写出分组标题行
	 *
	 * @param x        开始的列，下标从0开始
	 * @param y        开始的行，下标从0开始
	 * @param rowCount 当前分组行所占行数，此数值为标题占用行数+子分组占用的最大行数，不确定传1
	 * @param rowGroup 分组行
	 * @return this
	 */
	public SheetDataWriter writeHeader(int x, int y, int rowCount, final RowGroup rowGroup) {

		// 写主标题
		final String name = rowGroup.getName();
		final List<RowGroup> children = rowGroup.getChildren();
		if (null != name) {
			if(CollUtil.isNotEmpty(children)){
				// 有子节点，标题行只占用除子节点占用的行数
				rowCount = Math.max(1, rowCount - rowGroup.childrenMaxRowCount());
				//nameRowCount = 1;
			}

			// 如果无子节点，则标题行占用所有行
			final CellRangeAddress cellRangeAddresses = CellRangeUtil.of(y, y + rowCount - 1, x, x + rowGroup.maxColumnCount() - 1);
			CellStyle style = rowGroup.getStyle();
			if (null == style && null != this.styleSet) {
				style = styleSet.getStyleFor(new CellReference(cellRangeAddresses.getFirstRow(), cellRangeAddresses.getFirstColumn()), name, true);
			}
			CellUtil.mergingCells(this.sheet, cellRangeAddresses, style);
			final Cell cell = CellUtil.getOrCreateCell(this.sheet, cellRangeAddresses.getFirstColumn(), cellRangeAddresses.getFirstRow());
			if(null != cell){
				CellUtil.setCellValue(cell, name, style, this.config.getCellEditor());
			}

			// 子分组写到下N行
			y += rowCount;
		}

		// 写分组
		final int childrenMaxRowCount = rowGroup.childrenMaxRowCount();
		if(childrenMaxRowCount > 0){
			for (final RowGroup child : children) {
				// 子分组行高填充为当前分组最大值
				writeHeader(x, y, childrenMaxRowCount, child);
				x += child.maxColumnCount();
			}
		}

		return this;
	}

	/**
	 * 写出一行，根据rowBean数据类型不同，写出情况如下：
	 *
	 * <pre>
	 * 1、如果为Iterable，直接写出一行
	 * 2、如果为Map，isWriteKeyAsHead为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values
	 * 3、如果为Bean，转为Map写出，isWriteKeyAsHead为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values
	 * </pre>
	 *
	 * @param rowBean          写出的Bean
	 * @param isWriteKeyAsHead 为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values
	 * @return this
	 * @see #writeRow(Iterable)
	 * @see #writeRow(Map, boolean)
	 * @since 4.1.5
	 */
	@SuppressWarnings({"rawtypes", "unchecked"})
	public SheetDataWriter writeRow(final Object rowBean, final boolean isWriteKeyAsHead) {
		final ExcelWriteConfig config = this.config;

		final Map rowMap;
		if (rowBean instanceof Map) {
			if (MapUtil.isNotEmpty(config.getHeaderAlias())) {
				rowMap = MapUtil.newTreeMap((Map) rowBean, config.getCachedAliasComparator());
			} else {
				rowMap = (Map) rowBean;
			}
		} else if (rowBean instanceof Iterable) {
			// issue#2398@Github
			// MapWrapper由于实现了Iterable接口，应该优先按照Map处理
			return writeRow((Iterable<?>) rowBean);
		} else if (rowBean instanceof Hyperlink) {
			// Hyperlink当成一个值
			return writeRow(ListUtil.of(rowBean), isWriteKeyAsHead);
		} else if (BeanUtil.isReadableBean(rowBean.getClass())) {
			if (MapUtil.isEmpty(config.getHeaderAlias())) {
				rowMap = BeanUtil.beanToMap(rowBean, new LinkedHashMap<>(), false, false);
			} else {
				// 别名存在情况下按照别名的添加顺序排序Bean数据
				rowMap = BeanUtil.beanToMap(rowBean, new TreeMap<>(config.getCachedAliasComparator()), false, false);
			}
		} else {
			// 其它转为字符串默认输出
			return writeRow(ListUtil.of(rowBean), isWriteKeyAsHead);
		}
		return writeRow(rowMap, isWriteKeyAsHead);
	}

	/**
	 * 将一个Map写入到Excel，isWriteKeyAsHead为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values<br>
	 * 如果rowMap为空（包括null），则写出空行
	 *
	 * @param rowMap           写出的Map，为空（包括null），则写出空行
	 * @param isWriteKeyAsHead 为true写出两行，Map的keys做为一行，values做为第二行，否则只写出一行values
	 * @return this
	 */
	public SheetDataWriter writeRow(final Map<?, ?> rowMap, final boolean isWriteKeyAsHead) {
		if (MapUtil.isEmpty(rowMap)) {
			// 如果写出数据为null或空，跳过当前行
			passAndGet();
			return this;
		}

		final Table<?, ?, ?> aliasTable = this.config.aliasTable(rowMap);
		if (isWriteKeyAsHead) {
			// 写出标题行，并记录标题别名和列号的关系
			writeHeaderRow(aliasTable.columnKeys());
			// 记录原数据key和别名对应列号
			int i = 0;
			for (final Object key : aliasTable.rowKeySet()) {
				this.headerLocationCache.putIfAbsent(StrUtil.toString(key), i);
				i++;
			}
		}

		// 如果已经写出标题行，根据标题行找对应的值写入
		if (MapUtil.isNotEmpty(this.headerLocationCache)) {
			final Row row = RowUtil.getOrCreateRow(this.sheet, this.currentRow.getAndIncrement());
			final CellEditor cellEditor = this.config.getCellEditor();
			Integer columnIndex;
			for (final Table.Cell<?, ?, ?> cell : aliasTable) {
				columnIndex = getColumnIndex(cell);
				if (null != columnIndex) {
					CellUtil.setCellValue(CellUtil.getOrCreateCell(row, columnIndex), cell.getValue(), this.styleSet, false, cellEditor);
				}
			}
		} else {
			writeRow(aliasTable.values());
		}
		return this;
	}

	/**
	 * 写出一行标题数据，标题数据不替换别名<br>
	 * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>
	 * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动+1
	 *
	 * @param rowData 一行的数据
	 * @return this
	 */
	public SheetDataWriter writeHeaderRow(final Iterable<?> rowData) {
		final int rowNum = this.currentRow.getAndIncrement();
		final Row row = this.config.insertRow ? this.sheet.createRow(rowNum) : RowUtil.getOrCreateRow(this.sheet, rowNum);

		final Map<String, Integer> headerLocationCache = new LinkedHashMap<>();
		final CellEditor cellEditor = this.config.getCellEditor();
		int i = 0;
		Cell cell;
		for (final Object value : rowData) {
			cell = CellUtil.getOrCreateCell(row, i);
			CellUtil.setCellValue(cell, value, this.styleSet, true, cellEditor);
			headerLocationCache.put(StrUtil.toString(value), i);
			i++;
		}
		return setHeaderLocationCache(headerLocationCache);
	}

	/**
	 * 写出一行数据<br>
	 * 本方法只是将数据写入Workbook中的Sheet，并不写出到文件<br>
	 * 写出的起始行为当前行号，可使用{@link #getCurrentRow()}方法调用，根据写出的的行数，当前行号自动+1
	 *
	 * @param rowData 一行的数据
	 * @return this
	 */
	public SheetDataWriter writeRow(final Iterable<?> rowData) {
		final int rowNum = this.currentRow.getAndIncrement();
		final Row row = this.config.insertRow ? this.sheet.createRow(rowNum) : RowUtil.getOrCreateRow(this.sheet, rowNum);
		RowUtil.writeRow(row, rowData, this.styleSet, false, this.config.getCellEditor());
		return this;
	}

	// region ----- currentRow ops

	/**
	 * 获得当前行
	 *
	 * @return 当前行
	 */
	public int getCurrentRow() {
		return this.currentRow.get();
	}

	/**
	 * 设置当前所在行
	 *
	 * @param rowIndex 行号
	 * @return this
	 */
	public SheetDataWriter setCurrentRow(final int rowIndex) {
		this.currentRow.set(rowIndex);
		return this;
	}

	/**
	 * 跳过当前行，并获取下一行的行号
	 *
	 * @return this
	 */
	public int passAndGet() {
		return this.currentRow.incrementAndGet();
	}

	/**
	 * 跳过指定行数，并获取当前行号
	 *
	 * @param rowNum 跳过的行数
	 * @return this
	 */
	public int passRowsAndGet(final int rowNum) {
		return this.currentRow.addAndGet(rowNum);
	}

	/**
	 * 重置当前行为0
	 *
	 * @return this
	 */
	public SheetDataWriter resetRow() {
		this.currentRow.set(0);
		return this;
	}
	// endregion

	/**
	 * 查找标题或标题别名对应的列号
	 *
	 * @param cell 别名表，rowKey：原名，columnKey：别名
	 * @return 列号，如果未找到返回null
	 */
	private Integer getColumnIndex(final Table.Cell<?, ?, ?> cell) {
		// 首先查找原名对应的列号
		Integer location = this.headerLocationCache.get(StrUtil.toString(cell.getRowKey()));
		if (null == location) {
			// 未找到，则查找别名对应的列号
			location = this.headerLocationCache.get(StrUtil.toString(cell.getColumnKey()));
		}
		return location;
	}
}
