package net.w_horse.excelpojo.excel;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

import net.w_horse.excelpojo.annotation.ExcelPOJOAnnotationParser;
import net.w_horse.excelpojo.bean.Utils;
import net.w_horse.excelpojo.xml.tag.Use;

import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;

public abstract class AbstractRepeatsSeeker extends AbstractCellSeeker {

	private String label;
	private String position;
	private String terminate;
	private String retrieveFrom;
	private String listClass = "java.util.ArrayList";
	private boolean margedRows = false;

	abstract int getRowLimit(HSSFSheet sheet);
	abstract Offset getOffset(int rowIndex);
	abstract Offset getOffset4MappedCell(int rowIndex);

	@Override
	public boolean verify() throws IllegalArgumentException {
		if (((getLabel() == null) || getLabel().isEmpty())
			&& ((getPosition() == null) || getPosition().isEmpty())
		) {
			throw new IllegalArgumentException("Neither the label nor the position are specified.");
		}
		return true;
	}

	@Override
	public Object seekCellValue(HSSFSheet sheet, Class<?> requiredType)
					throws ClassNotFoundException, LinkageError, CellNotFoundException {
		ArrayList<Object> list = new ArrayList<Object>();

		// 表の位置を検索
		Offset basePosition = seekCellPosition(sheet);
		if (basePosition == null) {
			if (Use.equalsIgnoreCase(Use.REQUIRED, getUse())) {
				throw new CellNotFoundException("label='" + getLabel() + "'");
			}
			return convertListObject(list, ClassUtils.forName(getExcelPOJOBridge().getTargetClass()));
		}

		HashMap<String, AbstractCellSeeker> elementClassProperties = getExcelPOJOBridge().getTargetClassProperties();
		int rowLimit = getRowLimit(sheet);
		for (int tableRowIndex = 0; tableRowIndex <= rowLimit; tableRowIndex++) {
			// カラム情報を保持するインスタンスを作成
			Class<?> elementClass = ClassUtils.forName(getExcelPOJOBridge().getTargetClass());

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

			Object elementInstance = Utils.instantiateTarget(getExcelPOJOBridge().getTargetClass());
			boolean hasData = false;
			for (String propertyName : elementClassProperties.keySet()) {
				PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(elementClass, propertyName);
				if (propertyDescriptor == null) continue;

				AbstractCellSeeker cellSeeker = elementClassProperties.get(propertyName);
				Object property;

				if (cellSeeker instanceof MappedCellSeeker) {
					property = ((MappedCellSeeker)cellSeeker).seekCellValue(sheet, basePosition, getOffset4MappedCell(0), getOffset(tableRowIndex));
				} else if (cellSeeker instanceof ConstantValueCellSeeker) {
					property = ((ConstantValueCellSeeker)cellSeeker).getValue();
				} else {
					// 上記以外⇒LabeledCell はセルの値を直接インスタンスにセットする
					LabeledCellSeeker labeledCellSeeker = (LabeledCellSeeker)cellSeeker;

					Offset cellPosition = labeledCellSeeker.seekCellPosition(sheet, basePosition, getOffset(tableRowIndex));
					String strValue = getCellValue(sheet, cellPosition, String.class);
					if (!getTerminate().isEmpty() && (strValue != null) && strValue.equals(getTerminate())) {
						break;	// 表終端文字列に一致したので読み込みを終了
					}
					if (!isEmpty(strValue)) hasData = true;

					property = getCellValue(sheet, cellPosition, propertyDescriptor.getPropertyType());
					if (isEmpty(strValue) && (isMargedRows() || labeledCellSeeker.isMargedRows())) {
						// 行をさかのぼって値を取得
						property = seekMargedRowsValue(sheet, cellPosition, tableRowIndex, propertyDescriptor.getPropertyType());
					}
				}
				ReflectionUtils.invokeMethod(propertyDescriptor.getWriteMethod(),
						elementInstance,
						new Object[]{property});
			}
			if (!hasData) {
				// すべてのデータがNULLだった場合はループを抜ける
				break;
			}
			list.add(elementInstance);
		}
		return convertListObject(list, ClassUtils.forName(getExcelPOJOBridge().getTargetClass()));
	}

	@Override
	protected Offset seekCellPosition(HSSFSheet sheet) throws CellNotFoundException {
		return seekCellPosition(sheet, new Offset(), new Offset());
	}

	@Override
	protected Offset seekCellPosition(HSSFSheet sheet, Offset basePosition, Offset offset) throws CellNotFoundException {
		AbstractCellSeeker tableBaseSeeker;
		if (!getLabel().isEmpty()) {
			LabeledCellSeeker seeker = new LabeledCellSeeker();
			seeker.setLabel(getLabel());
			seeker.setRetrieveFrom(getRetrieveFrom());
			seeker.setUse(getUse());
			tableBaseSeeker = seeker;
		} else {
			PointedCellSeeker seeker = new PointedCellSeeker();
			seeker.setPosition(getPosition());
			tableBaseSeeker = seeker;
			seeker.setUse(getUse());
		}
		return tableBaseSeeker.seekCellPosition(sheet, basePosition, offset);
	}


	private Object convertListObject(ArrayList<?> list, Class<?> elementClass) throws ClassNotFoundException, LinkageError {
		// リストの指定がなければ ArrayList でそのまま返す。
		if (getListClass().isEmpty()) {
			return list;
		}

		Class<?> listClass = ClassUtils.forName(getListClass());
		if (ClassUtils.isAssignable(List.class, listClass)) {
			Constructor<?> c = ClassUtils.getConstructorIfAvailable(listClass, new Class[]{Collection.class});
			return BeanUtils.instantiateClass(c, new Object[]{list});
		} else if (listClass.isArray()) {
			Object array = Array.newInstance(elementClass, list.size());
        	for (int i = 0; i < list.size(); i++) {
        		Array.set(array, i, list.get(i));
        	}
        	return array;
		}
		return list;
	}

	@Override
	public void setValue(HSSFSheet sheet, Object listBean) throws CellNotFoundException {
		// 表の位置を検索
		Offset basePosition = seekCellPosition(sheet);

		List<?> list = null;
		if (ClassUtils.isAssignable(List.class, listBean.getClass())) {
			list = (List<?>) listBean;
		} else if (listBean.getClass().isArray()) {
			list = CollectionUtils.arrayToList(listBean);
		}

		HashMap<String, AbstractCellSeeker> elementBeanProperties = getExcelPOJOBridge().getTargetClassProperties();
		int tableRowIndex = 0;
		for (Object elementBean : list) {
			for (String propertyName : elementBeanProperties.keySet()) {
				PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(elementBean.getClass(), propertyName);
				AbstractCellSeeker cellSeeker = elementBeanProperties.get(propertyName);
				Object value = ReflectionUtils.invokeMethod(propertyDescriptor.getReadMethod(),
															elementBean,
															new Object[]{});
				if (cellSeeker instanceof MappedCellSeeker) {
					((MappedCellSeeker)cellSeeker).setValue(sheet, basePosition, getOffset4MappedCell(0), getOffset(tableRowIndex), value);
				} else {
					cellSeeker.setValue(sheet, basePosition, getOffset(tableRowIndex), value);
				}
			}
			tableRowIndex++;
		}
	}

	@Override
	protected void setValue(HSSFSheet sheet, Offset basePosition,
			Offset offset, Object value) {
		// List の書き込みは行わないのでなにもしない
	}

	public void setLabel(String label) {
		this.label = label;
	}
	public String getLabel() {
		return label;
	}

	public void setPosition(String position) {
		this.position = position;
	}
	public String getPosition() {
		return position;
	}

	public void setTerminate(String terminate) {
		this.terminate = terminate;
	}
	public String getTerminate() {
		return terminate;
	}

	public void setRetrieveFrom(String retrieveFrom) {
		this.retrieveFrom = retrieveFrom;
	}
	public String getRetrieveFrom() {
		return retrieveFrom;
	}

	public void setListClass(String listClass) {
		this.listClass = listClass;
	}
	public String getListClass() {
		return listClass;
	}

	private Object seekMargedRowsValue(HSSFSheet sheet, Offset basePosition, int limit, Class<?> requiredType) {
		Object value = Utils.convertIfNecessary(null, requiredType);
		for (int i = 1; i <= limit; i++) {
			Offset offset = getOffset(-i);
			value = getCellValue(getCell(sheet, basePosition, offset), requiredType);
			if (!isEmpty(value)) {
				return value;
			}
		}
		return value;
	}

	private boolean isEmpty(Object value) {
		if (value == null) return true;
		if (value instanceof String) {
			return ((String)value).isEmpty();
		}
		return false;
	}
	public void setMargedRows(boolean margedRows) {
		this.margedRows = margedRows;
	}
	public boolean isMargedRows() {
		return margedRows;
	}

}