package net.w_horse.excelpojo;

import java.beans.PropertyDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;

import net.w_horse.excelpojo.annotation.Bean;
import net.w_horse.excelpojo.annotation.ExcelPOJOAnnotationParser;
import net.w_horse.excelpojo.bean.Utils;
import net.w_horse.excelpojo.excel.SheetNotFoundException;
import net.w_horse.excelpojo.excel.cellseeker.AbstractRepeatsSeeker;
import net.w_horse.excelpojo.excel.cellseeker.CellSeeker;
import net.w_horse.excelpojo.excel.cellseeker.ConstantValueCellSeeker;
import net.w_horse.excelpojo.excel.cellseeker.HorizontalRepeatsSeeker;
import net.w_horse.excelpojo.excel.cellseeker.LabeledCellSeeker;
import net.w_horse.excelpojo.excel.cellseeker.MappedCellSeeker;
import net.w_horse.excelpojo.excel.cellseeker.PointedCellSeeker;
import net.w_horse.excelpojo.excel.cellseeker.VerticalRepeatsSeeker;
import net.w_horse.excelpojo.xml.tag.RetrieveType;

import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ResourceUtils;

/**
 * ExcelとPOJO間の読み込み/書込みを実行するクラス
 *
 * @author kawahara
 *
 */
public class ExcelPOJOBridge {

	private String sheetName;
	private String targetClass = "";

	private HashMap<String, CellSeeker> targetClassProperties = new HashMap<String, CellSeeker>();

	private String retrieveType;
	private CellSeeker cellSeeker;

	public ExcelPOJOBridge() {
	}

	/**
	 * XMLによる設定ファイルを使用せずに
	 * Excel⇔POJO間のマッピングを行う際に使用するコンストラクタ<br>
	 *
	 * @param targetClass 読み込んだ情報を格納するクラス
	 * @throws ExcelPOJOException アノテーションが指定されていない場合
	 */
	public ExcelPOJOBridge(Class<?> targetClass) throws ExcelPOJOException {
		Bean beanAnnotation = targetClass.getAnnotation(Bean.class);
		if (beanAnnotation == null) {
			throw new ExcelPOJOException("There is no annotation specified on the target class.");
		}

		switch (beanAnnotation.retrieveType()) {
		case CONSTANT_VALUE:
		case LABELED_CELL:
		case POINTED_CELL:
			throw new ExcelPOJOException(
					String.format("There is an invalid retrieveType(%s) in Bean.",
					beanAnnotation.retrieveType().getValue()));
		}

		try {
			setSheetName(beanAnnotation.sheetName());
			ExcelPOJOAnnotationParser parser = new ExcelPOJOAnnotationParser();
			parser.setTargetClassProperties(getTargetClassProperties(), targetClass);
			if (beanAnnotation.retrieveType() == RetrieveType.NONE) {
				setTargetClass(targetClass.getCanonicalName());
			} else {
				CellSeeker seeker = buildCellSeeker(beanAnnotation.retrieveType());
				parser.setTargetClass(targetClass.getCanonicalName());
				seeker.set(beanAnnotation, parser);
				setCellSeeker(seeker);
			}
		} catch (Throwable t) {
			throw new ExcelPOJOException(t);
		}
	}

	/**
	 * Excelからデータを読み込んでPOJOを作成する
	 *
	 * @param file 読み込み対象のExcelファイル名
	 * @return 作成されたPOJO
	 * @throws ExcelPOJOException シートが指定されずに呼び出された場合
	 */
	public Object load(String file) throws ExcelPOJOException {
		if ((getSheetName() == null) || getSheetName().isEmpty()) {
			throw new ExcelPOJOException("There is no sheet name specified.");
		}
		return load(file, getSheetName());
	}

	/**
	 * Excelからデータを読み込んでPOJOを作成する<br>
	 * シート名をコーディングにより指定する場合に使用<br>
	 *
	 * @param file 読み込み対象のExcelファイル名
	 * @param sheetName シート名
	 * @return 作成されたPOJO
	 * @throws ExcelPOJOException
	 */
	public Object load(String file, String sheetName) throws ExcelPOJOException {
		FileInputStream fileInputStream = null;
		try {
			fileInputStream = new FileInputStream(ResourceUtils.getFile(file));
			return load(fileInputStream, sheetName);
		} catch (ExcelPOJOException e) {
			throw e;
		} catch (Throwable t) {
			throw new ExcelPOJOException(t);
		} finally {
			try { fileInputStream.close(); } catch (Exception e) { }
		}
	}
	/**
	 * Excelからデータを読み込んでPOJOを作成する<br>
	 * InputStreamによってExcelのデータを読み込む際に使用<br>
	 *
	 * @param inputStream Excelのデータ
	 * @return 作成されたPOJO
	 * @throws ExcelPOJOException
	 */
	public Object load(InputStream inputStream) throws ExcelPOJOException  {
		return load(inputStream, getSheetName());
	}

	/**
	 * Excelからデータを読み込んでPOJOを作成する<br>
	 * InputStreamによってExcelのデータを読み込む際に使用<br>
	 *
	 * @param inputStream Excelのデータ
	 * @param sheetName シート名
	 * @return 作成されたPOJO
	 * @throws ExcelPOJOException
	 */
	public Object load(InputStream inputStream, String sheetName) throws ExcelPOJOException {
		Object targetBeanInstance;
		try {
			Workbook workBook = WorkbookFactory.create(inputStream);
			Sheet sheet = workBook.getSheet(sheetName);
			if (sheet == null) throw new SheetNotFoundException(sheetName);

			if (getCellSeeker() != null) {
				// ExcelPOJOXmlParser#doParse()で例外があげられないのでここでチェック
				if (RetrieveType.LABELED_CELL.equals(getRetrieveType())
					|| RetrieveType.POINTED_CELL.equals(getRetrieveType())
					|| RetrieveType.CONSTANT_VALUE.equals(getRetrieveType())
				) {
					throw new ExcelPOJOException(
							String.format("There is an invalid retrieveType(%s) in Bean.",
							getRetrieveType()));
				}

				// 表の場合（VerticalRepeats / HorizontalRepeats / MappedCell）
				// VerticalRepeats / HorizontalRepeats の場合は型の指定は定義情報にあり、
				// MappedCell の場合はMap<String, Object>で返すのでnullを指定する。
				getCellSeeker().verify();
				return getCellSeeker().seekCellValue(sheet, null);
			}

			Class<?> targetClass = ClassUtils.forName(getTargetClass());

			// アノテーションからの情報で補填
			ExcelPOJOAnnotationParser parser = new ExcelPOJOAnnotationParser();
			parser.setTargetClassProperties(getTargetClassProperties(), targetClass);

			targetBeanInstance = Utils.instantiateTarget(getTargetClass());
			for (String propertyName : getTargetClassProperties().keySet()) {
				PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(targetClass, propertyName);
				CellSeeker cellSeeker = getTargetClassProperties().get(propertyName);
				cellSeeker.verify();

				Object value = cellSeeker.seekCellValue(sheet, propertyDescriptor.getPropertyType());
				ReflectionUtils.invokeMethod(propertyDescriptor.getWriteMethod(),
						targetBeanInstance,
						new Object[]{value});
			}

			// 親クラスの値を引き継ぐフィールド（succeeds指定）があれば設定する。
			for (String propertyName : getTargetClassProperties().keySet()) {
				setSucceedsValue(targetBeanInstance, propertyName);
			}

		} catch (ExcelPOJOException e) {
			throw e;
		} catch (Throwable t) {
			throw new ExcelPOJOException(t);
		}
		return targetBeanInstance;

	}


	/**
	 * POJOからExcelへ書き込む<br>
	 *
	 * @param inputOutputBookName 書き込み先のExcelファイル名
	 * @param bean 書き込み情報を保持しているPOJO
	 * @throws ExcelPOJOException
	 */
	public void save(String inputOutputBookName, Object bean) throws ExcelPOJOException {
		save(inputOutputBookName, getSheetName(), inputOutputBookName, bean);
	}
	/**
	 * POJOからExcelへ書き込む<br>
	 * シート名をコーディングにより指定する場合に使用<br>
	 *
	 * @param inputOutputBookName 書き込み先のExcelファイル名
	 * @param sheetName シート名
	 * @param bean 書き込み情報を保持しているPOJO
	 * @throws ExcelPOJOException
	 */
	public void save(String inputOutputBookName, String sheetName, Object bean) throws ExcelPOJOException {
		save(inputOutputBookName, sheetName, inputOutputBookName, bean);
	}
	/**
	 * POJOからExcelへ書き込む<br>
	 * テンプレートとしてExcelファイルを読み込み、
	 * 別ファイルとして保存する場合に使用<br>
	 * シート名をコーディングにより指定する場合に使用<br>
	 *
	 * @param inputBookName テンプレートとして読み込むExcelファイル名
	 * @param sheetName シート名
	 * @param outputBookName 書き込み先のExcelファイル名
	 * @param bean 書き込み情報を保持しているPOJO
	 * @throws ExcelPOJOException
	 */
	public void save(String inputBookName, String sheetName, String outputBookName, Object bean) throws ExcelPOJOException {
		FileInputStream inputStream = null;
		try {
			inputStream = new FileInputStream(ResourceUtils.getFile(inputBookName));
			Workbook workBook = WorkbookFactory.create(inputStream);
			save(workBook, sheetName, outputBookName, bean);
		} catch (ExcelPOJOException e) {
			throw e;
		} catch (Throwable t) {
			throw new ExcelPOJOException(t);
		} finally {
			try { inputStream.close(); } catch (Exception e) { }
		}
	}


	/**
	 * POJOからExcelへ書き込む<br>
	 * テンプレートとしてExcelファイルを読み込み、
	 * 別ファイルとして保存する場合に使用<br>
	 *
	 * @param inputBook テンプレートとして読み込むExcelファイル
	 * @param sheetName シート名
	 * @param outputBookName 書き込み先のExcelファイル名
	 * @param bean 書き込み情報を保持しているPOJO
	 * @throws ExcelPOJOException 指定されたシートが存在しない場合
	 */
	public void save(Workbook inputBook, String sheetName, String outputBookName, Object bean) throws ExcelPOJOException {
		FileOutputStream outputBookStream = null;
		try {
			Sheet sheet = inputBook.getSheet(sheetName);
			if (sheet == null) throw new SheetNotFoundException(sheetName);

			if (getCellSeeker() != null) {
				// List / 配列、または Map で書き込みをする場合
				getCellSeeker().setValue(sheet, bean);
			}

			// 上記List / 配列、または Map で書き込みをする場合は下記ループは通らない
			for (String propertyName : getTargetClassProperties().keySet()) {
				PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(bean.getClass(), propertyName);
				if (propertyDescriptor == null) continue;

				CellSeeker cellSeeker = getTargetClassProperties().get(propertyName);
				Object value = ReflectionUtils.invokeMethod(propertyDescriptor.getReadMethod(),
						bean,
						new Object[]{});
				cellSeeker.setValue(sheet, value);
			}
			outputBookStream = new FileOutputStream(ResourceUtils.getFile(outputBookName));
			inputBook.write(outputBookStream);
		} catch (ExcelPOJOException e) {
			throw e;
		} catch (Throwable t) {
			throw new ExcelPOJOException(t);
		} finally {
			try { outputBookStream.close(); } catch (Exception e) { }
		}
	}

	public void setSheetName(String sheetName) {
		this.sheetName = sheetName;
	}
	public String getSheetName() {
		return sheetName;
	}

	public void setTargetClass(String targetClass) {
		this.targetClass = targetClass;
	}
	public String getTargetClass() {
		return targetClass;
	}

	public void setTargetClassProperties(HashMap<String, CellSeeker> targetBeanProperties) {
		this.targetClassProperties = targetBeanProperties;
	}
	public HashMap<String, CellSeeker> getTargetClassProperties() {
		return targetClassProperties;
	}

	public void setCellSeeker(CellSeeker cellSeeker) {
		this.cellSeeker = cellSeeker;
	}
	/**
	 * retrieveTypeを取得します。
	 * @return retrieveType
	 */
	public String getRetrieveType() {
	    return retrieveType;
	}

	/**
	 * retrieveTypeを設定します。
	 * @param retrieveType retrieveType
	 */
	public void setRetrieveType(String retrieveType) {
	    this.retrieveType = retrieveType;
	}

	public CellSeeker getCellSeeker() {
		return cellSeeker;
	}

	private CellSeeker buildCellSeeker(RetrieveType retrieveType) {
		CellSeeker seeker = null;
		switch (retrieveType) {
		case LABELED_CELL:
			seeker = new LabeledCellSeeker();
			break;
		case POINTED_CELL:
			seeker = new PointedCellSeeker();
			break;
		case CONSTANT_VALUE:
			seeker = new ConstantValueCellSeeker();
			break;
		case VERTICAL_REPEATS:
			seeker = new VerticalRepeatsSeeker();
			break;
		case HORIZONTAL_REPEATS:
			seeker = new HorizontalRepeatsSeeker();
			break;
		case MAPPED_CELL:
			seeker = new MappedCellSeeker();
			break;
		}

		return seeker;
	}

	private void setSucceedsValue(Object parentBean, String propertyName) {
		CellSeeker cellSeeker = getTargetClassProperties().get(propertyName);
		if (!(cellSeeker instanceof AbstractRepeatsSeeker)) {
			return;	// 表タイプでない場合は無視する
		}
		AbstractRepeatsSeeker seeker = (AbstractRepeatsSeeker)cellSeeker;
		if (seeker.getSucceedFields().isEmpty()) {
			return;	// 引き継ぐ指定がなければ無視する
		}
		// 親から値を引き継ぐフィールドをもつオブジェクトを取得
		PropertyDescriptor targetPropertyDescriptor
			= BeanUtils.getPropertyDescriptor(parentBean.getClass(), propertyName);
		Object targetBean = ReflectionUtils.invokeMethod(targetPropertyDescriptor.getReadMethod(),
				parentBean);

		String succeedFieldsLine = seeker.getSucceedFields();
		succeedFieldsLine = succeedFieldsLine.substring(1, succeedFieldsLine.length() -1);
		for (String ssucceedField : succeedFieldsLine.split(",")) {
			String fieldNames[] = ssucceedField.split("=");
			String targetFieldName = fieldNames[0].trim();
			String parentFieldName = fieldNames[1].trim();

			// 親から値を取得
			PropertyDescriptor propertyDescriptor
				= BeanUtils.getPropertyDescriptor(parentBean.getClass(), parentFieldName);
			Object parentValue = ReflectionUtils.invokeMethod(propertyDescriptor.getReadMethod(),
					parentBean);

			// 表の要素すべてに対して値を設定
			if (ClassUtils.isAssignable(List.class, targetBean.getClass())) {
				for (Object targetBeanElement : (List<?>)targetBean) {
					propertyDescriptor = BeanUtils.getPropertyDescriptor(
								targetBeanElement.getClass(), targetFieldName);
					ReflectionUtils.invokeMethod(propertyDescriptor.getWriteMethod(),
							targetBeanElement,
							new Object[]{parentValue});
				}
			} else if (targetBean.getClass().isArray()) {
				for (Object targetBeanElement : (Object[])targetBean) {
					propertyDescriptor = BeanUtils.getPropertyDescriptor(
								targetBeanElement.getClass(), targetFieldName);
					ReflectionUtils.invokeMethod(propertyDescriptor.getWriteMethod(),
							targetBeanElement,
							new Object[]{parentValue});
				}
			} else {
				throw new IllegalArgumentException(
						String.format("An illegal type(%s) is specified for verticalRepeats / horizontalRepeats. ",
						targetBean.getClass().getCanonicalName()));
			}
		}
	}

}
