package org.luaj.vm2.libs.jse;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.util.Properties;
import java.util.logging.Logger;

import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaUserdata;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.luaj.vm2.libs.OneArgFunction;
import org.luaj.vm2.libs.TwoArgFunction;
import org.luaj.vm2.libs.VarArgFunction;

public class LuaSqlMysqlLib extends TwoArgFunction {

	public LuaValue call(LuaValue modname, LuaValue env) {
		LuaValue pkg = env.get("package");
		if (!pkg.istable()) {
			return env;
		}
		LuaValue preload = pkg.get("preload");
		if (!preload.istable()) {
			return env;
		}
		preload.set("luasql.mysql", new Loader());
		return env;
	}

	static final class Loader extends TwoArgFunction {
		public LuaValue call(LuaValue modname, LuaValue env) {
			LuaTable module = new LuaTable(0, 1);
			module.set("mysql", new MysqlFactory());
			return module;
		}
	}

	static final class MysqlFactory extends VarArgFunction {
		public Varargs invoke(Varargs args) {
			String driverClass = args.optjstring(1, "com.mysql.jdbc.Driver");
			try {
				Class.forName(driverClass);
			} catch (ClassNotFoundException e) {
				throw new LuaError("mysql jdbc driver not found: " + driverClass);
			}
			return new EnvironmentValue();
		}
	}

	static final class EnvironmentValue extends LuaTable {
		private boolean closed;

		EnvironmentValue() {
			set("connect", new connect());
			set("close", new close());
		}

		final class connect extends VarArgFunction {
			public Varargs invoke(Varargs args) {
				checkOpen();
				String database = args.checkjstring(2);
				String user = args.optjstring(3, null);
				String password = args.optjstring(4, null);
				String host = args.optjstring(5, "localhost");
				int port = args.optint(6, 3306);
				String url = "jdbc:mysql://" + host + ":" + port + "/" + database;
				try {
					Connection connection = DriverManager.getConnection(url, user, password);
					return new ConnectionValue(connection);
				} catch (SQLException e) {
					throw sqlError("mysql connect failed", e);
				}
			}
		}

		final class close extends OneArgFunction {
			public LuaValue call(LuaValue self) {
				closed = true;
				return LuaValue.TRUE;
			}
		}

		private void checkOpen() {
			if (closed) {
				throw new LuaError("environment is closed");
			}
		}
	}

	static final class ConnectionValue extends LuaTable {
		private final Connection connection;
		private boolean closed;

		ConnectionValue(Connection connection) {
			this.connection = connection;
			set("execute", new execute());
			set("close", new close());
			set("commit", new commit());
			set("rollback", new rollback());
			set("setautocommit", new setautocommit());
		}

		final class execute extends VarArgFunction {
			public Varargs invoke(Varargs args) {
				checkOpen();
				String sql = args.checkjstring(2);
				try {
					Statement statement = connection.createStatement();
					boolean hasResultSet = statement.execute(sql);
					if (hasResultSet) {
						return new CursorValue(statement, statement.getResultSet());
					}
					int updated = statement.getUpdateCount();
					statement.close();
					return LuaValue.valueOf(updated);
				} catch (SQLException e) {
					throw sqlError("mysql execute failed", e);
				}
			}
		}

		final class close extends OneArgFunction {
			public LuaValue call(LuaValue self) {
				if (!closed) {
					try {
						connection.close();
					} catch (SQLException e) {
						throw sqlError("mysql close failed", e);
					}
					closed = true;
				}
				return LuaValue.TRUE;
			}
		}

		final class commit extends OneArgFunction {
			public LuaValue call(LuaValue self) {
				checkOpen();
				try {
					connection.commit();
					return LuaValue.TRUE;
				} catch (SQLException e) {
					throw sqlError("mysql commit failed", e);
				}
			}
		}

		final class rollback extends OneArgFunction {
			public LuaValue call(LuaValue self) {
				checkOpen();
				try {
					connection.rollback();
					return LuaValue.TRUE;
				} catch (SQLException e) {
					throw sqlError("mysql rollback failed", e);
				}
			}
		}

		final class setautocommit extends VarArgFunction {
			public Varargs invoke(Varargs args) {
				checkOpen();
				try {
					connection.setAutoCommit(args.arg(2).toboolean());
					return LuaValue.TRUE;
				} catch (SQLException e) {
					throw sqlError("mysql setautocommit failed", e);
				}
			}
		}

		private void checkOpen() {
			if (closed) {
				throw new LuaError("connection is closed");
			}
		}
	}

	static final class CursorValue extends LuaTable {
		private final Statement statement;
		private final ResultSet resultSet;
		private boolean closed;

		CursorValue(Statement statement, ResultSet resultSet) {
			this.statement = statement;
			this.resultSet = resultSet;
			set("fetch", new fetch());
			set("close", new close());
		}

		final class fetch extends VarArgFunction {
			public Varargs invoke(Varargs args) {
				checkOpen();
				try {
					if (!resultSet.next()) {
						return LuaValue.NIL;
					}
					LuaValue target = args.arg(2);
					String mode = args.optjstring(3, "n");
					LuaTable row = target.istable() ? target.checktable() : new LuaTable();
					boolean assoc = "a".equals(mode);
					boolean both = "an".equals(mode) || "na".equals(mode);
					if (!assoc && !both && !"n".equals(mode)) {
						throw new LuaError("invalid fetch mode: " + mode);
					}
					ResultSetMetaData meta = resultSet.getMetaData();
					int cols = meta.getColumnCount();
					for (int i = 1; i <= cols; i++) {
						LuaValue value = LuaSqlMysqlLib.toLuaValue(resultSet.getObject(i));
						if (!assoc) {
							row.set(i, value);
						}
						if (assoc || both) {
							row.set(meta.getColumnLabel(i), value);
						}
					}
					return row;
				} catch (SQLException e) {
					throw sqlError("mysql fetch failed", e);
				}
			}
		}

		final class close extends OneArgFunction {
			public LuaValue call(LuaValue self) {
				if (!closed) {
					try {
						resultSet.close();
						statement.close();
					} catch (SQLException e) {
						throw sqlError("mysql cursor close failed", e);
					}
					closed = true;
				}
				return LuaValue.TRUE;
			}
		}

		private void checkOpen() {
			if (closed) {
				throw new LuaError("cursor is closed");
			}
		}
	}

	private static LuaValue toLuaValue(Object value) {
		if (value == null) {
			return LuaValue.NIL;
		}
		if (value instanceof LuaValue) {
			return (LuaValue) value;
		}
		if (value instanceof String) {
			return LuaValue.valueOf((String) value);
		}
		if (value instanceof Boolean) {
			return LuaValue.valueOf(((Boolean) value).booleanValue());
		}
		if (value instanceof Integer || value instanceof Short || value instanceof Byte) {
			return LuaValue.valueOf(((Number) value).intValue());
		}
		if (value instanceof Long) {
			return LuaValue.valueOf(((Long) value).longValue());
		}
		if (value instanceof Float || value instanceof Double) {
			return LuaValue.valueOf(((Number) value).doubleValue());
		}
		return new LuaUserdata(value);
	}

	private static LuaError sqlError(String prefix, SQLException e) {
		return new LuaError(prefix + ": " + e.getMessage());
	}

	public static final class FakeMysqlDriver implements Driver {
		public Connection connect(String url, Properties info) throws SQLException {
			if (!acceptsURL(url)) {
				return null;
			}
			return (Connection) Proxy.newProxyInstance(
				getClass().getClassLoader(),
				new Class[] { Connection.class },
				new ConnectionHandler());
		}

		public boolean acceptsURL(String url) {
			return url != null && url.startsWith("jdbc:mysql://");
		}

		public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
			return new DriverPropertyInfo[0];
		}

		public int getMajorVersion() { return 1; }
		public int getMinorVersion() { return 0; }
		public boolean jdbcCompliant() { return false; }
		public Logger getParentLogger() throws SQLFeatureNotSupportedException { throw new SQLFeatureNotSupportedException(); }
	}

	static final class ConnectionHandler implements InvocationHandler {
		private boolean autoCommit = true;
		private boolean closed;

		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			String name = method.getName();
			if ("createStatement".equals(name)) {
				checkOpen();
				return Proxy.newProxyInstance(
					getClass().getClassLoader(),
					new Class[] { Statement.class },
					new StatementHandler());
			}
			if ("close".equals(name)) {
				closed = true;
				return null;
			}
			if ("commit".equals(name) || "rollback".equals(name)) {
				checkOpen();
				return null;
			}
			if ("setAutoCommit".equals(name)) {
				checkOpen();
				autoCommit = ((Boolean) args[0]).booleanValue();
				return null;
			}
			if ("getAutoCommit".equals(name)) {
				checkOpen();
				return Boolean.valueOf(autoCommit);
			}
			if ("isClosed".equals(name)) {
				return Boolean.valueOf(closed);
			}
			if ("isValid".equals(name)) {
				return Boolean.valueOf(!closed);
			}
			return defaultValue(method.getReturnType());
		}

		private void checkOpen() throws SQLException {
			if (closed) {
				throw new SQLException("connection closed");
			}
		}
	}

	static final class StatementHandler implements InvocationHandler {
		private boolean closed;
		private ResultSet resultSet;
		private int updateCount = -1;

		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			String name = method.getName();
			if ("execute".equals(name)) {
				checkOpen();
				String sql = ((String) args[0]).trim();
				if ("select 1 as answer, 'ok' as status".equalsIgnoreCase(sql)) {
					resultSet = (ResultSet) Proxy.newProxyInstance(
						getClass().getClassLoader(),
						new Class[] { ResultSet.class },
						new ResultSetHandler());
					updateCount = -1;
					return Boolean.TRUE;
				}
				resultSet = null;
				updateCount = 1;
				return Boolean.FALSE;
			}
			if ("getResultSet".equals(name)) {
				checkOpen();
				return resultSet;
			}
			if ("getUpdateCount".equals(name)) {
				checkOpen();
				return Integer.valueOf(updateCount);
			}
			if ("close".equals(name)) {
				closed = true;
				return null;
			}
			if ("isClosed".equals(name)) {
				return Boolean.valueOf(closed);
			}
			return defaultValue(method.getReturnType());
		}

		private void checkOpen() throws SQLException {
			if (closed) {
				throw new SQLException("statement closed");
			}
		}
	}

	static final class ResultSetHandler implements InvocationHandler {
		private boolean closed;
		private boolean delivered;

		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			String name = method.getName();
			if ("next".equals(name)) {
				checkOpen();
				if (!delivered) {
					delivered = true;
					return Boolean.TRUE;
				}
				return Boolean.FALSE;
			}
			if ("getObject".equals(name)) {
				checkOpen();
				int index = ((Integer) args[0]).intValue();
				switch (index) {
				case 1: return Integer.valueOf(1);
				case 2: return "ok";
				default: return null;
				}
			}
			if ("getMetaData".equals(name)) {
				checkOpen();
				return Proxy.newProxyInstance(
					getClass().getClassLoader(),
					new Class[] { ResultSetMetaData.class },
					new ResultSetMetaDataHandler());
			}
			if ("close".equals(name)) {
				closed = true;
				return null;
			}
			if ("isClosed".equals(name)) {
				return Boolean.valueOf(closed);
			}
			return defaultValue(method.getReturnType());
		}

		private void checkOpen() throws SQLException {
			if (closed) {
				throw new SQLException("resultset closed");
			}
		}
	}

	static final class ResultSetMetaDataHandler implements InvocationHandler {
		public Object invoke(Object proxy, Method method, Object[] args) {
			String name = method.getName();
			if ("getColumnCount".equals(name)) {
				return Integer.valueOf(2);
			}
			if ("getColumnLabel".equals(name) || "getColumnName".equals(name)) {
				int index = ((Integer) args[0]).intValue();
				return index == 1 ? "answer" : "status";
			}
			return defaultValue(method.getReturnType());
		}
	}

	private static Object defaultValue(Class type) {
		if (type == Void.TYPE) return null;
		if (type == Boolean.TYPE) return Boolean.FALSE;
		if (type == Integer.TYPE) return Integer.valueOf(0);
		if (type == Long.TYPE) return Long.valueOf(0L);
		if (type == Double.TYPE) return Double.valueOf(0d);
		if (type == Float.TYPE) return Float.valueOf(0f);
		if (type == Short.TYPE) return Short.valueOf((short) 0);
		if (type == Byte.TYPE) return Byte.valueOf((byte) 0);
		if (type == Character.TYPE) return Character.valueOf((char) 0);
		return null;
	}
}
