-- This file is part of thesixthroc's Pirate Ship softmod, licensed under GPLv3 and stored at https://github.com/ComfyFactory/ComfyFactorio and https://github.com/danielmartin0/ComfyFactorio-Pirates.

local Math = require('maps.pirates.math')
-- local _inspect = require 'utils.inspect'.inspect
local simplex_noise = require('utils.math.simplex_noise').d2 --rms ~ 0.1925
-- local perlin_noise = require 'utils.math.perlin_noise'
-- local Memory = require 'maps.pirates.memory'
-- local CoreData = require 'maps.pirates.coredata'
local NoisePregen = require('maps.pirates.noise_pregen.noise_pregen')

local Public = {}

-- Lua 5.2 compatibility
-- local unpack = unpack or table.unpack

function Public.rgb_from_hsv(h, s, v)
	-- 0 ≤ H < 360, 0 ≤ S ≤ 1 and 0 ≤ V ≤ 1
	local r, g, b
	local c = v * s
	local x = c * (1 - Math.abs(((h / 60) % 2) - 1))
	local m = v - c
	if h < 60 then
		r = c + m
		g = x + m
		b = m
	elseif h < 120 then
		r = x + m
		g = c + m
		b = m
	elseif h < 180 then
		r = m
		g = c + m
		b = x + m
	elseif h < 240 then
		r = m
		g = x + m
		b = c + m
	elseif h < 300 then
		r = x + m
		g = m
		b = c + m
	else
		r = c + m
		g = m
		b = x + m
	end
	return { r = 255 * r, g = 255 * g, b = 255 * b }
end

function Public.stable_sort(list, comp) --sorts but preserves ordering of equals
	comp = comp or function(a, b)
		return a < b
	end

	local num = 0
	for k, _ in ipairs(list) do
		num = num + 1
	end

	if num <= 1 then
		return
	end

	local sorted = false
	local n = num
	while not sorted do
		sorted = true
		for i = 1, n - 1 do
			if comp(list[i + 1], list[i]) then
				local tmp = list[i]
				list[i] = list[i + 1]
				list[i + 1] = tmp

				sorted = false
			end
		end
		n = n - 1
	end
end

function Public.psum(plist)
	local totalx, totaly = 0, 0
	for i = 1, #plist do
		if plist[i][1] then --multiplier
			if plist[i][2] and plist[i][2].x and plist[i][2].y then
				totalx = totalx + plist[i][1] * plist[i][2].x
				totaly = totaly + plist[i][1] * plist[i][2].y
			end
		elseif plist[i].x and plist[i].y then
			totalx = totalx + plist[i].x
			totaly = totaly + plist[i].y
		end
	end
	return { x = totalx, y = totaly }
end

function Public.interpolate(vector1, vector2, param)
	return { x = vector1.x * (1 - param) + vector2.x * param, y = vector1.y * (1 - param) + vector2.y * param }
end

function Public.contains(table, element)
	if not table then
		return false
	end
	for _, value in pairs(table) do
		if value == element then
			return true
		end
	end
	return false
end

function Public.snap_coordinates_for_rails(p)
	return { x = p.x + (p.x % 2) - 1, y = p.y + (p.y % 2) }
end

function Public.spritepath_to_richtext(spritepath)
	return '[' .. spritepath:gsub('/', '=') .. ']'
end

function Public.nonrepeating_join_dict(t1, t2)
	t1 = t1 or {}
	t2 = t2 or {}
	local t3 = {}
	for k, i1 in pairs(t1) do
		t3[k] = i1
	end
	for k, i2 in pairs(t2) do
		t3[k] = i2
	end
	return t3
end

function Public.nonrepeating_join(t1, t2)
	t1 = t1 or {}
	t2 = t2 or {}
	local t3 = {}
	for _, i1 in pairs(t1) do
		t3[#t3 + 1] = i1
	end
	for _, i2 in pairs(t2) do
		if not Public.contains(t3, i2) then
			t3[#t3 + 1] = i2
		end
	end
	return t3
end

function Public.exclude(t, t_exclude)
	t_exclude = t_exclude or {}
	local t2 = {}
	for _, i in pairs(t) do
		if not Public.contains(t_exclude, i) then
			t2[#t2 + 1] = i
		end
	end
	return t2
end

function Public.exclude_position_arrays(a, b_exclude)
	b_exclude = b_exclude or {}
	local a2 = {}
	for x, xtab in pairs(a) do
		for y, _ in pairs(xtab) do
			if not (b_exclude[x] and b_exclude[x][y]) then
				if not a2[x] then
					a2[x] = {}
				end
				a2[x][y] = true
			end
		end
	end
	return a2
end

function Public.unordered_table_with_values_removed(tbl, val)
	local to_keep = {}
	for k, v in pairs(tbl) do
		if v ~= val then
			to_keep[k] = v
		end
	end
	return to_keep
end

function Public.ordered_table_with_values_removed(tbl, val)
	local to_keep = {}
	local j = 1
	for i = 1, #tbl do
		if tbl[i] ~= val then
			to_keep[j] = tbl[i]
			j = j + 1
		end
	end
	return to_keep
end

function Public.ordered_table_with_single_value_removed(tbl, val)
	local to_keep = {}
	local j = 1
	local taken_one = false
	for i = 1, #tbl do
		if (tbl[i] ~= val) or taken_one then
			to_keep[j] = tbl[i]
			j = j + 1
		else
			taken_one = true
		end
	end
	return to_keep
end

function Public.ordered_table_with_index_removed(tbl, index)
	local to_keep = {}
	local j = 1
	for i = 1, #tbl do
		if i ~= index then
			to_keep[j] = tbl[i]
			j = j + 1
		end
	end
	return to_keep
end

-- Implementation of table.fast_remove
function Public.fast_remove(tbl, index)
	local count = #tbl
	if index > count then
		return
	elseif index < count then
		tbl[index] = tbl[count]
	end

	tbl[count] = nil
end

function Public.length(tbl)
	local count = 0
	for k, _ in pairs(tbl) do
		count = count + 1
	end
	return count
end

function Public.standard_string_form_of_time_in_seconds(time)
	local time2, str1
	if time >= 0 then
		time2 = time
		str1 = ''
	else
		time2 = -time
		str1 = '-'
	end
	local str2
	local hours = Math.floor(Math.ceil(time2) / 3600)
	local minutes = Math.floor(Math.ceil(time2) / 60)
	local seconds = Math.ceil(time2)
	if hours >= 1 then
		str2 = string.format('%.0fh %.0fm %.0fs', hours, minutes % 60, seconds % 60)
	elseif minutes >= 1 then
		str2 = string.format('%.0fm %.0fs', minutes, seconds % 60)
	else
		str2 = string.format('%.0fs', seconds)
	end
	return str1 .. str2
end

function Public.time_longform(seconds)
	local seconds2, str1
	if seconds >= 0 then
		seconds2 = seconds
		str1 = ''
	else
		seconds2 = -seconds
		str1 = '-'
	end
	local str2
	if seconds2 < 60 - 1 then
		str2 = string.format('%.0f seconds', Math.ceil(seconds2))
	elseif seconds2 < 60 * 60 - 1 then
		str2 = string.format('%.0f mins, %.0f seconds', Math.floor(Math.ceil(seconds2) / 60), Math.ceil(seconds2) % 60)
	elseif seconds2 < 60 * 60 * 24 - 1 then
		str2 = string.format(
			'%.0f hours, %.0f mins, %.0f seconds',
			Math.floor(Math.ceil(seconds2) / (60 * 60)),
			Math.floor(Math.ceil(seconds2) / 60) % 60,
			Math.ceil(seconds2) % 60
		)
	else
		str2 = string.format(
			'%.0f days, %.0f hours, %.0f mins, %.0f seconds',
			Math.floor(Math.ceil(seconds2) / (24 * 60 * 60)),
			Math.floor(Math.ceil(seconds2) / (60 * 60)) % 24,
			Math.floor(Math.ceil(seconds2) / 60) % 60,
			Math.ceil(seconds2) % 60
		)
	end
	return str1 .. str2
end

function Public.time_mediumform(seconds)
	local seconds2, str1
	if seconds >= 0 then
		seconds2 = seconds
		str1 = ''
	else
		seconds2 = -seconds
		str1 = '-'
	end
	local str2
	local seconds3 = Math.floor(seconds2)
	if seconds3 < 60 - 1 then
		str2 = string.format('%.0fs', seconds3)
	elseif seconds3 < 60 * 60 - 1 then
		str2 = string.format('%.0fm%.0fs', Math.floor(seconds3 / 60), seconds3 % 60)
	elseif seconds3 < 60 * 60 * 24 - 1 then
		str2 = string.format(
			'%.0fh%.0fm%.0fs',
			Math.floor(seconds3 / (60 * 60)),
			Math.floor(seconds3 / 60) % 60,
			seconds3 % 60
		)
	else
		str2 = string.format(
			'%.0fd%.0fh%.0fm%.0fs',
			Math.floor(seconds3 / (24 * 60 * 60)),
			Math.floor(seconds3 / (60 * 60)) % 24,
			Math.floor(seconds3 / 60) % 60,
			seconds3 % 60
		)
	end
	return str1 .. str2
end

function Public.deepcopy(obj) --doesn't copy metatables
	if type(obj) ~= 'table' then
		return obj
	end
	local res = {}
	for k, v in pairs(obj) do
		res[Public.deepcopy(k)] = Public.deepcopy(v)
	end
	return res
end

function Public.bignumber_abbrevform(number) --e.g. 516, 1.2k, 21.4k, 137k
	local str1, str2, number2
	if number >= 0 then
		number2 = number
		str1 = ''
	else
		number2 = -number
		str1 = '-'
	end

	if number2 == 0 then
		str2 = '0'
	elseif number2 < 1000 then
		str2 = string.format('%.0d', Math.floor(number2))
	elseif number2 < 100000 then
		str2 = string.format('%.1fk', Math.floor(number2 / 100) / 10)
	else
		str2 = string.format('%.0dk', Math.floor(number2 / 1000))
	end

	return str1 .. str2
end

function Public.bignumber_abbrevform2(number) --e.g. 516, 1.2k, 21k, 137k
	local str1, str2, number2
	if number >= 0 then
		number2 = number
		str1 = ''
	else
		number2 = -number
		str1 = '-'
	end

	if number2 == 0 then
		str2 = '0'
	elseif number2 < 1000 then
		str2 = string.format('%.0d', Math.floor(number2))
	elseif number2 < 10000 then
		str2 = string.format('%.1fk', Math.floor(number2 / 100) / 10)
	else
		str2 = string.format('%.0dk', Math.floor(number2 / 1000))
	end

	return str1 .. str2
end

function Public.negative_rate_abbrevform(number)
	local str1, str2, number2
	if number > 0 then
		number2 = number
		str1 = ''
	else
		number2 = -number
		str1 = '-'
	end
	if number2 == 0 then
		str2 = '0'
	elseif number2 < 10 then
		str2 = string.format('%.1f', Math.ceil(number2 * 10) / 10)
	else
		str2 = string.format('%.0d', Math.ceil(number2))
	end

	return str1 .. str2 .. '/s'
end

function Public.noise_field_simplex_2d(noise_data, seed, normalised)
	normalised = normalised or false

	local f = function(position)
		local noise, _seed, weight_sum = 0, seed, 0
		for i = 1, #noise_data do
			local n = noise_data[i]
			local toadd = n.amplitude
			if n.wavelength > 0 then --=0 codes for infinite
				toadd = toadd * simplex_noise(position.x / n.wavelength, position.y / n.wavelength, _seed)
				_seed = _seed + 12345 --some deficiencies
			end

			if normalised then
				weight_sum = weight_sum + n.amplitude
			end
			noise = noise + toadd
		end
		if normalised then
			noise = noise / weight_sum
		end
		return noise
	end
	return f
end

function Public.hardcoded_noise_field_decompress(fieldtype, noise_data, seed, normalised)
	normalised = normalised or false

	-- local hardcoded_upperscale = NoisePregen[fieldtype].upperscale
	-- local hardcoded_boxsize = NoisePregen[fieldtype].boxsize
	local hardcoded_wordlength = NoisePregen[fieldtype].wordlength
	local factor = NoisePregen[fieldtype].factor

	local f = function(position)
		local noise, weight_sum, _seed = 0, 0, seed
		for i = 1, #noise_data do
			local n = noise_data[i]
			local toadd = n.amplitude
			local seedfactor = n.seedfactor or 1

			if n.upperscale > 0 then --=0 codes for infinite
				local scale = n.upperscale / 100

				local seed2 = seed * seedfactor

				local x2 = position.x / scale
				local y2 = position.y / scale

				local x2remainder = x2 % 1
				local y2remainder = y2 % 1

				local x2floor = x2 - x2remainder
				local y2floor = y2 - y2remainder

				local topleftnoiseindex = seed2 % (1000 * 1000)

				local relativeindex00 = x2floor + y2floor * 1000

				local totalindex00 = (topleftnoiseindex + relativeindex00) % (1000 * 1000)
				local totalindex10 = (1 + topleftnoiseindex + relativeindex00) % (1000 * 1000)
				local totalindex01 = (1000 + topleftnoiseindex + relativeindex00) % (1000 * 1000)

				local strindex00 = 1 + totalindex00 * hardcoded_wordlength
				local strindex10 = 1 + totalindex10 * hardcoded_wordlength
				local strindex01 = 1 + totalindex01 * hardcoded_wordlength

				local str00 = NoisePregen[fieldtype].Data:sub(strindex00, strindex00 + (hardcoded_wordlength - 1))
				local str10 = NoisePregen[fieldtype].Data:sub(strindex10, strindex10 + (hardcoded_wordlength - 1))
				local str01 = NoisePregen[fieldtype].Data:sub(strindex01, strindex01 + (hardcoded_wordlength - 1))
				local noise00, noise10, noise01 = 0, 0, 0
				for j = 0, hardcoded_wordlength - 1 do
					noise00 = noise00
						+ NoisePregen.dec[str00:sub(hardcoded_wordlength - j, hardcoded_wordlength - j)]
							* (NoisePregen.encoding_length ^ j)
					noise10 = noise10
						+ NoisePregen.dec[str10:sub(hardcoded_wordlength - j, hardcoded_wordlength - j)]
							* (NoisePregen.encoding_length ^ j)
					noise01 = noise01
						+ NoisePregen.dec[str01:sub(hardcoded_wordlength - j, hardcoded_wordlength - j)]
							* (NoisePregen.encoding_length ^ j)
				end

				if noise00 % 2 == 1 then
					noise00 = -noise00
				end
				noise00 = noise00 / (NoisePregen.encoding_length ^ (hardcoded_wordlength - 1))

				if noise10 % 2 == 1 then
					noise10 = -noise10
				end
				noise10 = noise10 / (NoisePregen.encoding_length ^ (hardcoded_wordlength - 1))

				if noise01 % 2 == 1 then
					noise01 = -noise01
				end
				noise01 = noise01 / (NoisePregen.encoding_length ^ (hardcoded_wordlength - 1))

				-- local hardnoise00 = tonumber(strsub00:sub(2,6))/10000
				-- if strsub00:sub(1,1) == '-' then hardnoise00 = -hardnoise00 end
				-- local hardnoise10 = tonumber(strsub10:sub(2,6))/10000
				-- if strsub10:sub(1,1) == '-' then hardnoise10 = -hardnoise10 end
				-- local hardnoise01 = tonumber(strsub01:sub(2,6))/10000
				-- if strsub01:sub(1,1) == '-' then hardnoise01 = -hardnoise01 end

				-- log(_inspect{topleftnoiseindex, topleftnoiseindex2, relativeindex00, relativeindex10, relativeindex01})
				-- log(_inspect{strindex00, strindex10, strindex01, hardnoise1, hardnoise2, hardnoise3})

				local interpolatedhardnoise = noise00
					+ x2remainder * (noise10 - noise00)
					+ y2remainder * (noise01 - noise00)

				toadd = toadd * factor * tonumber(interpolatedhardnoise)
				_seed = _seed + 12345
			end

			if normalised then
				weight_sum = weight_sum + n.amplitude
			end
			noise = noise + toadd
		end
		if normalised then
			noise = noise / weight_sum
		end
		return noise
	end
	return f
end

function Public.hardcoded_noise_field(fieldtype, noise_data, seed, normalised)
	normalised = normalised or false

	-- local hardcoded_upperscale = NoisePregen[fieldtype].upperscale --100
	-- local hardcoded_boxsize = NoisePregen[fieldtype].boxsize --1000
	local hardcoded_wordlength = NoisePregen[fieldtype].wordlength
	local factor = NoisePregen[fieldtype].factor

	local f = function(position)
		local noise, weight_sum, _seed = 0, 0, seed
		for i = 1, #noise_data do
			local n = noise_data[i]
			local toadd = n.amplitude
			local seedfactor = n.seedfactor or 1

			if n.upperscale > 0 then --=0 codes for infinite
				local scale = n.upperscale / 100

				local seed2 = seed * seedfactor

				local x2 = position.x / scale
				local y2 = position.y / scale

				local x2remainder = x2 % 1
				local y2remainder = y2 % 1

				local x2floor = x2 - x2remainder
				local y2floor = y2 - y2remainder

				local seedindex = seed2 % (1000 * 1000)

				local relativeindex00 = x2floor + y2floor * 1000

				local noiseindex1 = seedindex + relativeindex00

				local totalindex00 = noiseindex1 % (1000 * 1000)
				local totalindex10 = (1 + noiseindex1) % (1000 * 1000)
				local totalindex01 = (1000 + noiseindex1) % (1000 * 1000)

				local strindex00 = 1 + totalindex00 * hardcoded_wordlength
				local strindex10 = 1 + totalindex10 * hardcoded_wordlength
				local strindex01 = 1 + totalindex01 * hardcoded_wordlength

				local str00 = NoisePregen[fieldtype].Data:sub(strindex00, strindex00 + (hardcoded_wordlength - 1))
				local str10 = NoisePregen[fieldtype].Data:sub(strindex10, strindex10 + (hardcoded_wordlength - 1))
				local str01 = NoisePregen[fieldtype].Data:sub(strindex01, strindex01 + (hardcoded_wordlength - 1))

				local noise00 = tonumber(str00:sub(2, 6)) / 10000
				if str00:sub(1, 1) == '-' then
					noise00 = -noise00
				end
				local noise10 = tonumber(str10:sub(2, 6)) / 10000
				if str10:sub(1, 1) == '-' then
					noise10 = -noise10
				end
				local noise01 = tonumber(str01:sub(2, 6)) / 10000
				if str01:sub(1, 1) == '-' then
					noise01 = -noise01
				end

				-- log(_inspect{topleftnoiseindex, topleftnoiseindex2, relativeindex00, relativeindex10, relativeindex01})
				-- log(_inspect{strindex00, strindex10, strindex01, hardnoise1, hardnoise2, hardnoise3})

				local interpolatedhardnoise = noise00
					+ x2remainder * (noise10 - noise00)
					+ y2remainder * (noise01 - noise00)

				toadd = toadd * factor * tonumber(interpolatedhardnoise)
				_seed = _seed + 12345 --some deficiencies
			end

			if normalised then
				weight_sum = weight_sum + n.amplitude
			end
			noise = noise + toadd
		end
		if normalised then
			noise = noise / weight_sum
		end
		return noise
	end
	return f
end

function Public.noise_generator(noiseparams, seed)
	--memoizes locally
	noiseparams = noiseparams or {}
	seed = seed or 0

	local ret = {}
	for k, v in pairs(noiseparams) do
		local fn
		if v.type == 'simplex_2d' then
			fn = Public.noise_field_simplex_2d(v.params, seed, v.normalised)
		elseif v.type == 'perlin_1d_circle' then
			fn = Public.noise_field_perlin_1d_circle(v.params, seed, v.normalised)
		else
			fn = Public.hardcoded_noise_field(v.type, v.params, seed, v.normalised)
		end
		ret[k] = Public.memoize(fn)
	end

	function ret:addNoise(key, new_noise_function)
		if self[key] then
			return
		else
			self[key] = Public.memoize(new_noise_function)
		end
	end

	ret.seed = seed
	-- ret.noiseparams = noiseparams
	return ret
end

local function cache_get(cache, params)
	local node = cache
	for i = 1, #params do
		node = node.children and node.children[params[i]]
		if not node then
			return nil
		end
	end
	return node.results
end

local function cache_put(cache, params, results)
	local node = cache
	local param
	for i = 1, #params do
		param = params[i]
		node.children = node.children or {}
		node.children[param] = node.children[param] or {}
		node = node.children[param]
	end
	node.results = results
end

-- The following functions were adapted from https://github.com/kikito/memoize.lua/blob/master/memoize.lua, under the MIT License:
-- [[
--     MIT LICENSE
--     Copyright (c) 2018 Enrique García Cota
--     Permission is hereby granted, free of charge, to any person obtaining a
--     copy of this software and associated documentation files (the
--     "Software"), to deal in the Software without restriction, including
--     without limitation the rights to use, copy, modify, merge, publish,
--     distribute, sublicense, and/or sell copies of the Software, and to
--     permit persons to whom the Software is furnished to do so, subject to
--     the following conditions:
--     The above copyright notice and this permission notice shall be included
--     in all copies or substantial portions of the Software.
--     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
--     OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
--     MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
--     IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
--     CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
--     TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
--     SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--   ]]

local function is_callable(f)
	local tf = type(f)
	if tf == 'function' then
		return true
	end
	if tf == 'table' then
		local mt = getmetatable(f)
		return type(mt) == 'table' and is_callable(mt.__call)
	end
	return false
end

-- == memoization
-- memoize takes in a function and outputs an auto-memoizing version of the same
-- e.g. local memoized_f = memoize(f, <cache>), explicit cache is optional
function Public.memoize(f, cache)
	cache = cache or {}

	if not is_callable(f) then
		log(
			string.format('Only functions and callable tables are memoizable. Received %s (a %s)', tostring(f), type(f))
		)
	end

	return function(...)
		local params = { ... }

		local results = cache_get(cache, params)
		if not results then
			results = { f(...) }
			cache_put(cache, params, results)
		end

		return table.unpack(results)
	end
end

return Public
