package net.java.amateras.xlsbeans;

import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import jxl.Workbook;
import jxl.WorkbookSettings;
import net.java.amateras.xlsbeans.annotation.PostProcess;
import net.java.amateras.xlsbeans.annotation.Sheet;
import net.java.amateras.xlsbeans.processor.FieldProcessor;
import net.java.amateras.xlsbeans.processor.FieldProcessorFactory;
import net.java.amateras.xlsbeans.xml.AnnotationReader;
import net.java.amateras.xlsbeans.xml.XMLInfo;
import net.java.amateras.xlsbeans.xml.XMLLoader;

/**
 * This is the entry point of XLSBeans.
 * 
 * @author Naoki Takezoe
 */
public class XLSBeans {
	
	private boolean ignoreSheetNotFound = false;
	
	/**
	 * Constructor.
	 */
	public XLSBeans(){
		super();
	}
	
	public void setIgnoreSheetNotFound(boolean flag){
		ignoreSheetNotFound = flag;
	}
	
	public boolean isIgnoreSheetNotFound(){
		return ignoreSheetNotFound;
	}
	
	/**
	 * Loads one sheet as the single object from the Excel file.
	 * 
	 * @param <P> the type parameter for the object type
	 * @param xlsIn the InputStream of the Excel file
	 * @param clazz the object type
	 * @return the loaded object
	 * @throws XLSBeansException
	 */
	public <P> P load(InputStream xlsIn, Class<P> clazz) throws XLSBeansException {
		return load(xlsIn, null, clazz);
	}
	
	private <P> P loadSheet(Class<P> clazz, jxl.Sheet jxlSheet, AnnotationReader reader) throws Exception {
		
		List<NeedPostProcess> needPostProcess = new ArrayList<NeedPostProcess>();
		P obj = clazz.newInstance();
		
		try {
			for(Method method : clazz.getMethods()){
				for(Annotation ann : reader.getAnnotations(clazz, method)){
					FieldProcessor processor = FieldProcessorFactory.getProcessor(ann);
					if(processor!=null){
						processor.doProcess(jxlSheet, obj, method, ann, reader, needPostProcess);
						break;
					} else if(ann instanceof PostProcess){
						NeedPostProcess post = new NeedPostProcess(obj, method);
						needPostProcess.add(post);
						break;
					}
				}
			}
			
			for(NeedPostProcess need : needPostProcess){
				need.getMethod().invoke(need.getTarget(), new Object[0]);
			}
			
		} catch(InvocationTargetException ex){
			Throwable t = ex.getCause();
			if(t!=null && t instanceof Exception){
				throw (Exception)t;
			}
			throw ex;
		}
		
		return obj;
	}
	
	private jxl.Sheet[] findSheet(Workbook w, Sheet sheet) throws XLSBeansException {
		if(sheet==null){
			throw new XLSBeansException("Can't find @Sheet.");
		}
		
//		jxl.Sheet jxlSheet = null;
		if(sheet.name().length() > 0){
			jxl.Sheet jxlSheet = w.getSheet(sheet.name());
			if(jxlSheet==null){
				throw new SheetNotFoundException(sheet.name());
			}
			return new jxl.Sheet[]{jxlSheet};
			
		} else if(sheet.number()!=-1){
			if(sheet.number() >= w.getSheets().length){
				throw new SheetNotFoundException(sheet.number());
			}
			jxl.Sheet jxlSheet = w.getSheet(sheet.number());
			return new jxl.Sheet[]{jxlSheet};
			
		} else if(sheet.regex().length() > 0){
			Pattern pattern = Pattern.compile(sheet.regex());
			List<jxl.Sheet> matches = new ArrayList<jxl.Sheet>();
			for(jxl.Sheet jxlSheet: w.getSheets()){
				if(pattern.matcher(jxlSheet.getName()).matches()){
					matches.add(jxlSheet);
				}
			}
			if(matches.size() > 0){
				return matches.toArray(new jxl.Sheet[matches.size()]);
			}
			throw new SheetNotFoundException(sheet.number());
		}
		
		throw new XLSBeansException("@Sheet requires name or number parameter.");
	}
	
	/**
	 * Loads one sheet as the single object from the Excel file.
	 * 
	 * @param <P> the type parameter for the object type
	 * @param xlsIn the InputStream of the Excel file
	 * @param xmlIn the InputStream of the External annotation definition file
	 * @param clazz the object type
	 * @return the loaded object
	 * @throws XLSBeansException
	 */
	public <P> P load(InputStream xlsIn, InputStream xmlIn, Class<P> clazz) throws XLSBeansException {
		try {
			XMLInfo info = null;
			if(xmlIn!=null){
				info = XMLLoader.load(xmlIn);
			}
			
			AnnotationReader reader = new AnnotationReader(info);
			
			WorkbookSettings settings = new WorkbookSettings();
			settings.setGCDisabled(true);
			Workbook w = Workbook.getWorkbook(xlsIn, settings);
			
			Sheet sheet = reader.getAnnotation(clazz, Sheet.class);
			if(sheet==null){
				throw new XLSBeansException("Can't find @Sheet.");
			}
			try {
				jxl.Sheet[] jxlSheets = findSheet(w, sheet);
				return loadSheet(clazz, jxlSheets[0], reader);
				
			} catch(SheetNotFoundException ex){
				if(ignoreSheetNotFound){
					return null;
				} else {
					throw ex;
				}
			}
		} catch(XLSBeansException ex){
			throw ex;
		} catch(Exception ex){
			throw new XLSBeansException(ex);
		}
	}
	
	/**
	 * Loads sheets as multiple objects from the Excel file.
	 * 
	 * @param <P> the type parameter for the object type
	 * @param xlsIn the InputStream of the Excel file
	 * @param clazz the object type
	 * @return the loaded objects
	 * @throws XLSBeansException
	 */
	public <P> P[] loadMultiple(InputStream xlsIn, Class<P> clazz) throws XLSBeansException {
		return loadMultiple(xlsIn, null, clazz);
	}
	
	/**
	 * Loads sheets as multiple objects from the Excel file.
	 * 
	 * @param <P> the type parameter for the object type
	 * @param xlsIn the InputStream of the Excel file
	 * @param xmlIn the InputStream of the External annotation definition file
	 * @param clazz the object type
	 * @return the loaded objects
	 * @throws XLSBeansException
	 */
	@SuppressWarnings("unchecked")
	public <P> P[] loadMultiple(InputStream xlsIn, InputStream xmlIn, Class<P> clazz) throws XLSBeansException {
		try {
			XMLInfo info = null;
			if(xmlIn!=null){
				info = XMLLoader.load(xmlIn);
			}
			
			AnnotationReader reader = new AnnotationReader(info);
			
			WorkbookSettings settings = new WorkbookSettings();
			settings.setGCDisabled(true);
			Workbook w = Workbook.getWorkbook(xlsIn, settings);
			
			Sheet sheet = reader.getAnnotation(clazz, Sheet.class);
			if(sheet==null){
				throw new XLSBeansException("Can't find @Sheet.");
			}
			
			List<P> list = new ArrayList<P>();
			
			if(sheet.number()==-1 && sheet.name().length()==0 && sheet.regex().length()==0){
				// If sheet isn't specified
				jxl.Sheet[] sheets = w.getSheets();
				for(jxl.Sheet jxlSheet : sheets){
					list.add(loadSheet(clazz, jxlSheet, reader));
				}
			} else {
				// If sheet is specified 
				try {
					for(jxl.Sheet jxlSheet: findSheet(w, sheet)){
						list.add(loadSheet(clazz, jxlSheet, reader));
					}
				} catch(SheetNotFoundException ex){
					if(!ignoreSheetNotFound){
						throw ex;
					}
				}
			}
			
			return list.toArray((P[])Array.newInstance(clazz, list.size()));
			
		} catch(XLSBeansException ex){
			throw ex;
		} catch(Exception ex){
			throw new XLSBeansException(ex);
		}
	}
	
	/**
	 * Loads sheets as multiple type objects from the Excel file.
	 * 
	 * @param xlsIn the InputStream of the Excel file
	 * @param classes the object types
	 * @return the loaded objects
	 * @throws XLSBeansException
	 */
	public Object[] loadMultiple(InputStream xlsIn, Class<?>[] classes) throws XLSBeansException {
		return loadMultiple(xlsIn, null, classes);
	}
	
	/**
	 * Loads sheets as multiple type objects from the Excel file.
	 * 
	 * @param xlsIn the InputStream of the Excel file
	 * @param xmlIn the InputStream of the External annotation definition file
	 * @param classes the object types
	 * @return the loaded objects
	 * @throws XLSBeansException
	 */
	public Object[] loadMultiple(InputStream xlsIn, InputStream xmlIn, Class<?>[] classes) throws XLSBeansException {
		try {
			XMLInfo info = null;
			if(xmlIn!=null){
				info = XMLLoader.load(xmlIn);
			}
			
			AnnotationReader reader = new AnnotationReader(info);
			
			WorkbookSettings settings = new WorkbookSettings();
			settings.setGCDisabled(true);
			Workbook w = Workbook.getWorkbook(xlsIn, settings);
			
			List<Object> list = new ArrayList<Object>();
			
			for(Class<?> clazz : classes){
				Sheet sheet = reader.getAnnotation(clazz, Sheet.class);
				if(sheet==null){
					throw new XLSBeansException("Can't find @Sheet.");
				}
				try {
					for(jxl.Sheet jxlSheet: findSheet(w, sheet)){
						list.add(loadSheet(clazz, jxlSheet, reader));
					}
				} catch(SheetNotFoundException ex){
					if(!ignoreSheetNotFound){
						throw ex;
					}
				}
			}
			
			return list.toArray();
			
		} catch(XLSBeansException ex){
			throw ex;
		} catch(Exception ex){
			throw new XLSBeansException(ex);
		}
	}

}
