package eu.dnetlib.dlms.jdbc.server;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import eu.dnetlib.dlms.jdbc.ColumnInfo;
import eu.dnetlib.dlms.jdbc.DataResult;
import eu.dnetlib.dlms.jdbc.ExecutorContext;
import eu.dnetlib.dlms.jdbc.ast.Statement;

/**
 * Executer class of commands in DOroty Language. This class should use dlms-core DAO to work with the system.
 * 
 * @author lexis
 */
public abstract class AbstractDOLExecuter {

	/** Logger. */
	private static final Log log = LogFactory.getLog(AbstractDOLExecuter.class);

	/** Info about the columns returned by the execution of a dol statement. */
	private ColumnInfo[] columns;
	/**
	 * Result of the execution of a list of statements.
	 */
	private Future<List<DataResult>> exeResults;

	/**
	 * No args constructor.
	 */
	public AbstractDOLExecuter() {
		this.columns = new ColumnInfo[0];
		this.exeResults = null;
	}

	public List<DataResult> getNextResult() throws InterruptedException, ExecutionException {
		log.debug("They ask me for nextResult..." + this.exeResults);
		List<DataResult> res = this.exeResults.get();
		log.debug("nextResult is..." + res);
		return res;
	}

	/**
	 * Executes the command, calling the method executeImpl(String) that has to be overridden by this class' subclasses.
	 * 
	 * @param stm
	 *            the statement to execute
	 * @throws SQLException
	 *             while executing the request
	 * @throws ExecutionException
	 * @see AbstractDOLExecuter.executeImpl(ASTNode)
	 */
	public void execute(final Collection<eu.dnetlib.dlms.jdbc.ast.Statement> stmColl, final Map<String, ParamInfo> params) throws SQLException,
			ExecutionException {
		if (stmColl == null)
			throw new SQLException("Empty collection of Statement");
		ExecutorService exeService = Executors.newCachedThreadPool();
		Future<List<DataResult>> futureList = exeService.submit(new Callable<List<DataResult>>() {

			public List<DataResult> call() throws Exception {
				return executeTask(stmColl, params);
			}

		});
		this.exeResults = futureList;
	}

	/**
	 * Contains the logic of the execution of the ASTNode owned by this executer, eventually enqueuing data in
	 * this.dataRows, so that data can be consumed afterward.
	 * 
	 * @param stm
	 *            ASTNode instance to execute
	 * @throws SQLException
	 *             while executing the request
	 */
	protected abstract Object executeImpl(final eu.dnetlib.dlms.jdbc.ast.Statement stm, ExecutorContext context) throws SQLException;

	private boolean isArrayOfArray(final Object obj) {
		if (obj instanceof Object[]) {
			// anche i suoi oggetti sono object[], prendo il primo e basta, e'
			// sufficiente
			Object[] res = (Object[]) obj;
			if (res.length == 0)
				return false;
			return res[0] instanceof Object[];
		} else
			return false;
	}

	private List<DataResult> executeTask(final Collection<eu.dnetlib.dlms.jdbc.ast.Statement> stmColl, final Map<String, ParamInfo> params)
			throws SQLException {
		List<Statement> stmList = new ArrayList<Statement>(stmColl);
		ExecutorContext theContext = new ExecutorContext(new HashMap<String, Variable<?>>(), params, null);
		List<DataResult> dataRes = new ArrayList<DataResult>();
		for (int i = 0; i < stmList.size(); i++) {
			Statement stm = stmList.get(i);
			log.debug(this + ".execute(" + stm + ")");
			Object result = executeImpl(stm, theContext);
			// if it was the last statement, then I have to build the
			// DataResult for ResultSet
			if (i == stmList.size() - 1) {
				// se res e' un array di array di object allora viene da una
				// select e devo creare un data result per ciascun elemento
				Object[] res = null;
				if (isArrayOfArray(result)) {
					res = (Object[]) result;
					for (int k = 0; k < res.length; k++) {
						Object[] content = (Object[]) res[k];
						// potrei farlo anche solo alla prima...
						setColumns(new ColumnInfo[content.length]);
						dataRes.add(new DataResult(content));
					}
				} else {
					if (!isSimpleObject(result)) {
						// e' un array ma i suoi oggetti sono semplici
						// Object
						if (result != null) {
							res = (Object[]) result;
							setColumns(new ColumnInfo[1]);
							for (int k = 0; k < res.length; k++) {
								dataRes.add(new DataResult(new Object[] { res[k] }));
							}
						}
					} else {
						// lui stesso e' solo un Object
						if (result != null) {
							res = new Object[] { result };
							setColumns(new ColumnInfo[res.length]);
							dataRes.add(new DataResult(res));
						}
					}
				}
			}
		}
		log.debug("All ASTNode executed");
		return dataRes;
	}

	private boolean isSimpleObject(final Object obj) {
		return !(obj instanceof Object[]);
	}

	public void setColumns(final ColumnInfo[] columns) {
		this.columns = columns;
	}

	public ColumnInfo[] getColumns() {
		return this.columns;
	}
}
