-- POINT CONSTRUCT API/UTILITY

-- USAGE:
-- To use the point construct API, you must create a point provider object.  This object must contain the following::
-- sizeX, sizeY, sizeZ - The dimensions of the construction
-- startX, startY, startZ - The start point of construction
-- numMaterials - The number of materials used by the construction
-- getBlockAt(x, y, z) - Function that returns the material number (indexed from 1) of the block at that point, or nil if it should be empty

provider = nil
-- Offset from the turtle's origin that corresponds to the provider's origin
offsetX = 0
offsetY = 0
offsetZ = 0
-- Start values of X, Y, and Z for the getBlockAt function.  Values scan from these to these + dimensions
startX = 0
startY = 0
startZ = 0
-- Size of construction area, from the start point.
sizeX = 0
sizeY = 0
sizeZ = 0
-- Number of materials
numMaterials = 1
-- If true, the first chest is a fuel chest, and subsequent ones are item chests
hasFuelChest = false

skipEmptyBlocks = true

function setSkipEmptyBlocks(b)
	skipEmptyBlocks = b
end

function setHasFuelChest(b)
	hasFuelChest = b
end

function getOffset()
	return offsetX, offsetY, offsetZ
end

function getSize()
	return sizeX, sizeY, sizeZ
end

function getStart()
	return startX, startY, startZ
end

function getNumMaterials()
	return numMaterials
end

function getHasFuelChest()
	return hasFuelChest
end

function initValues(aoffsetX, aoffsetY, aoffsetZ, anumMaterials)
	offsetX, offsetY, offsetZ = aoffsetX, aoffsetY, aoffsetZ
	numMaterials = anumMaterials
	aturtle.setOriginNoAttackRadius(8)
	aturtle.setThrowAwaySlot(16)
	aturtle.setThrowAwayForceDigBlocks(true)
end

function init(aprovider, aoffsetX, aoffsetY, aoffsetZ)
	provider = aprovider
	sizeX, sizeY, sizeZ = provider.sizeX, provider.sizeY, provider.sizeZ
	initValues(aoffsetX, aoffsetY, aoffsetZ, provider.numMaterials)
	aturtle.resetPosition(0, 0, 0, aturtle.DIR_POS_X)
	if provider.startX ~= nil then
		startX, startY, startZ = provider.startX, provider.startY, provider.startZ
	end
	if offsetX == nil then offsetX = 2 - startX end
	if offsetY == nil then offsetY = 0 - startY end
	if offsetZ == nil then offsetZ = 0 - startZ end
end

function isAtHomeLine()
	local x, y, z = aturtle.getPosition()
	if x == 0 and y == 0 then return true end
	return false
end

function goHome(zval)
	local x, y, z = aturtle.getPosition()
	local toz = 0
	if zval ~= nil then toz = zval end
	if x == 0 and y == 0 then
		aturtle.moveToZ(toz, true)
		aturtle.changeDirection(aturtle.DIR_POS_X)
		return
	end
	if y < 1 then
		aturtle.moveToY(1, true)
	else
		aturtle.forceUp()
	end
	aturtle.moveToXZ(0, toz, true)
	aturtle.moveToY(0, true)
	aturtle.changeDirection(aturtle.DIR_POS_X)
end

function isAtChestLine()
	local x, y, z = aturtle.getPosition()
	if x == 1 and y == 1 then return true end
	return false
end

function goChestLine(zval)
	local x, y, z = aturtle.getPosition()
	local toz = 0
	if zval ~= nil then toz = zval end
	if x == 1 and y == 1 then
		if z == toz then return end
		aturtle.moveToZ(toz, "check")
		return
	end
	if y < 1 then
		aturtle.moveToY(1, true)
	else
		aturtle.forceUp()
	end
	aturtle.moveToXZ(1, zval, true)
	aturtle.moveToY(1, true)
end

function goToPosition(x, y, z)
	-- If the destination is only 1 block away, just move there
	local cx, cy, cz = aturtle.getX(), aturtle.getY(), aturtle.getZ()
	if (x == cx and y == cy and math.abs(z - cz) < 2)
		or (math.abs(x - cx) < 2 and y == cy and z == cz)
		or (x == cx and z == cz) then
		aturtle.moveTo(x, y, z, nil, true)
		return
	end
	-- If destination is in build area, go to 1 above the location, and down 1
	if x > 1 then
		-- If same Y level, just go
		if y == cy then
			aturtle.moveToXZ(x, z, true)
			return
		end
		local interY = y + 1
		if interY < 1 then interY = 1 end
		aturtle.moveToY(interY, true)
		aturtle.moveToXZ(x, z, true)
		aturtle.moveToY(y, true)
		return
	end
	-- Otherwise, go to the y level of, the maximum of, currentY+1 or 1, or the target + 1
	local cInterY = aturtle.getY() + 1
	if cInterY < 1 then cInterY = 1 end
	if cInterY < y + 1 then cInterY = y + 1 end
	aturtle.moveToY(cInterY, true)
	aturtle.moveToXZ(x, z, true)
	aturtle.moveToY(y, true)
end

function fuelToPosition(x, y, z)
	local fuel = 0
	if x > 1 then
		local interY = y + 1
		if interY < 1 then interY = 1 end
		fuel = fuel + math.abs(aturtle.getY() - interY)
		fuel = fuel + math.abs(aturtle.getX() - x)
		fuel = fuel + math.abs(aturtle.getZ() - z)
		fuel = fuel + math.abs(interY - y)
		return fuel
	end
	local cInterY = aturtle.getY() + 1
	if cInterY < 1 then cInterY = 1 end
	if cInterY < y + 1 then cInterY = y + 1 end
	fuel = fuel + math.abs(aturtle.getY() - cInterY)
	fuel = fuel + math.abs(aturtle.getX() - x)
	fuel = fuel + math.abs(aturtle.getZ() - z)
	fuel = fuel + math.abs(cInterY - y)
	return fuel
end

-- Index is resource number (starting from 1), value is Z along chest line
resourceChests = {}

function discoverChestsFromCurrentPos(maxZ)
	if maxZ == nil then maxZ = 16 end
	aturtle.changeDirection(aturtle.DIR_POS_Z)
	resourceChests = {}
	local lastWasEmpty = true
	local chestNum = 1
	while true do
		if turtle.detectDown() then
			if lastWasEmpty then
				resourceChests[chestNum] = aturtle.getZ()
				chestNum = chestNum + 1
			end
			lastWasEmpty = false
		else
			if lastWasEmpty then
				break
			end
			lastWasEmpty = true
		end
		if aturtle.getZ() >= maxZ then
			break
		end
		aturtle.forceForward()
	end
end

function discoverChests(maxZ)
	goChestLine(0)
	discoverChestsFromCurrentPos(maxZ)
end

function validateNumChests()
	local numChests = # resourceChests
	local reqChests = numMaterials
	if hasFuelChest then reqChests = reqChests + 1 end
	if numChests < reqChests then
		print("At least " .. reqChests .. " are required.  Only " .. numChests .. " detected.")
		return false
	end
	if numChests > reqChests then
		print("Warning: Extra chests detected.  Need " .. reqChests .. ", found " .. numChests .. ".")
	end
	return true
end

function goChest(resourceNum)
	if resourceNum > # resourceChests then
		print("No chest for resource " .. resourceNum .. " found.")
		return false
	end
	goChestLine(resourceChests[resourceNum])
end

function refuelFromChest(minFuel)
	if turtle.getFuelLevel() >= minFuel then return true end
	if not hasFuelChest then return false end
	goChest(1)
	-- Use throw away slot for refueling
	turtle.select(16)
	if turtle.getItemCount(16) > 0 then turtle.dropUp() end
	while turtle.getFuelLevel() < minFuel do
		if not turtle.suckDown() then
			print("No fuel found in chest.  Place fuel in chest and press enter.")
			io.read()
			return refuelFromChest(minFuel)
		end
		if turtle.getItemCount(16) < 1 then
			print("Error getting fuel from chest.")
			return false
		end
		if not turtle.refuel() then
			print("Error refueling from slot 16.  Fix the issue and press enter.")
			io.read()
			return refuelFromChest(minFuel)
		end
	end
	return true
end

function checkFuel(minFuel)
	if hasFuelChest then
		if turtle.getFuelLevel() < minFuel then
			local returnX, returnY, returnZ = aturtle.getPosition()
			minFuel = minFuel + fuelToPosition(0, 0, 0)
			if not refuelFromChest(minFuel) then
				aturtle.checkFuel(minFuel)
			end
			goToPosition(returnX, returnY, returnZ)
		end
	else
		aturtle.checkFuel(minFuel)
	end
end

function toOriginPosition(x, y, z)
	return offsetX + x, offsetY + y, offsetZ + z
end

-- Returns the next x,y,z in sequence for optimal scanning of blocks in a 3D area
-- Initial x,y,z are expected to be 0
function nextScanPosition3D(x, y, z, sizeX, sizeY, sizeZ)
	local zDir, xDir
	local doNextX = false
	local doNextY = false
	if y % 2 == 0 then
		if x % 2 == 0 then
			zDir = 1
		else
			zDir = -1
		end
	else
		if x % 2 == 0 then
			zDir = -1
		else
			zDir = 1
		end
	end
	if zDir == 1 then
		z = z + 1
		if z >= sizeZ then
			doNextX = true
			z = sizeZ - 1
		end
	else
		z = z - 1
		if z < 0 then
			doNextX = true
			z = 0
		end
	end
	if doNextX then
		if y % 2 == 0 then
			xDir = 1 
		else
			xDir = -1
		end
		if xDir == 1 then
			x = x + 1
			if x >= sizeX then
				doNextY = true
				x = sizeX - 1
			end
		else
			x = x - 1
			if x < 0 then
				doNextY = true
				x = 0
			end
		end
	end
	if doNextY then
		y = y + 1
	end
	return x, y, z
end

function nextPosition(x, y, z, x1, y1, z1, x2, y2, z2)
	if x1 == nil then x1 = startX end
	if y1 == nil then y1 = startY end
	if z1 == nil then z1 = startZ end
	if x2 == nil then x2 = startX + sizeX - 1 end
	if y2 == nil then y2 = startY + sizeY - 1 end
	if z2 == nil then z2 = startZ + sizeZ - 1 end
	local segSizeX = x2 - x1 + 1
	local segSizeY = y2 - y1 + 1
	local segSizeZ = z2 - z1 + 1
	local ix, iy, iz = nextScanPosition3D(x - x1, y - y1, z - z1, segSizeX, segSizeY, segSizeZ)
	return x1 + ix, y1 + iy, z1 + iz
end

-- all coords here are relative to the construction origin, not the aturtle origin
-- last 6 parameters are start and end values for the parallel slice
function configureNextBatch(afromX, afromY, afromZ, x1, y1, z1, x2, y2, z2)
	local slotItems = {}
	local slotCounts = {}
	local blockItems = {}
	local blockLog = {}
	local endX = startX + sizeX - 1
	local endY = startY + sizeY - 1
	local endZ = startZ + sizeZ - 1
	local x = afromX
	local y = afromY
	local z = afromZ
	-- print("batch start " .. startX .. "," .. startY .. "," .. startZ .. " end " .. endX .. "," .. endY .. "," .. endZ .. " cur " .. x .. "," .. y .."," .. z)
	local cItem
	local cSlot
	local itemSlot
	while true do
		cItem = provider.getBlockAt(x, y, z)
		if cItem ~= nil and cItem > 0 then
			-- Find a slot with the same item (if it exists)
			itemSlot = nil
			for cSlot = 1,15 do
				if slotItems[cSlot] ~= nil and slotItems[cSlot] == cItem and slotCounts[cSlot] < 64 then
					itemSlot = cSlot
					break
				end
			end
			-- If no existing slot was found, see if there's an empty one
			if itemSlot == nil then
				for cSlot = 1,15 do
					if slotItems[cSlot] == nil then
						itemSlot = cSlot
						break
					end
				end
			end
			-- If no item slot was still found, then the simulated inventory is full, and the batch is done
			if itemSlot == nil then
				break
			end
			-- Increase the count for that slot, and add the blockItems record
			slotItems[itemSlot] = cItem
			if slotCounts[itemSlot] == nil then
				slotCounts[itemSlot] = 1
			else
				slotCounts[itemSlot] = slotCounts[itemSlot] + 1
			end
			blockItems["a" .. x .. "_" .. y .. "_" .. z] = cItem
			-- Add the block log entry
			local bLogEntry = {
				["x"] = x,
				["y"] = y,
				["z"] = z,
				["item"] = cItem
			}
			table.insert(blockLog, bLogEntry)
		else
			blockItems["a" .. x .. "_" .. y .. "_" .. z] = 0
			if not skipEmptyBlocks then
				local bLogEntry = {
					["x"] = x,
					["y"] = y,
					["z"] = z,
					["item"] = nil
				}
				table.insert(blockLog, bLogEntry)
			end
		end
		-- Increment x, y, z
		x, y, z = nextPosition(x, y, z, x1, y1, z1, x2, y2, z2)
		if y > endY or (y2 ~= nil and y > y2) then
			break
		end
	end
	-- Create itemSlots array from slotItems
	local itemSlots = {}
	for cSlot, cItem in pairs(slotItems) do
		if itemSlots[cItem] == nil then
			itemSlots[cItem] = { cSlot }
		else
			table.insert(itemSlots[cItem], cSlot)
		end
	end
	-- Return the calculated values
	local batchSpec = {
		["fromX"] = afromX,
		["fromY"] = afromY,
		["fromZ"] = afromZ,
		["slotItems"] = slotItems,
		["itemSlots"] = itemSlots,
		["slotCounts"] = slotCounts,
		["blockItems"] = blockItems,
		["endX"] = endX,
		["endY"] = endY,
		["endZ"] = endZ,
		["nextX"] = x,
		["nextY"] = y,
		["nextZ"] = z,
		["blockLog"] = blockLog
	}
	return batchSpec
end

function checkSuckDown()
	while not turtle.suckDown() do
		print("Out of items.  Add items to this chest and wait 10 seconds.")
		os.sleep(10)
	end
end

function getItemStackExact(numItems, slot)
	-- If there's something in the slot, eject it
	turtle.select(slot)
	if turtle.getItemCount(slot) > 0 then
		turtle.dropUp()
	end
	-- Suck a stack
	checkSuckDown()
	if turtle.getItemCount(slot) > numItems then
		turtle.dropDown(turtle.getItemCount(slot) - numItems)
		return
	elseif turtle.getItemCount(slot) == numItems then
		return
	end
	-- Got less than the required number.  Have to accumulate more in trash stack.
	turtle.select(16)
	if turtle.getItemCount(16) > 0 then turtle.dropUp() end
	while true do
		local needItems = numItems - turtle.getItemCount(slot)
		checkSuckDown()
		local haveItems = turtle.getItemCount(16)
		if haveItems >= needItems then
			turtle.transferTo(slot, needItems)
			if turtle.getItemCount(16) > 0 then
				turtle.dropDown()
			end
			return
		else
			turtle.transferTo(slot, haveItems)
		end
	end
end

function getItemsForBatch(batch)
	local cResource, cZ
	local cSlot, cSlotIdx
	-- Go through each chest (in order, to minimize movement)
	for cResource, cZ in pairs(resourceChests) do
		local cItem = cResource
		if hasFuelChest then cItem = cResource - 1 end
		if cItem > 0 then
			-- Check if we need an item that's in this chest
			if batch.itemSlots[cItem] ~= nil then
				-- Go to the chest
				goChest(cResource)
				-- Go through each slot we need to fill with this item
				for cSlotIdx, cSlot in ipairs(batch.itemSlots[cItem]) do
					turtle.select(cSlot)
					local requireCount = batch.slotCounts[cSlot]
					-- If there's already something in the slot, eject it
					if turtle.getItemCount(cSlot) > 0 then turtle.dropUp() end
					-- Get the item
					getItemStackExact(requireCount, cSlot)
					-- Return any extra to the chest
					if turtle.getItemCount(cSlot) > requireCount then
						turtle.dropDown(turtle.getItemCount(cSlot) - requireCount)
					end
				end
			end
		end
	end
end

function ensurePlaceDown()
	while not turtle.placeDown() do
		print("Obstruction while placing.  Trying again ...")
		os.sleep(1)
	end
end

function copyItemSlots(batch)
	local remainingItemSlots = {}
	local cItem, cSlots
	for cItem, cSlots in pairs(batch.itemSlots) do
		local cSlotAr = {}
		local cSlotIdx, cSlot
		for cSlotIdx, cSlot in ipairs(cSlots) do
			table.insert(cSlotAr, cSlot)
		end
		remainingItemSlots[cItem] = cSlotAr
	end
	return remainingItemSlots
end

function placeBatchItem(batch, remainingItemSlots, x, y, z, cItem)
	-- Go to the position of the block
	local originX, originY, originZ = toOriginPosition(x, y + 1, z)
	-- print("placing item " .. cItem .. " at " .. x .. "," .. y .. "," .. z)
	-- print("origin position " .. originX .. "," .. originY .. "," .. originZ)
	goToPosition(originX, originY, originZ)
	-- If the block is supposed to be clear, remove it
	if cItem == nil or cItem == 0 then
		if turtle.detectDown() then aturtle.throwAwayDigDown() end
	else
		-- Get the slots that contain the item we need
		local slots = remainingItemSlots[cItem]
		if slots == nil or (# slots) == 0 then
			return
		else
			-- Find a slot that contains the item
			local slot, slotidx
			local goodSlot = nil
			local toRemove = {}
			for slotidx, slot in ipairs(slots) do
				if turtle.getItemCount(slot) == 0 then
					table.insert(toRemove, slotidx)
				else
					goodSlot = slot
					break
				end
			end
			-- Remove any slots determined to be empty
			for removeidx, removeslot in ipairs(toRemove) do
				table.remove(slots, removeslot)
			end
			-- If out of items, return
			if goodSlot == nil then
				return
			else
				-- Place the item
				turtle.select(goodSlot)
				if turtle.detectDown() then
					if not turtle.compareDown() then
						aturtle.throwAwayDigDown()
						turtle.select(goodSlot)
						ensurePlaceDown()
					end
				else
					ensurePlaceDown()
				end
			end
		end
	end
end

function placeBatch(batch)
	local x, y, z = batch.fromX, batch.fromY, batch.fromZ
	-- Create a copy of itemSlots
	local remainingItemSlots = copyItemSlots(batch)
	-- Do the batch
	while true do
		local cItem
		-- If the block item was cached from a previous call to the provider, use that.  Otherwise, ask the provider.
		local cachedBlockItem = batch.blockItems["a" .. x .. "_" .. y .. "_" .. z]
		if cachedBlockItem ~= nil then
			cItem = cachedBlockItem
		else
			cItem = provider.getBlockAt(x, y, z)
		end
		-- Skip the rest if not going to empty blocks
		if (not skipEmptyBlocks) or (cItem ~= nil and cItem ~= 0) then
			placeBatchItem(batch, remainingItemSlots, x, y, z, cItem)
		end
		-- Advance position
		x, y, z = nextPosition(x, y, z)
		if y > batch.endY then break end
	end
	-- Return the *next* coordinates
	return x, y, z
end

function placeBatchFromBlockLog(batch)
	local bLog = batch.blockLog
	local remainingItemSlots = copyItemSlots(batch)
	local bLogIdx, bLogEntry
	for bLogIdx, bLogEntry in ipairs(bLog) do
		placeBatchItem(batch, remainingItemSlots, bLogEntry.x, bLogEntry.y, bLogEntry.z, bLogEntry.item)
	end
end

function hasBatchExtraItems(batch)
	local cItem, cSlots
	for cItem, cSlots in pairs(batch.itemSlots) do
		local cSlotIdx, cSlot
		for cSlotIdx, cSlot in ipairs(cSlots) do
			if turtle.getItemCount(cSlot) > 0 then
				return true
			end
		end
	end
	return false
end

function returnBatchExtraItems(batch)
	local cItem, cSlots
	for cItem, cSlots in pairs(batch.itemSlots) do
		local cSlotIdx, cSlot
		for cSlotIdx, cSlot in ipairs(cSlots) do
			if turtle.getItemCount(cSlot) > 0 then
				local cResource = cItem
				if hasFuelChest then cResource = cResource + 1 end
				goChest(cResource)
				turtle.select(cSlot)
				turtle.dropDown()
			end
		end
	end
end

function run(provider, bx, by, bz, skipLevels)
	if provider ~= nil then
		init(provider, bx, by, bz)
	end
	discoverChests()
	if not validateNumChests() then
		return false
	end
	local x, y, z = startX, startY, startZ
	if skipLevels ~= nil then y = startY + skipLevels end
	while true do
		local batch = configureNextBatch(x, y, z)
		getItemsForBatch(batch)
		x, y, z = placeBatch(batch)
		returnBatchExtraItems(batch)
		if y >= batch.endY then
			break
		end
	end
	goHome()
	return true
end

function makeProvider(func, asizeX, asizeY, asizeZ, anumMaterials, astartX, astartY, astartZ)
	return {
		["getBlockAt"] = func,
		["sizeX"] = asizeX,
		["sizeY"] = asizeY,
		["sizeZ"] = asizeZ,
		["numMaterials"] = anumMaterials,
		["startX"] = astartX,
		["startY"] = astartY,
		["startZ"] = astartZ
	}
end






