local mymodule = {}

db = require("acf.db")

-- Helper library for model functions for viewing and editing database entries
-- Relies heavily on the acf-lib/db.lua library (db object passed into each function)

local function get_connection(dbase, self, clientdata)
	local retval = cfe({ type="group", value={} })
	retval.value.connection = cfe({ type="group", value={}, label="Database Connection", seq=0 })
	retval.value.connection.value.engine = cfe({ type="select", label="Database Engine", option={}, key=true, seq=1 })
	for n,v in pairs(db.engine) do
		retval.value.connection.value.engine.option[#retval.value.connection.value.engine.option+1] = {label=n, value=tostring(v)}
	end
	retval.value.connection.value.database = cfe({ label="Database", key=true, seq=2 })
	retval.value.connection.value.user = cfe({ label="User", key=true, seq=3 })
	retval.value.connection.value.password = cfe({ type="password", label="Password", key=true, seq=4 })
	retval.value.connection.value.host = cfe({ label="Host", key=true, seq=5 })
	retval.value.connection.value.port = cfe({ label="Port", key=true, seq=6 })

	self.handle_clientdata(retval, clientdata)

	-- If dbase has a default, remove the cfe. Otherwise, fill in with the value from the cfe.
	for n,v in pairs(retval.value.connection.value) do
		if dbase[n] then
			retval.value.connection.value[n] = nil
		elseif n == "engine" or n == "port" then
			dbase[n] = tonumber(v.value)
		else
			dbase[n] = v.value
		end
	end

	return retval
end

local function fill_connection(dbase, self, retval)
	-- Fill in dbase with the values from the cfe retval
	for n,v in pairs(retval.value.connection.value) do
		if n == "engine" or n == "port" then
			dbase[n] = tonumber(v.value)
		else
			dbase[n] = v.value
		end
	end

	return retval
end

function mymodule.list_databases(dbase, self, clientdata)
	local db = dbase
	if type(dbase) == "function" then
		db = dbase()
	end

	local retval = get_connection(db, self, clientdata)
	retval.label = "List of Databases"
	retval.value.databases = cfe({ type="list", value={}, label="List of Databases" })
	local res, err = pcall(function()
		retval.value.databases.value = db.listdatabases()
	end)
	if not res and err then
		retval.value.connection.errtxt = err
	end

	return retval
end

function mymodule.list_tables(dbase, self, clientdata)
	local db = dbase
	if type(dbase) == "function" then
		db = dbase()
	end

	local retval = get_connection(db, self, clientdata)
	retval.label = "List of Database Tables"
	retval.value.tables = cfe({ type="list", value={}, label="List of Database Tables" })
	local res, err = pcall(function()
		local connected = db.databaseconnect()
		retval.value.tables.value = db.listtables()
		if connected then db.databasedisconnect() end
	end)
	if not res and err then
		retval.value.connection.errtxt = err
	end
	return retval
end

function mymodule.list_table_entries(dbase, self, clientdata)
	local db = dbase
	if type(dbase) == "function" then
		db = dbase()
	end

	local retval = get_connection(db, self, clientdata)
	retval.label = "Database Table Entries"
	-- Need to specify the table
	retval.value.table = cfe({ label="Table", key=true })
	-- Here is the pagination/filtering info
        retval.value.page = cfe({ value=0, label="Page Number", descr="0 indicates ALL", key=true })
	retval.value.pagesize = cfe({ value=10, label="Page Size", key=true })
	retval.value.rowcount = cfe({ value=0, label="Row Count" })
	-- orderby must be an array of tables with column name and direction
	retval.value.orderby = cfe({ type="structure", value={}, label="Order By", key=true })
	-- filter is a table with a string filter for each column
	retval.value.filter = cfe({ type="structure", value={}, label="Filter", key=true })
	self.handle_clientdata(retval, clientdata)

	-- Here is the result
	retval.value.fields = cfe({ type="list", value={}, label="List of Table Fields" })
	retval.value.keyfields = cfe({ type="list", value={}, label="List of Key Table Fields" })
	retval.value.entries = cfe({ type="structure", value={}, label="List of Database Entries" })

        -- Process the incoming page data
	local page = tonumber(retval.value.page.value) or 0
	retval.value.page.value = page
	local pagesize = tonumber(retval.value.pagesize.value) or 10
	retval.value.pagesize.value = pagesize

	local res, err = pcall(function()
		local connected = db.databaseconnect()
		local tables = db.listtables()
		retval.value.table.errtxt = "Table does not exist"
		retval.errtxt = "Table does not exist"
		for i,t in ipairs(tables) do
			if t == retval.value.table.value then
				retval.value.table.errtxt = nil
				retval.errtxt = nil
				-- FIXME - in the future, we should be able to do next these three things in one function call to db
				retval.value.fields.value = db.listcolumns(t) or {}
				retval.value.keyfields.value = db.listkeycolumns(t) or {}
				-- this will not work for sqlite and the results are database-specific
				local sql = "SELECT data_type FROM information_schema.columns WHERE table_name = '"..db.escape(t).."' ORDER BY ordinal_position"
				local types = db.getselectresponse(sql)
				local fieldtypes = {}
				for i,f in ipairs(types) do fieldtypes[retval.value.fields.value[i]] = f.data_type end

				local orderby = {}
				local columns = {}
				for i,c in ipairs(retval.value.fields.value) do columns[c] = c end
				local directions = {asc="ASC", desc="DESC", ASC="ASC", DESC="DESC"}
				for i,o in ipairs(retval.value.orderby.value) do
					if columns[o.column] and directions[o.direction] then
						orderby[#orderby+1] = columns[o.column].." "..directions[o.direction]
					end
				end

				local filter = {}
				-- FIXME - the following has not been exhaustively tested for all column types and databases
				for c,f in pairs(retval.value.filter.value) do
					if columns[c] and f ~= "" then
						if string.find(fieldtypes[c]:lower(), "char") or string.find(fieldtypes[c]:lower(), "text") then
							filter[#filter+1] = columns[c].."~'"..db.escape(f).."'"
						elseif tonumber(f) then
							filter[#filter+1] = columns[c].."="..tonumber(f)
						else
							filter[#filter+1] = columns[c].."='"..db.escape(f).."'"
						end
					end
				end

				sql = " FROM "..db.escape(t)
				if #filter>0 then
					sql = sql.." WHERE "..table.concat(filter, " AND ")
				end
				if page > 0 then
					local count = db.getselectresponse("SELECT count(*)"..sql)
					retval.value.rowcount.value = count[1].count
				end
				if #orderby>0 then
					sql = sql.." ORDER BY "..table.concat(orderby, ", ")
				end
				if page > 0 then
					sql = sql.." LIMIT "..pagesize.." OFFSET "..(page - 1)*pagesize
				end
				retval.value.entries.value = db.getselectresponse("SELECT *"..sql) or {}
				if page <= 0 then
					retval.value.rowcount.value = #retval.value.entries.value
				end
			end
		end
		if connected then db.databasedisconnect() end
	end)
	if not res and err then
		retval.value.connection.errtxt = err
	end

	return retval
end

function mymodule.get_new_table_entry(dbase, self, clientdata)
	local db = dbase
	if type(dbase) == "function" then
		db = dbase()
	end

	local retval = get_connection(db, self, clientdata)
	retval.label = "Database Table Entry"

	retval.value.table = cfe({ label="Table", errtxt="Table does not exist", key=true, seq=1 })
	self.handle_clientdata(retval, clientdata)
	retval.errtxt = "Table does not exist"
	if retval.value.table.value ~= "" then
		local res, err = pcall(function()
			local connected = db.databaseconnect()
			local tables = db.listtables()
			for i,t in ipairs(tables) do
				if t == retval.value.table.value then
					retval.value.table.errtxt = nil
					retval.value.table.readonly = true
					retval.errtxt = nil
					for n,v in pairs(retval.value.connection.value) do
						v.readonly = true
					end
					break
				end
			end
			if not errtxt then
				retval.value.fields = cfe({ type="group", value={}, label="Table Fields", seq=2 })
				retval.value.nulls = cfe({ type="group", value={}, label="Null Value Table Fields", seq=3 })
				retval.value.defaults = cfe({ type="group", value={}, label="Default Value Table Fields", seq=4 })
				local fields, defaults, nullable = db.listcolumns(retval.value.table.value)
				for i,f in ipairs(fields) do
					retval.value.fields.value[f] = cfe({ label=f, seq=i })
					if nullable[f] then
						retval.value.nulls.value[f] = cfe({ type="boolean", value=false, label=f, seq=i })
					end
					if defaults[f] then
						retval.value.defaults.value[f] = cfe({ type="boolean", value=true, label=f, default=defaults[f], seq=i })
					end
				end
			end
			if connected then db.databasedisconnect() end
		end)
		if not res and err then
			retval.value.connection.errtxt = err
		end
	end

	return retval
end

local function get_field_value(dbase, self, entry, field)
	local retval
	if entry.value.defaults and entry.value.defaults.value[field] and entry.value.defaults.value[field].value then
		retval = "DEFAULT"
	elseif entry.value.nulls and entry.value.nulls.value[field] and entry.value.nulls.value[field].value then
		retval = "NULL"
	elseif entry.value.fields and entry.value.fields.value[field] then
		retval = "'"..dbase.escape(entry.value.fields.value[field].value).."'"
	end
	return retval
end

function mymodule.get_table_entry(dbase, self, clientdata)
	local db = dbase
	if type(dbase) == "function" then
		db = dbase()
	end

	local retval = mymodule.get_new_table_entry(db, self, clientdata)
	if retval.value.fields and retval.value.defaults then
		for n,v in pairs(retval.value.fields.value) do
			if retval.value.defaults.value[n] then
				retval.value.defaults.value[n].value = false
			end
		end
	end
	self.handle_clientdata(retval, clientdata) -- Fill in the fields
	if not retval.value.connection.errtxt and not retval.value.table.errtxt and retval.value.fields then
		local res, err = pcall(function()
			local connected = db.databaseconnect()
			local keyfields = db.listkeycolumns(retval.value.table.value)
			local reversekeyfields = {}
			local fieldchecks = {}
			for i,f in ipairs(keyfields) do
				reversekeyfields[f] = i
				retval.value.fields.value[f].errtxt = "Entry does not exist"
				fieldchecks[#fieldchecks+1] = f.."="..get_field_value(db, self, retval, f)
			end
			retval.errtxt = "Entry does not exist"
			local sql = "SELECT * FROM "..db.escape(retval.value.table.value).." WHERE "..table.concat(fieldchecks, " AND ")
			local entry = db.getselectresponse(sql)
			if entry and #entry > 0 then
				retval.errtxt = nil
				--for n,v in pairs(entry[1]) do
				for n,v in pairs(retval.value.fields.value) do
					if not entry[1][n] then
						retval.value.nulls.value[n].value = true
					else
						retval.value.fields.value[n].value = entry[1][n]
					end
					if reversekeyfields[n] then
						retval.value.fields.value[n].readonly = true
						retval.value.fields.value[n].errtxt = nil
					end
				end
			end
			if connected then db.databasedisconnect() end
		end)
		if not res and err then
			retval.errtxt = err
		end
	end

	return retval
end

function mymodule.create_table_entry(dbase, self, entry, action)
        return mymodule.update_table_entry(dbase, self, entry, action, true)
end

function mymodule.update_table_entry(dbase, self, entry, action, create)
	local db = dbase
	if type(dbase) == "function" then
		db = dbase()
	end
	fill_connection(db, self, entry)

	local success = true
	local errtxt
	-- Validate the settings
	-- relying on get_table_entry to do the validation of table
	if entry.value.table.value == "" or entry.value.table.errtxt or nil == entry.value.fields then
		success = false
		entry.value.table.errtxt = "Table does not exist"
	end
	if success then
        	local res, err = pcall(function()
			local connected = db.databaseconnect()
			local keyfields = db.listkeycolumns(entry.value.table.value)
			local reversekeyfields = {}
			local fieldchecks = {}
			for i,f in ipairs(keyfields) do
				reversekeyfields[f] = i
				fieldchecks[#fieldchecks+1] = f.."="..get_field_value(db, self, entry, f)
			end
			-- We want to catch syntax errors, because they just indicate the entry does not exist
			local res2, err2 = pcall(function()
				local sql = "SELECT * FROM "..db.escape(entry.value.table.value).." WHERE "..table.concat(fieldchecks, " AND ")
				local tmp = db.getselectresponse(sql)
				if not create and (not tmp or #tmp == 0) then
					success = false
					for i,n in ipairs(keyfields) do
						entry.value.fields.value[n].errtxt = "Entry does not exist"
				end
				elseif create and tmp and #tmp>0 then
					success = false
					for i,n in ipairs(keyfields) do
						entry.value.fields.value[n].errtxt = "Entry already exists"
					end
				end
			end)
			if not res2 and err2 then
				if not create then
					success = false
					errtxt = err
					for i,n in ipairs(keyfields) do
						entry.value.fields.value[n].errtxt = "Entry does not exist"
					end
				end
			end
			if success then
				local sql
				local names = {}
				local values = {}
				if create then
					for n,v in pairs(entry.value.fields.value) do
						names[#names+1] = n
						values[#values+1] = get_field_value(db, self, entry, n)
					end
					sql = "INSERT INTO "..db.escape(entry.value.table.value).." ("..table.concat(names, ", ")..") VALUES("..table.concat(values, ", ")..")"
				else
					for n,v in pairs(entry.value.fields.value) do
						if not reversekeyfields[n] then
							values[#values+1] = n.."="..get_field_value(db, self, entry, n)
						end
					end
					sql = "UPDATE "..db.escape(entry.value.table.value).." SET "..table.concat(values, ", ").." WHERE "..table.concat(fieldchecks, " AND ")
				end
				db.runsqlcommand(sql)
			end
			if connected then db.databasedisconnect() end
		end)
		if not res and err then
			success = false
			errtxt = err
		end
	end
	if not success then
		if create then
			entry.errtxt = errtxt or "Failed to create entry"
		else
			entry.errtxt = errtxt or "Failed to save entry"
		end
	end
	return entry
end

function mymodule.get_delete_table_entry(dbase, self, clientdata)
	local db = dbase
	if type(dbase) == "function" then
		db = dbase()
	end

	local retval = get_connection(db, self, clientdata)
	retval.label = "Delete Database Table Entry"

	retval.value.table = cfe({ label="Table", errtxt="Table does not exist", key=true, seq=1 })
	self.handle_clientdata(retval, clientdata)
	retval.errtxt = "Table does not exist"
	if retval.value.table.value ~= "" then
		local res, err = pcall(function()
			local connected = db.databaseconnect()
			local tables = db.listtables()
			for i,t in ipairs(tables) do
				if t == retval.value.table.value then
					retval.value.table.errtxt = nil
					retval.value.table.readonly = true
					retval.errtxt = nil
					for n,v in pairs(retval.value.connection.value) do
						v.readonly = true
					end
					break
				end
			end
			if not errtxt then
				local keyfields = db.listkeycolumns(retval.value.table.value)
				retval.value.fields = cfe({ type="group", value={}, label="Table Fields", seq=2 })
				retval.value.nulls = cfe({ type="group", value={}, label="Null Value Table Fields", seq=3 })
				for i,f in ipairs(keyfields) do
					retval.value.fields.value[f] = cfe({ label=f, seq=i })
					retval.value.nulls.value[f] = cfe({ type="boolean", value=false, label=f, seq=i })
				end
			end
			if connected then db.databasedisconnect() end
		end)
		if not res and err then
			retval.value.connection.errtxt = err
		end
	end

	return retval
end

function mymodule.delete_table_entry(dbase, self, entry)
	local db = dbase
	if type(dbase) == "function" then
		db = dbase()
	end
	fill_connection(db, self, entry)

	entry.errtxt = "Failed to delete table entry"
	if entry.value.table.value == "" then
		entry.value.table.errtxt = "Invalid table"
	else
	       	local res, err = pcall(function()
			local connected = db.databaseconnect()
			entry.value.table.errtxt = "Invalid table"
			local tables = db.listtables()
			for i,t in ipairs(tables) do
				if t == entry.value.table.value then
					entry.value.table.errtxt = nil
					break
				end
			end
			if not entry.value.table.errtxt then
				local keyfields = db.listkeycolumns(entry.value.table.value)
				local fieldchecks = {}
				for i,f in ipairs(keyfields) do
					fieldchecks[#fieldchecks+1] = f.."="..get_field_value(db, self, entry, f)
				end
				local sql = "DELETE FROM "..db.escape(entry.value.table.value).." WHERE "..table.concat(fieldchecks, " AND ")
				db.runsqlcommand(sql)
				entry.errtxt = nil
			end
			if connected then db.databasedisconnect() end
		end)
		if not res and err then
			entry.errtxt = err
		end
	end

	return entry
end

return mymodule
