package net.w_horse.excelpojo.excel.cellseeker;

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 java.util.regex.Pattern;

import net.w_horse.excelpojo.ExcelPOJOException;
import net.w_horse.excelpojo.annotation.ExcelPOJOAnnotationParser;
import net.w_horse.excelpojo.bean.Utils;
import net.w_horse.excelpojo.converter.CellFieldConverter;
import net.w_horse.excelpojo.excel.CellNotFoundException;
import net.w_horse.excelpojo.xml.tag.Use;

import org.apache.poi.ss.usermodel.Sheet;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

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;
	private String margedLabel;
	private String succeedFields = "";

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

	@Override
	public boolean verify() throws IllegalArgumentException, ExcelPOJOException {
		super.verify();
		if (!StringUtils.hasLength(getLabel())
			&& !StringUtils.hasLength(getPosition())
		) {
			throw new IllegalArgumentException("Neither the label nor the position are specified.");
		}
		Pattern pattern = Pattern.compile("\\{(\\s*\\w+(_\\w+)*\\s*=\\s*\\w+(_\\w+)*\\s*)(,(\\s*\\w+(_\\w+)*\\s*=\\s*\\w+(_\\w+)*\\s*))*\\}");
		if (!getSucceedFields().isEmpty() && !pattern.matcher(getSucceedFields()).matches()) {
			throw new IllegalArgumentException("There is a mistake in the format of the succeedFields specification. \n"
					+ "Format:{fieldName1 = parentFieldName1 [, fieldName2 = parentFieldName2]...}");
		}
		if (StringUtils.hasLength(getListClass())) {
			try {
				Class<?> listClass = ClassUtils.forName(getListClass());
				if (!ClassUtils.isAssignable(List.class, listClass)
					&& !listClass.isArray()
				) {
					throw new IllegalArgumentException("There is an invalid class in the listClass specification. ");
				}
			} catch (Throwable t) {
				throw new ExcelPOJOException(t);
			}
		}
		return true;
	}

	protected abstract Offset getRangeOffset();
	@Override
	public Object seekCellValue(Sheet sheet, Class<?> requiredType)
					throws ClassNotFoundException, LinkageError, IllegalArgumentException, ExcelPOJOException {
		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, CellSeeker> 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;

				CellSeeker cellSeeker = elementClassProperties.get(propertyName);
				cellSeeker.verify();

				Object property;
				if (cellSeeker instanceof LabeledCellSeeker) {
					LabeledCellSeeker labeledCellSeeker = (LabeledCellSeeker)cellSeeker;
					labeledCellSeeker.setRange(convertOffset2RangeString(basePosition, getRangeOffset()));

					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(), labeledCellSeeker.getConverter());
					}
				} else if (cellSeeker instanceof MappedCellSeeker) {
					property = ((MappedCellSeeker)cellSeeker).seekCellValue(sheet, basePosition, getOffset4MappedCell(0), getOffset(tableRowIndex));
				} else {
					property = cellSeeker.seekCellValue(sheet, 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(Sheet sheet) throws IllegalArgumentException, ExcelPOJOException {
		return seekCellPosition(sheet, new Offset(), new Offset());
	}

	@Override
	protected Offset seekCellPosition(Sheet sheet, Offset basePosition, Offset offset) throws IllegalArgumentException, ExcelPOJOException {
		AbstractCellSeeker tableBaseSeeker;
		if (!getLabel().isEmpty()) {
			LabeledCellSeeker seeker = new LabeledCellSeeker();
			seeker.setLabel(getLabel());
			seeker.setMargedLabel(getMargedLabel());
			seeker.setRetrieveFrom(getRetrieveFrom());
			seeker.setUse(getUse());
			seeker.setRange(getRange());
			tableBaseSeeker = seeker;
		} else {
			PointedCellSeeker seeker = new PointedCellSeeker();
			seeker.setPosition(getPosition());
			seeker.setUse(getUse());
			seeker.setRange(getRange());
			tableBaseSeeker = seeker;
		}
		tableBaseSeeker.verify();
		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(Sheet sheet, Object listBean) throws IllegalArgumentException, ExcelPOJOException {
		// 表の位置を検索
		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, CellSeeker> elementBeanProperties = getExcelPOJOBridge().getTargetClassProperties();
		int tableRowIndex = 0;
		for (Object elementBean : list) {
			for (String propertyName : elementBeanProperties.keySet()) {
				PropertyDescriptor propertyDescriptor = BeanUtils.getPropertyDescriptor(elementBean.getClass(), propertyName);
				CellSeeker cellSeeker = elementBeanProperties.get(propertyName);
				cellSeeker.setRange(convertOffset2RangeString(basePosition, getRangeOffset()));
				cellSeeker.verify();

				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
	public void setValue(Sheet sheet, Offset basePosition,
			Offset offset, Object value) {
		// List の書き込みは行わないのでなにもしない
	}


	private Object seekMargedRowsValue(Sheet sheet, Offset basePosition, int limit,
										Class<?> requiredType, CellFieldConverter converter) throws ExcelPOJOException {
		Object value;
		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 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;
	}
	public void setMargedRows(boolean margedRows) {
		this.margedRows = margedRows;
	}
	public boolean isMargedRows() {
		return margedRows;
	}
	public void setMargedLabel(String margedLabel) {
		this.margedLabel = margedLabel;
	}
	public String getMargedLabel() {
		return margedLabel;
	}
	public void setSucceedFields(String succeedFields) {
		this.succeedFields = succeedFields;
	}
	public String getSucceedFields() {
		return succeedFields;
	}

}