--[[
	Copyright (C) 2021 random-geek (https://github.com/random-geek)
	Luanti (Minetest): Copyright (C) 2010-2021 celeron55, Perttu Ahola <celeron55@gmail.com>

	This file is part of Meshport.

	Meshport is free software: you can redistribute it and/or modify it under
	the terms of the GNU Lesser General Public License as published by the Free
	Software Foundation, either version 3 of the License, or (at your option)
	any later version.

	Meshport is distributed in the hope that it will be useful, but WITHOUT ANY
	WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
	FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
	more details.

	You should have received a copy of the GNU Lesser General Public License
	along with Meshport. If not, see <https://www.gnu.org/licenses/>.
]]

-- Much of the mesh generation code in this file is derived from Luanti's (Minetest's)
-- MapblockMeshGenerator class. See minetest/src/client/content_mapblock.cpp.

local S = meshport.S
local vec = vector.new -- Makes defining tables of vertices a little less painful.

--[[
	THE CUBIC NODE PRIORITY SYSTEM

	For each face on each cubic node, Meshport decides whether or not to draw
	that face based on a combination of the current node's drawtype (show in
	the top row of the table below), the neighboring node's drawtype (shown in
	the leftmost column), the direction of the face, and both nodes'
	visual_scale.

	A "YES" combination means the face is drawn, "no" means the face is not
	drawn, and "Offset" means the face is drawn, but slightly inset to avoid
	duplication of faces.

	|  This node => | allfaces (1) | glasslike | liquid  | normal (2) |
	|--------------:|:------------:|:---------:|:-------:|:----------:|
	| air/non-cubic | YES          | YES       | YES (3) | YES        |
	|      allfaces | (4)          | YES       | YES     | YES        |
	|     glasslike | Offset       | (5)       | YES     | YES        |
	|        liquid | Offset       | Offset    | no      | YES        |
	|    normal (2) | no           | no        | no      | no         |

	1. Allfaces faces are always drawn if `visual_scale` is not 1.
	2. The base of `plantlike_rooted` is treated as a normal node.
	3. Liquid faces are not drawn bordering a corresponding flowing liquid.
	4. Only drawn if facing X+, Y+, or Z+, or if either node's `visual_scale`
	   is not 1.
	5. Only drawn if the nodes are different. X-, Z-, and Y- faces are offset.
]]

local CUBIC_FACE_PRIORITY = {
	allfaces = 1,
	glasslike = 2,
	liquid = 3,
	normal = 4,
	plantlike_rooted = 4, -- base of plantlike_rooted is equivalent to `normal`.
}

local CUBIC_SIDE_FACES = {
	{vec(-0.5,  0.5, -0.5), vec( 0.5,  0.5, -0.5), vec( 0.5,  0.5,  0.5), vec(-0.5,  0.5,  0.5)}, -- Y+
	{vec(-0.5, -0.5,  0.5), vec( 0.5, -0.5,  0.5), vec( 0.5, -0.5, -0.5), vec(-0.5, -0.5, -0.5)}, -- Y-
	{vec( 0.5, -0.5, -0.5), vec( 0.5, -0.5,  0.5), vec( 0.5,  0.5,  0.5), vec( 0.5,  0.5, -0.5)}, -- X+
	{vec(-0.5, -0.5,  0.5), vec(-0.5, -0.5, -0.5), vec(-0.5,  0.5, -0.5), vec(-0.5,  0.5,  0.5)}, -- X-
	{vec( 0.5, -0.5,  0.5), vec(-0.5, -0.5,  0.5), vec(-0.5,  0.5,  0.5), vec( 0.5,  0.5,  0.5)}, -- Z+
	{vec(-0.5, -0.5, -0.5), vec( 0.5, -0.5, -0.5), vec( 0.5,  0.5, -0.5), vec(-0.5,  0.5, -0.5)}, -- Z-
}


-- For normal, plantlike_rooted, and liquid drawtypes
local function create_cubic_node(pos, content, param2, nodeDef, drawtype, neighbors)
	local facedir = meshport.get_facedir(nodeDef.paramtype2, param2)
	local selfPriority = CUBIC_FACE_PRIORITY[drawtype]
	-- If the current node is a liquid, get the flowing version of it.
	local flowingLiquid = drawtype == "liquid"
		and meshport.get_content_id_or_nil(nodeDef.liquid_alternative_flowing) or nil

	local faces = meshport.Faces:new()

	for i = 1, 6 do
		local drawFace

		if neighbors[i] == core.CONTENT_AIR then
			drawFace = true
		elseif neighbors[i] == core.CONTENT_IGNORE
				-- Don't draw faces between identical nodes
				or neighbors[i] == content
				-- Don't draw liquid faces bordering a corresponding flowing liquid
				or neighbors[i] == flowingLiquid then
			drawFace = false
		else
			local neighborDef = meshport.get_def_from_id(neighbors[i])
			local neighborDrawtype = meshport.get_aliased_drawtype(neighborDef.drawtype)
			drawFace = selfPriority > (CUBIC_FACE_PRIORITY[neighborDrawtype] or 0)
		end

		if drawFace then
			local norm = meshport.NEIGHBOR_DIRS[i]

			faces:insert_face(meshport.prepare_cuboid_face({
				verts = table.copy(CUBIC_SIDE_FACES[i]),
				vert_norms = {norm, norm, norm, norm},
				tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
			}, nodeDef.tiles, pos, facedir, i))
		end
	end

	return faces
end


-- For allfaces and glasslike drawtypes, and equivalent variants.
local function create_special_cubic_node(pos, content, nodeDef, drawtype, neighbors)
	local selfPriority = CUBIC_FACE_PRIORITY[drawtype]
	local isAllfaces = drawtype == "allfaces"
	local allfacesScale = isAllfaces and nodeDef.visual_scale or 1

	local faces = meshport.Faces:new()

	for i = 1, 6 do
		local drawFace
		local inset = false

		if allfacesScale ~= 1 or neighbors[i] == core.CONTENT_AIR or neighbors[i] == core.CONTENT_IGNORE then
			drawFace = true
		elseif neighbors[i] == content then
			drawFace = isAllfaces and i % 2 == 1
		else
			local neighborDef = meshport.get_def_from_id(neighbors[i])
			local neighborDrawtype = meshport.get_aliased_drawtype(neighborDef.drawtype)
			local neighborPriority = CUBIC_FACE_PRIORITY[neighborDrawtype] or 0

			if neighborPriority < selfPriority then
				drawFace = true
			elseif neighborPriority >= 4 then
				-- Don't draw faces bordering normal nodes.
				drawFace = false
			elseif neighborPriority > selfPriority then
				drawFace = true
				inset = true
			elseif isAllfaces then -- neighborPriority == selfPriority
				drawFace = i % 2 == 1 or neighborDef.visual_scale ~= 1
			else -- neighborPriority == selfPriority
				drawFace = true
				inset = i % 2 == 0
			end
		end

		if drawFace then
			local verts = table.copy(CUBIC_SIDE_FACES[i])

			if inset then
				local offset = vector.multiply(meshport.NEIGHBOR_DIRS[i], -0.003)
				for j, vert in ipairs(verts) do
					verts[j] = vector.add(vert, offset)
				end
			end

			local norm = meshport.NEIGHBOR_DIRS[i]
			faces:insert_face(meshport.prepare_cuboid_face({
				verts = verts,
				vert_norms = {norm, norm, norm, norm},
				tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
				tile_idx = 1, -- Only the first tile is used.
			}, nodeDef.tiles, pos, 0, i))
		end
	end

	faces:scale(allfacesScale)
	return faces
end


local GLASSLIKE_FRAMED_CONSTANTS = (function()
	local a = 0.5
	local g = 0.5 - 0.003
	local b = 0.876 * 0.5

	return {
		G = g,
		B = b,
		FRAME_EDGES = {
			{ b,  b, -a,  a,  a,  a}, -- Y+ / X+
			{-a,  b, -a, -b,  a,  a}, -- Y+ / X-
			{ b, -a, -a,  a, -b,  a}, -- Y- / X+
			{-a, -a, -a, -b, -b,  a}, -- Y- / X-
			{ b, -a,  b,  a,  a,  a}, -- X+ / Z+
			{ b, -a, -a,  a,  a, -b}, -- X+ / Z-
			{-a, -a,  b, -b,  a,  a}, -- X- / Z+
			{-a, -a, -a, -b,  a, -b}, -- X- / Z-
			{-a,  b,  b,  a,  a,  a}, -- Z+ / Y+
			{-a, -a,  b,  a, -b,  a}, -- Z+ / Y-
			{-a,  b, -a,  a,  a, -b}, -- Z- / Y+
			{-a, -a, -a,  a, -b, -b}, -- Z- / Y-
		},
		GLASS_FACES = {
			{vec(-a,  g, -a), vec( a,  g, -a), vec( a,  g,  a), vec(-a,  g,  a)}, -- Y+
			{vec(-a, -g,  a), vec( a, -g,  a), vec( a, -g, -a), vec(-a, -g, -a)}, -- Y-
			{vec( g, -a, -a), vec( g, -a,  a), vec( g,  a,  a), vec( g,  a, -a)}, -- X+
			{vec(-g, -a,  a), vec(-g, -a, -a), vec(-g,  a, -a), vec(-g,  a,  a)}, -- X-
			{vec( a, -a,  g), vec(-a, -a,  g), vec(-a,  a,  g), vec( a,  a,  g)}, -- Z+
			{vec(-a, -a, -g), vec( a, -a, -g), vec( a,  a, -g), vec(-a,  a, -g)}, -- Z-
		},
		EDGE_NEIGHBORS = {
			{1, 3,  8}, {1, 4,  7}, {2, 3, 16}, {2, 4, 15},
			{3, 5, 12}, {3, 6, 14}, {4, 5, 11}, {4, 6, 13},
			{5, 1,  9}, {5, 2, 17}, {6, 1, 10}, {6, 2, 18},
		},
	}
end)()


local function create_glasslike_framed_node(pos, param2, nodeDef, area, vContent)
	local idx = area:indexp(pos)
	local llParam2 = nodeDef.paramtype2 == "glasslikeliquidlevel" and param2 or 0
	local hMerge = llParam2 < 128 -- !(param2 & 128)
	local vMerge = llParam2 % 128 < 64 -- !(param2 & 64)
	local intLevel = llParam2 % 64

	-- Localize constants
	local G, B, FRAME_EDGES, GLASS_FACES, EDGE_NEIGHBORS = (function(c)
		return c.G, c.B, c.FRAME_EDGES, c.GLASS_FACES, c.EDGE_NEIGHBORS
	end)(GLASSLIKE_FRAMED_CONSTANTS)

	local neighbors = {
		false, false, false, false, false, false, false, false, false,
		false, false, false, false, false, false, false, false, false
	}

	if hMerge or vMerge then
		for i = 1, 18 do
			local dir = meshport.NEIGHBOR_DIRS[i]
			if (hMerge or (dir.x == 0 and dir.z == 0)) and (vMerge or dir.y == 0) then
				local nIdx = area:indexp(vector.add(pos, dir))
				neighbors[i] = vContent[nIdx] == vContent[idx]
			end
		end
	end

	local boxes = meshport.Boxes:new()

	for i = 1, 12 do
		local edgeVisible
		local touching = EDGE_NEIGHBORS[i]

		if neighbors[touching[3]] then
			edgeVisible = not (neighbors[touching[1]] and neighbors[touching[2]])
		else
			edgeVisible = neighbors[touching[1]] == neighbors[touching[2]]
		end

		if edgeVisible then
			boxes:insert_box(FRAME_EDGES[i])
		end
	end

	local faces = boxes:to_faces(nodeDef, pos, 0, 1)

	for i = 1, 6 do
		if not neighbors[i] then
			local norm = meshport.NEIGHBOR_DIRS[i]

			faces:insert_face({
				verts = table.copy(GLASS_FACES[i]),
				vert_norms = {norm, norm, norm, norm},
				tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
				tile_idx = 2,
			})
		end
	end

	if intLevel > 0 and nodeDef.special_tiles and nodeDef.special_tiles[1] then
		local level = intLevel / 63 * 2 - 1
		local liquidBoxes = meshport.Boxes:new()
		liquidBoxes:insert_box({
			-(neighbors[4] and G or B),
			-(neighbors[2] and G or B),
			-(neighbors[6] and G or B),
			(neighbors[3] and G or B),
			(neighbors[1] and G or B) * level,
			(neighbors[5] and G or B)
		})
		faces:insert_all(liquidBoxes:to_faces(nodeDef, pos, 0, 1, true))
	end

	return faces
end


local FLOWING_LIQUID_CONSTANTS = {
	SIDE_DIRS = {vec(1, 0, 0), vec(-1, 0, 0), vec(0, 0, 1), vec(0, 0, -1)},
	SIDE_CORNERS = {
		{{x = 1, z = 1}, {x = 1, z = 0}}, -- X+
		{{x = 0, z = 0}, {x = 0, z = 1}}, -- X-
		{{x = 0, z = 1}, {x = 1, z = 1}}, -- Z+
		{{x = 1, z = 0}, {x = 0, z = 0}}, -- Z-
	},
}


local function create_flowing_liquid_node(pos, nodeDef, area, vContent, vParam2)
	local cSource = meshport.get_content_id_or_nil(nodeDef.liquid_alternative_source)
	local cFlowing = meshport.get_content_id_or_nil(nodeDef.liquid_alternative_flowing)
	local range = math.min(math.max(meshport.get_def_from_id(cFlowing).liquid_range or 8, 1), 8)

	--[[ Step 1: Gather neighbor data ]]
	local neighbors = {[-1] = {}, [0] = {}, [1] = {}}

	for dz = -1, 1 do
		for dx = -1, 1 do
			local nPos = vector.add(pos, vector.new(dx, 0, dz))
			local nIdx = area:indexp(nPos)

			neighbors[dz][dx] = {
				content = vContent[nIdx],
				level = -0.5,
				is_same_liquid = false,
				top_is_same_liquid = false,
			}
			local nData = neighbors[dz][dx]

			if vContent[nIdx] ~= core.CONTENT_IGNORE then
				if vContent[nIdx] == cSource then
					nData.is_same_liquid = true
					nData.level = 0.5
				elseif vContent[nIdx] == cFlowing then
					nData.is_same_liquid = true
					local intLevel = math.max(vParam2[nIdx] % 8 - 8 + range, 0)
					nData.level = -0.5 + (intLevel + 0.5) / range
				end

				local tPos = vector.add(nPos, vector.new(0, 1, 0))
				local tIdx = area:indexp(tPos)
				if vContent[tIdx] == cSource or vContent[tIdx] == cFlowing then
					nData.top_is_same_liquid = true
				end
			end
		end
	end

	--[[ Step 2: Determine level at each corner ]]
	local cornerLevels = {[0] = {[0] = 0, 0}, {[0] = 0, 0}}

	local function get_corner_level(cx, cz)
		local sum = 0
		local count = 0
		local airCount = 0

		for dz = -1, 0 do
			for dx = -1, 0 do
				local nData = neighbors[cz + dz][cx + dx]

				if nData.top_is_same_liquid or nData.content == cSource then
					return 0.5
				elseif nData.content == cFlowing then
					sum = sum + nData.level
					count = count + 1
				elseif nData.content == core.CONTENT_AIR then
					airCount = airCount + 1

					if airCount >= 2 then
						return -0.5 + 0.02
					end
				end
			end
		end

		if count > 0 then
			return sum / count
		end

		return 0
	end

	for cz = 0, 1 do
		for cx = 0, 1 do
			cornerLevels[cz][cx] = get_corner_level(cx, cz)
		end
	end

	--[[ Step 3: Actually create the liquid mesh ]]
	local faces = meshport.Faces:new()

	-- Localize constants
	local SIDE_DIRS, SIDE_CORNERS = (function(c)
		return c.SIDE_DIRS, c.SIDE_CORNERS
	end)(FLOWING_LIQUID_CONSTANTS)

	-- Add side faces
	local sideVerts = {
		{vec( 0.5, 0.5,  0.5), vec( 0.5, 0.5, -0.5), vec( 0.5, -0.5, -0.5), vec( 0.5, -0.5,  0.5)}, -- X+
		{vec(-0.5, 0.5, -0.5), vec(-0.5, 0.5,  0.5), vec(-0.5, -0.5,  0.5), vec(-0.5, -0.5, -0.5)}, -- X-
		{vec(-0.5, 0.5,  0.5), vec( 0.5, 0.5,  0.5), vec( 0.5, -0.5,  0.5), vec(-0.5, -0.5,  0.5)}, -- Z+
		{vec( 0.5, 0.5, -0.5), vec(-0.5, 0.5, -0.5), vec(-0.5, -0.5, -0.5), vec( 0.5, -0.5, -0.5)}, -- Z-
	}

	local function need_side(dir)
		local neighbor = neighbors[dir.z][dir.x]
		if neighbor.is_same_liquid
				and (not neighbors[0][0].top_is_same_liquid or neighbor.top_is_same_liquid) then
			return false
		end

		local nContent = neighbors[dir.z][dir.x].content
		local drawtype = meshport.get_aliased_drawtype(meshport.get_def_from_id(nContent).drawtype)
		if (CUBIC_FACE_PRIORITY[drawtype] or 0) >= 4 then
			return false -- Don't draw bordering normal nodes
		end

		return true
	end

	for i = 1, 4 do
		local dir = SIDE_DIRS[i]

		if need_side(dir) then
			local verts = sideVerts[i]
			local sideTexCoords = {{x = 1, y = 1}, {x = 0, y = 1}, {x = 0, y = 0}, {x = 1, y = 0}}

			if not neighbors[0][0].top_is_same_liquid then -- If there's liquid above, default to a full block.
				local corners = SIDE_CORNERS[i]

				for j = 1, 2 do
					local corner = cornerLevels[corners[j].z][corners[j].x]
					verts[j].y = corner
					sideTexCoords[j].y = corner + 0.5
				end
			end

			faces:insert_face({
				verts = verts,
				vert_norms = {dir, dir, dir, dir},
				tex_coords = sideTexCoords,
				tile_idx = 2,
				use_special_tiles = true,
			})
		end
	end

	-- Add top faces
	if not neighbors[0][0].top_is_same_liquid then -- Check node above the current node
		local verts = {
			vec( 0.5, cornerLevels[0][1], -0.5),
			vec( 0.5, cornerLevels[1][1],  0.5),
			vec(-0.5, cornerLevels[1][0],  0.5),
			vec(-0.5, cornerLevels[0][0], -0.5),
		}

		local norm1 = vector.normalize(vector.cross(
			vector.subtract(verts[1], verts[2]),
			vector.subtract(verts[3], verts[2])
		))
		local norm2 = vector.normalize(vector.cross(
			vector.subtract(verts[3], verts[4]),
			vector.subtract(verts[1], verts[4])
		))

		local dz = (cornerLevels[0][0] + cornerLevels[0][1]) -
			(cornerLevels[1][0] + cornerLevels[1][1])
		local dx = (cornerLevels[0][0] + cornerLevels[1][0]) -
			(cornerLevels[0][1] + cornerLevels[1][1])
		local textureAngle = -math.atan2(dz, dx)

		-- Get texture coordinate offset based on position.
		local tx, ty = pos.z, -pos.x
		-- Rotate offset around (0, 0) by textureAngle.
		-- Then isolate the fractional part, since the texture is tiled anyway.
		local sinTA = math.sin(textureAngle)
		local cosTA = math.cos(textureAngle)
		local textureOffset = {
			x = (tx * cosTA - ty * sinTA) % 1,
			y = (tx * sinTA + ty * cosTA) % 1
		}

		faces:insert_face({
			verts = {verts[1], verts[2], verts[3]},
			vert_norms = {norm1, norm1, norm1},
			tex_coords = meshport.translate_texture_coordinates(
				meshport.rotate_texture_coordinates_rad(
					{{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}},
					textureAngle
				),
				textureOffset
			),
			tile_idx = 1,
			use_special_tiles = true,
		})
		faces:insert_face({
			verts = {verts[3], verts[4], verts[1]},
			vert_norms = {norm2, norm2, norm2},
			tex_coords = meshport.translate_texture_coordinates(
				meshport.rotate_texture_coordinates_rad(
					{{x = 1, y = 1}, {x = 0, y = 1}, {x = 0, y = 0}},
					textureAngle
				),
				textureOffset
			),
			tile_idx = 1,
			use_special_tiles = true,
		})
	end

	-- Add bottom face
	local function need_liquid_bottom()
		local bContent = vContent[area:indexp(vector.add(pos, vector.new(0, -1, 0)))]
		if bContent == cSource or bContent == cFlowing then
			return false
		end

		local drawtype = meshport.get_aliased_drawtype(meshport.get_def_from_id(bContent).drawtype)
		if (CUBIC_FACE_PRIORITY[drawtype] or 0) >= 4 then
			return false -- Don't draw bordering normal nodes
		end

		return true
	end

	if need_liquid_bottom() then
		local norm = vector.new(0, -1, 0)

		faces:insert_face({
			verts = {
				vec(-0.5, -0.5,  0.5),
				vec( 0.5, -0.5,  0.5),
				vec( 0.5, -0.5, -0.5),
				vec(-0.5, -0.5, -0.5),
			},
			vert_norms = {norm, norm, norm, norm},
			tex_coords = {{x = 0, y = 0}, {x = 1, y = 0}, {x = 1, y = 1}, {x = 0, y = 1}},
			tile_idx = 1,
			use_special_tiles = true,
		})
	end

	return faces
end


local function create_nodebox_node(pos, content, param2, neighbors)
	local nodeName = core.get_name_from_content_id(content)
	local nodeDef = core.registered_nodes[nodeName]

	if not meshport.nodebox_cache[nodeName] then
		meshport.nodebox_cache[nodeName] = meshport.prepare_nodebox(nodeDef.node_box)
	end

	local facedir = meshport.get_facedir(nodeDef.paramtype2, param2)
	local boxes = meshport.collect_boxes(meshport.nodebox_cache[nodeName], nodeDef, param2, facedir, neighbors)

	if meshport.nodebox_cache[nodeName].type ~= "connected" then
		boxes:rotate_by_facedir(facedir)
	end

	return boxes:to_faces(nodeDef, pos, facedir)
end


local function create_mesh_node(nodeDef, param2, playerName)
	local meshName = nodeDef.mesh

	if not meshName then
		return
	end

	if not meshport.mesh_cache[meshName] then
		-- Get the paths of all .obj meshes.
		if not meshport.obj_paths then
			meshport.obj_paths = meshport.get_obj_paths()
		end

		if not meshport.obj_paths[meshName] then
			if string.lower(string.sub(meshName, -4)) ~= ".obj" then
				meshport.log(playerName, "warning", S("Mesh \"@1\" is not supported.", meshName))
			else
				meshport.log(playerName, "warning", S("Mesh \"@1\" could not be found.", meshName))
			end

			-- Cache a blank faces object so the player isn't warned again.
			meshport.mesh_cache[meshName] = meshport.Faces:new()
		else
			-- TODO: pcall this in case of failure
			local meshFaces = meshport.parse_obj(meshport.obj_paths[meshName])
			meshFaces:scale(nodeDef.visual_scale)
			meshport.mesh_cache[meshName] = meshFaces
		end
	end

	local faces = meshport.mesh_cache[meshName]:copy()

	local facedir = meshport.get_facedir(nodeDef.paramtype2, param2)
	faces:rotate_by_facedir(facedir)

	local rotation = meshport.get_degrotate(nodeDef.paramtype2, param2)
	faces:rotate_xz_degrees(rotation)

	return faces
end


-- Plant rotation is slightly different from normal wallmounted rotation.
local PLANTLIKE_WALLMOUNTED_TO_FACEDIR = {[0] = 20, 0, 16, 14, 11, 5}


local function create_plantlike_node(pos, param2, nodeDef)
	local isRooted = nodeDef.drawtype == "plantlike_rooted"
	local style = 0
	local height = 1.0
	local scale = 0.5 * nodeDef.visual_scale
	local rotation = meshport.get_degrotate(nodeDef.paramtype2, param2)
	local offset = vector.new(0, 0, 0)
	local randomOffsetY = false
	local faceNum = 0

	local faces = meshport.Faces:new()

	if isRooted then
		-- Place plant above the center node.
		offset.y = 1
	end

	if nodeDef.paramtype2 == "meshoptions" then
		style = param2 % 8

		if param2 % 16 >= 8 then -- param2 & 8
			-- TODO: Use MT's seed generators
			local seed = (pos.x % 0xFF) * 0x100 + (pos.z % 0xFF) + (pos.y % 0xFF) * 0x10000
			local rng = PseudoRandom(seed)
			offset.x = ((rng:next() % 16) / 16) * 0.29 - 0.145
			offset.z = ((rng:next() % 16) / 16) * 0.29 - 0.145
		end

		if param2 % 32 >= 16 then -- param2 & 16
			scale = scale * 1.41421
		end

		if param2 % 64 >= 32 then -- param2 & 32
			randomOffsetY = true
		end
	elseif nodeDef.paramtype2 == "leveled" then
		height = param2 / 16
		if height == 0 then
			-- No height, no plant!
			-- But seriously, zero-area faces cause problems with Blender.
			return faces
		end
	end

	local function create_plantlike_quad(faceRotation, topOffset, bottomOffset)
		-- Use Faces, even though it's just one face.
		local face = meshport.Faces:new()
		local plantHeight = 2.0 * scale * height
		local norm = vector.normalize(vector.new(0, bottomOffset - topOffset, plantHeight))

		face:insert_face({
			verts = {
				vec(-scale, -0.5 + plantHeight, topOffset),
				vec( scale, -0.5 + plantHeight, topOffset),
				vec( scale, -0.5, bottomOffset),
				vec(-scale, -0.5, bottomOffset),
			},
			tex_coords = {{x = 0, y = 1}, {x = 1, y = 1}, {x = 1, y = 1 - height}, {x = 0, y = 1 - height}},
			vert_norms = {norm, norm, norm, norm},
			tile_idx = 0,
			use_special_tiles = isRooted,
		})
		face:rotate_xz_degrees(faceRotation + rotation)

		local faceOffset = vector.new(offset)
		if randomOffsetY then
			local seed = faceNum + (pos.x % 0xFF) * 0x10000 + (pos.z % 0xFF) * 0x100 + (pos.y % 0xFF) * 0x1000000
			local yRng = PseudoRandom(seed)
			faceOffset.y = faceOffset.y - ((yRng:next() % 16) / 16 * 0.125)
			faceNum = faceNum + 1
		end

		face:translate(faceOffset)
		return face
	end

	if style == 0 then
		faces:insert_all(create_plantlike_quad(46, 0, 0))
		faces:insert_all(create_plantlike_quad(-44, 0, 0))
	elseif style == 1 then
		faces:insert_all(create_plantlike_quad(91, 0, 0))
		faces:insert_all(create_plantlike_quad(1, 0, 0))
	elseif style == 2 then
		faces:insert_all(create_plantlike_quad(121, 0, 0))
		faces:insert_all(create_plantlike_quad(241, 0, 0))
		faces:insert_all(create_plantlike_quad(1, 0, 0))
	elseif style == 3 then
		faces:insert_all(create_plantlike_quad(1, 0.25, 0.25))
		faces:insert_all(create_plantlike_quad(91, 0.25, 0.25))
		faces:insert_all(create_plantlike_quad(181, 0.25, 0.25))
		faces:insert_all(create_plantlike_quad(271, 0.25, 0.25))
	elseif style == 4 then
		faces:insert_all(create_plantlike_quad(1, -0.5, 0))
		faces:insert_all(create_plantlike_quad(91, -0.5, 0))
		faces:insert_all(create_plantlike_quad(181, -0.5, 0))
		faces:insert_all(create_plantlike_quad(271, -0.5, 0))
	end

	-- TODO: Support facedir if added.
	if nodeDef.paramtype2 == "wallmounted" or nodeDef.paramtype2 == "colorwallmounted" then
		local facedir = PLANTLIKE_WALLMOUNTED_TO_FACEDIR[param2 % 8] or 0
		faces:rotate_by_facedir(facedir)
	end

	return faces
end


local function create_node(idx, area, vContent, vParam2, playerName)
	if vContent[idx] == core.CONTENT_AIR
			or vContent[idx] == core.CONTENT_IGNORE
			or vContent[idx] == core.CONTENT_UNKNOWN then -- TODO: Export unknown nodes?
		return
	end

	local nodeDef = meshport.get_def_from_id(vContent[idx])
	if nodeDef.drawtype == "airlike" then
		return
	end

	local pos = area:position(idx)
	local nodeDrawtype = meshport.get_aliased_drawtype(nodeDef.drawtype)
	local neighbors, faces

	if CUBIC_FACE_PRIORITY[nodeDrawtype] or nodeDrawtype == "nodebox" then
		neighbors = meshport.get_node_neighbors(vContent, area, idx)
	end

	if (CUBIC_FACE_PRIORITY[nodeDrawtype] or 0) >= 3 then -- liquid, normal, plantlike_rooted
		faces = create_cubic_node(pos, vContent[idx], vParam2[idx], nodeDef, nodeDrawtype, neighbors)

		if nodeDrawtype == "plantlike_rooted" then
			local plantPos = vector.add(pos, vector.new(0, 1, 0))
			local plantFaces = create_plantlike_node(plantPos, vParam2[idx], nodeDef)
			faces:insert_all(plantFaces)
		end
	elseif CUBIC_FACE_PRIORITY[nodeDrawtype] then -- Any other cubic nodes (allfaces, glasslike)
		faces = create_special_cubic_node(pos, vContent[idx], nodeDef, nodeDrawtype, neighbors)
	elseif nodeDrawtype == "glasslike_framed" then
		faces = create_glasslike_framed_node(pos, vParam2[idx], nodeDef, area, vContent)
	elseif nodeDrawtype == "flowingliquid" then
		faces = create_flowing_liquid_node(pos, nodeDef, area, vContent, vParam2)
	elseif nodeDrawtype == "nodebox" then
		faces = create_nodebox_node(pos, vContent[idx], vParam2[idx], neighbors)
	elseif nodeDrawtype == "mesh" then
		faces = create_mesh_node(nodeDef, vParam2[idx], playerName)
	elseif nodeDrawtype == "plantlike" then
		faces = create_plantlike_node(pos, vParam2[idx], nodeDef)
	end

	if not faces then
		return
	end

	faces:apply_tiles(nodeDef)
	return faces
end


local function initialize_resources()
	meshport.texture_paths = meshport.get_texture_paths()
	meshport.texture_dimension_cache = {}
	-- meshport.obj_paths is only loaded if needed
	meshport.nodebox_cache = {}
	meshport.mesh_cache = {}
end


local function cleanup_resources()
	meshport.texture_paths = nil
	meshport.texture_dimension_cache = nil
	meshport.obj_paths = nil
	meshport.nodebox_cache = nil
	meshport.mesh_cache = nil
end


function meshport.create_mesh(playerName, p1, p2, path)
	meshport.log(playerName, "info", S("Generating mesh..."))
	initialize_resources()

	p1, p2 = vector.sort(p1, p2)
	local vm = core.get_voxel_manip()

	-- Add one node of padding to area so we can read neighbor blocks.
	local vp1, vp2 = vm:read_from_map(vector.subtract(p1, 1), vector.add(p2, 1))
	local vContent = vm:get_data()
	local vParam2 = vm:get_param2_data()

	-- Create a VoxelArea for converting from flat array indices to position vectors.
	local vArea = VoxelArea:new{MinEdge = vp1, MaxEdge = vp2}
	local meshOrigin = vector.subtract(p1, 0.5)
	local mesh = meshport.Mesh:new()

	-- Loop through all positions in the desired area.
	for idx in vArea:iterp(p1, p2) do
		-- Generate a mesh for the node.
		local faces = create_node(idx, vArea, vContent, vParam2, playerName)

		if faces then
			-- Move the node to its proper position.
			faces:translate(vector.subtract(vArea:position(idx), meshOrigin))

			-- Add faces to our final mesh.
			mesh:insert_faces(faces)
		end
	end

	core.mkdir(path)
	mesh:write_obj(path)
	mesh:write_mtl(path, playerName)

	cleanup_resources()
	meshport.log(playerName, "info", S("Finished. Saved to @1", path))
end
