-- Distance from center to side of the laser beam square. Not really the radius, but it sounds cooler. :P
local LASER_RADIUS = -1/16

local COLSTRING_IDS = {
	[lzr_globals.COLOR_NONE] = "_", -- nothing
	[lzr_globals.COLOR_RED] = "R", -- red
	[lzr_globals.COLOR_GREEN] = "G", -- green
	[lzr_globals.COLOR_YELLOW] = "Y", -- yellow (R+G)
	[lzr_globals.COLOR_BLUE] = "B", -- blue
	[lzr_globals.COLOR_MAGENTA] = "M", -- magenta (R+B)
	[lzr_globals.COLOR_CYAN] = "C", -- cyan (G+B)
	[lzr_globals.COLOR_WHITE] = "W", -- white (R+G+B)
}

-- The maximum possible colorcode
local MAX_COLORCODE = 7

-- DATA FORMATS:
--[[ colorcode:
A number representing a laser color.
It ranges from 0 to 7.
Value 0 stands for 'no color', the absense of a laser.
There are 3 primary colors:
* 1 = red
* 2 = green
* 4 = blue
The other values are reserved for secondary colors that
result from adding the values from 2 primary colors
that have been additive color mixing.
See lzr_globals for the full list of colorcodes.
]]

--[[ dirstring: A representation of 3 axes (XYZ) which each have
a colorcode converted to a string.
The 3 characters stand for these directions, in order: X, Y, Z
Example: "140" = red on X axis, blue on Y axis and no laser in the Z axis
]]

--[[ dirs table: Like dirstring, but in table format. It's a table
with 3 numbers, each of them is a colorcode.
The same order of axes is used like for dirstring.
Example: { 1, 4, 0 } -- X = red, Y = blue, Z = no laser
]]

-- Converts a dirstring to a dirs table
function lzr_laser.dirstring_to_dirs(dirstring)
	local dirs = {}
	for d=1, 3 do
		local dirnum = tonumber(string.sub(dirstring, d, d))
		if not dirnum then
			error("[lzr_laser] dirstring_to_dirs: Invalid dirstring! Character #"..d.." is not a digit.")
		elseif dirnum >= 0 and dirnum <= MAX_COLORCODE then
			dirs[d] = dirnum
		else
			error("[lzr_laser] dirstring_to_dirs: Invalid dirstring! Character #"..d.." is outside the valid digit value range (0-"..MAX_COLORCODE..").")
		end
	end
	return dirs
end

-- Converts a dirstring into a "colstring", a string
-- of letters each representing a color
function lzr_laser.dirstring_to_colstring(dirstring)
	local dirs = {}
	local colstr = ""
	for d=1, string.len(dirstring) do
		local dirnum = tonumber(string.sub(dirstring, d, d))
		if not dirnum then
			error("[lzr_laser] dirstring_to_dirs: Invalid dirstring! Character #"..d.." is not a digit.")
		elseif dirnum >= 0 or dirnum <= MAX_COLORCODE then
			colstr = colstr .. COLSTRING_IDS[dirnum]
		else
			error("[lzr_laser] dirstring_to_dirs: Invalid dirstring! Character #"..d.." is outside the valid digit value range (0-7).")
		end
	end
	return colstr
end



-- Converts a dirs table to a dirstring
function lzr_laser.dirs_to_dirstring(dirs)
	local dirstring = ""
	for d=1, 3 do
		if dirs[d] >= 0 or dirs[d] <= MAX_COLORCODE then
			dirstring = dirstring .. tostring(dirs[d])
		else
			error("[lzr_laser] dirs_to_dirstring: Invalid dirs!")
		end
	end
	return dirstring
end

function lzr_laser.vector_to_dirs(dir_vector)
	local dirs = {0,0,0}
	if dir_vector.x ~= 0 then
		dirs[1] = 1
	end
	if dir_vector.y ~= 0 then
		dirs[2] = 1
	end
	if dir_vector.z ~= 0 then
		dirs[3] = 1
	end
	return dirs
end

function lzr_laser.vector_and_color_to_dirs(dir_vector, color)
	local dirs = {0,0,0}
	if dir_vector.x ~= 0 then
		dirs[1] = color
	end
	if dir_vector.y ~= 0 then
		dirs[2] = color
	end
	if dir_vector.z ~= 0 then
		dirs[3] = color
	end
	return dirs
end

-- Convert number to string in binary form
-- * num: Number
-- * minLength: Minimum number of characters the string must have, will
--              fill string with leading zeroes if needed (default: 1)
-- Returns: string
function lzr_laser.dec2bin(num, minLength)
	if not minLength then
		minLength = 1
	end
	local t = {}
	local rem
	while num > 0 do
		rem = math.fmod(num,2)
		t[#t+1] = rem
		num = (num-rem)/2
	end
	local bin = table.concat(t)
	bin = string.reverse(bin)
	local length = string.len(bin)
	if length < minLength then
		bin = string.rep("0", minLength - length) .. bin
	end
	return bin
end

-- Takes 2 binary numbers (as strings!)
-- and returns the 'bitwise or' of them (also as string).
-- Both strings MUST be of equal length.
function lzr_laser.bitwise_or(bin1, bin2)
	local len = string.len(bin1)
	local out = ""
	for i=1, len do
		if string.sub(bin1, i, i) == "1" or string.sub(bin2, i, i) == "1" then
			out = out .. "1"
		else
			out = out .. "0"
		end
	end
	return out
end

-- Lookup table for dirstring_or
local colorcode_bor_lookup = {}
for i=0, MAX_COLORCODE do
	colorcode_bor_lookup[tostring(i)] = {}
	for j=0, MAX_COLORCODE do
		colorcode_bor_lookup[tostring(i)][tostring(j)] = tostring(bit.bor(i, j))
	end
end

-- Takes two dirstrings (with digits from 0 to 7)
-- and does a bitwise or on each digit on them
-- and returns the result.
-- Example: "001" and "003" give "003"
function lzr_laser.dirstring_or(dirstring1, dirstring2)
	local len = string.len(dirstring1)
	local out = ""
	for i=1, len do
		local d1 = string.sub(dirstring1, i, i)
		local d2 = string.sub(dirstring2, i, i)
		-- We use a lookup table for improved performance
		-- since this function is called for every
		-- laser step in the laser propagation
		-- algorithm.
		local res = colorcode_bor_lookup[d1][d2]
		out = out .. tostring(res)
	end
	return out
end

-- Generates a line of particles between two positions
-- * pos1, pos2: Start and end position
-- * texture: Particle texture file name
-- * amount: (optional) number of particles per step
-- * spread: (optional) how far to spread out particles from the perfect line
-- * vspread: (optional) maximum random velocity of particles
-- * size: (optional) particle size multiplier
-- * steps: (optional) how many iterations to use (more = more detailed but slower)
-- * minexptime: (optional) minimum particle expiration time
-- * maxexptime: (optional) maximum particle expiration time
-- * fade_out_time: (optional) if set, will fade out particle during its lifetime.
--                  value is between 0.0 and 1.0 for the ratio of its lifetime
--                  when the fade-out begins. Default: no fade out
function lzr_laser.particle_line(pos1, pos2, texture, amount, spread, vspread, size, steps, minexptime, maxexptime, fade_out_time)
	if not amount then amount = 10 end
	if not spread then spread = 0.1 end
	if not vspread then vspread = 0.01 end
	if not size then size = 0.4 end
	if not steps then steps = 30 end
	if not minexptime then minexptime = 1.5 end
	if not maxexptime then maxexptime = 1.7 end

	if fade_out_time then
		texture = {
			name = texture,
			alpha_tween = { 1, 0, start = fade_out_time },
		}
	end

	local pos = vector.copy(pos1)
	for i=0,steps-1 do
		pos.x = pos1.x + (pos2.x - pos1.x) * (i/steps)
		pos.y = pos1.y + (pos2.y - pos1.y) * (i/steps)
		pos.z = pos1.z + (pos2.z - pos1.z) * (i/steps)
		minetest.add_particlespawner({
			amount = amount,
			time = 0.001,
			minpos = vector.subtract(pos, vector.new(spread, spread, spread)),
			maxpos = vector.add(pos, vector.new(spread, spread, spread)),
			minvel = vector.new(-vspread, -vspread, -vspread),
			maxvel = vector.new(vspread, vspread, vspread),
			minsize = size,
			maxsize = size,
			texture = texture,
			minexptime = minexptime,
			maxexptime = maxexptime,
		})
	end
end

local dirstring_lookup = {}
function lzr_laser.laser_group_to_dirstring(laser_group)
	return dirstring_lookup[laser_group]
end
function lzr_laser.colors_to_laser_group(color_x, color_y, color_z)
	return color_x + color_y * 8 + color_z * 64
end

-- Create a lookup table for laser_group_to_distring for performance
for x=0,MAX_COLORCODE do
for y=0,MAX_COLORCODE do
for z=0,MAX_COLORCODE do
	local val = lzr_laser.colors_to_laser_group(x,y,z)
	dirstring_lookup[val] = x .. y .. z
end
end
end

-- Returns true if the given laser block (by nodename)
-- is active.
function lzr_laser.is_laser_block_active(nodename)
	local def = minetest.registered_nodes[nodename]
	if not def then
		return false
	end
	local element_group = def._lzr_element_group
	if not element_group then
		return false
	end
	if minetest.get_item_group(nodename, element_group) == 2 then
		return true
	else
		return false
	end
end

-- Convert a 'pos string' to a list of positions.
-- A 'pos string' is a string containing of positions,
-- each in the `minetest.pos_to_string` format, and each
-- position is separated by a semicolon.
lzr_laser.pos_string_to_positions = function(pos_str)
	local strs = string.split(pos_str, ";")
	local posses  = {}
	if strs then
		for s=1, #strs do
			local stpos = minetest.string_to_pos(strs[s])
			if not stpos then
				minetest.log("error", "[lzr_laser] Invalid pos_string syntax of node at "..minetest.pos_to_string(stpos)..": "..tostring(pos_str))
				return {}
			end
			table.insert(posses, stpos)
		end
	end
	return posses
end

-- Convert a list of positions to a 'pos string'.
-- See `lzr_laser.pos_string_to_positions` for a definition
-- of 'pos string'.
lzr_laser.positions_to_pos_string = function(positions)
	local to_str = ""
	for p=1, #positions do
		to_str = to_str .. minetest.pos_to_string(positions[p])
		if p < #positions then
			to_str = to_str .. ";"
		end
	end
	return to_str
end

