-- POINT CONSTRUCT API/UTILITY

-- USAGE:
-- To use the point construct API, you must create a point provider object.  This object must contain the following methods:
-- getDimensions() returns the width (x), height(y), length(z) size of the construction area
-- getNumMaterials() returns the number of different materials used in the construction
-- getBlockAt(x, y, z) returns the material number (indexed from 1) of the block at that point, or nil if it should be empty

provider = nil
-- Start point of the construction.  This is the corner with the least x, y, and z values (ie, bottom front left corner)
startX = 0
startY = 0
startZ = 0
-- Size of construction area, from the start point.
sizeX = 0
sizeY = 0
sizeZ = 0
-- If true, the first chest is a fuel chest, and subsequent ones are item chests
hasFuelChest = false

skipEmptyBlocks = false

function setSkipEmptyBlocks(b)
	skipEmptyBlocks = b
end

function setHasFuelChest(b)
	hasFuelChest = b
end

function init(aprovider, astartX, astartY, astartZ)
	provider = aprovider
	aturtle.resetPosition(0, 0, 0, aturtle.DIR_POS_X)
	aturtle.setOriginNoAttackRadius(8)
	aturtle.setThrowAwaySlot(16)
	aturtle.setThrowAwayForceDigBlocks(true)
	startX, startY, startZ = astartX, astartY, astartZ
	sizeX, sizeY, sizeZ = provider.getDimensions()
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, true)
		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 discoverChests(maxZ)
	if maxZ == nil then maxZ = 16 end
	goChestLine(0)
	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 validateNumChests()
	local numChests = # resourceChests
	local reqChests = provider.getNumMaterials()
	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 startX + x, startY + y, startZ + z
end

function nextPosition(x, y, z)
	local endX = sizeX - 1
	local endY = sizeY - 1
	local endZ = sizeZ - 1
	local zDir, xDir
	local doNextX = false
	local doNextY = false
	if sizeX % 2 == 0 then
		if x % 2 == 0 then
			zDir = 1
		else
			zDir = 0-1
		end
	else
		if y % 2 == 0 then
			if x % 2 == 0 then
				zDir = 1
			else
				zDir = 0-1
			end
		else
			if x % 2 == 0 then
				zDir = 0-1
			else
				zDir = 1
			end
		end
	end
	if zDir == 1 then
		z = z + 1
		if z > endZ then
			doNextX = true
			z = endZ
		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 = 0-1
		end
		if xDir == 1 then
			x = x + 1
			if x > endX then
				doNextY = true
				x = endX
			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

-- all coords here are relative to the construction origin, not the aturtle origin
function configureNextBatch(afromX, afromY, afromZ)
	local slotItems = {}
	local slotCounts = {}
	local blockItems = {}
	local endX = sizeX - 1
	local endY = sizeY - 1
	local endZ = sizeZ - 1
	local x = afromX
	local y = afromY
	local z = afromZ
	local cItem
	local cSlot
	local itemSlot
	while true do
		cItem = provider.getBlockAt(x, y, z)
		if cItem ~= nil 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
		else
			blockItems["a" .. x .. "_" .. y .. "_" .. z] = 0
		end
		-- Increment x, y, z
		x, y, z = nextPosition(x, y, z)
		if y > endY 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
	}
	return batchSpec
end

function checkSuckDown()
	if not turtle.suckDown() then
		print("Out of items.  Add items to this chest and press enter.")
		io.read()
		checkSuckDown()
	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 placeBatch(batch)
	local x, y, z = batch.fromX, batch.fromY, batch.fromZ
	-- Create a copy of itemSlots
	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
	-- 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
			-- Go to the position of the block
			goToPosition(toOriginPosition(x, y + 1, z))
			-- 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
					break
				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
						break
					else
						-- Place the item
						turtle.select(goodSlot)
						if turtle.detectDown() then
							if not turtle.compareDown() then
								aturtle.throwAwayDigDown()
								turtle.select(goodSlot)
								turtle.placeDown()
							end
						else
							turtle.placeDown()
						end
					end
				end
			end
		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 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)
	if provider ~= nil and bx ~= nil and by ~= nil and bz ~= nil then
		init(provider, bx, by, bz)
	end
	discoverChests()
	if not validateNumChests() then
		return false
	end
	local x, y, z = 0, 0, 0
	while true do
		local batch = configureNextBatch(x, y, z)
		getItemsForBatch(batch)
		x, y, z = placeBatch(batch)
		returnBatchExtraItems(batch)
		if y >= sizeY then
			break
		end
	end
	goHome()
	return true
end








