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 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.AbstractCellSeeker;
import net.w_horse.excelpojo.excel.ConstantValueCellSeeker;
import net.w_horse.excelpojo.excel.HorizontalRepeatsSeeker;
import net.w_horse.excelpojo.excel.LabeledCellSeeker;
import net.w_horse.excelpojo.excel.MappedCellSeeker;
import net.w_horse.excelpojo.excel.PointedCellSeeker;
import net.w_horse.excelpojo.excel.SheetNotFoundException;
import net.w_horse.excelpojo.excel.VerticalRepeatsSeeker;
import net.w_horse.excelpojo.xml.tag.RetrieveType;

import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

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

	private String sheetName;
	private String targetClass = "";

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

	private AbstractCellSeeker 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.");
		}

		try {
			setSheetName(beanAnnotation.sheetName());
			if (beanAnnotation.retrieveType() == RetrieveType.NONE) {
				setTargetClass(targetClass.getCanonicalName());
			} else {
				ExcelPOJOAnnotationParser parser = new ExcelPOJOAnnotationParser();
				AbstractCellSeeker 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) {
			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(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 {
			HSSFWorkbook workBook = new HSSFWorkbook(new POIFSFileSystem(inputStream));
			HSSFSheet sheet = workBook.getSheet(sheetName);
			if (sheet == null) throw new SheetNotFoundException(sheetName);

			if (getCellSeeker() != null) {
				// 表の場合（VerticalRepeats / HorizontalRepeats / MappedCell）
				// VerticalRepeats / HorizontalRepeats の場合は型の指定は定義情報にあり、
				// MappedCell の場合はMap<String, Object>で返すのでnullを指定する。
				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);
				AbstractCellSeeker cellSeeker = getTargetClassProperties().get(propertyName);

				Object value = cellSeeker.seekCellValue(sheet, propertyDescriptor.getPropertyType());
				ReflectionUtils.invokeMethod(propertyDescriptor.getWriteMethod(),
						targetBeanInstance,
						new Object[]{value});
			}
		} 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 inputBookStream = null;
		try {
			inputBookStream = new FileInputStream(inputBookName);
			HSSFWorkbook workBook = new HSSFWorkbook(new POIFSFileSystem(inputBookStream));
			save(workBook, sheetName, outputBookName, bean);
		} catch (ExcelPOJOException e) {
			throw e;
		} catch (Throwable t) {
			throw new ExcelPOJOException(t);
		} finally {
			try { inputBookStream.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(HSSFWorkbook inputBook, String sheetName, String outputBookName, Object bean) throws ExcelPOJOException {
		FileOutputStream outputBookStream = null;
		try {
			HSSFSheet 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;

				AbstractCellSeeker cellSeeker = getTargetClassProperties().get(propertyName);
				Object value = ReflectionUtils.invokeMethod(propertyDescriptor.getReadMethod(),
						bean,
						new Object[]{});
				cellSeeker.setValue(sheet, value);
			}
			outputBookStream = new FileOutputStream(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, AbstractCellSeeker> targetBeanProperties) {
		this.targetClassProperties = targetBeanProperties;
	}
	public HashMap<String, AbstractCellSeeker> getTargetClassProperties() {
		return targetClassProperties;
	}

	public void setCellSeeker(AbstractCellSeeker cellSeeker) {
		this.cellSeeker = cellSeeker;
	}
	public AbstractCellSeeker getCellSeeker() {
		return cellSeeker;
	}

	private AbstractCellSeeker buildCellSeeker(RetrieveType retrieveType) {
		AbstractCellSeeker 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;
	}

}
