local C = ffi.C

_CALLOCED = 0
_VALLOCED = 0

lfs = {}
sfx = {}
afx = {}
mth = {}
lwindow = {}
fonts = {
	inconsolata = { sizes = {}, file = "fonts/inconsolata.ttf" },
	interfont = { sizes = {}, file = "fonts/interfont.ttf" },
	mplus = { sizes = {}, file = "fonts/inconsolata.ttf" },
}

print_orig = print


-- sokol procs
ffi.cdef([[
  uint64_t stm_now(void);
  double stm_sec(uint64_t ticks);
  double sapp_frame_duration();
  void sapp_request_quit();
  void sapp_lock_mouse(bool lock);
  void setWindowSize(int w, int h);

  void sgl_scissor_rectf(float x, float y, float w, float h, bool origin_top_left);
    void sgl_push_matrix(void);
  void sgl_pop_matrix(void);
  void sgl_rotate(float angle_rad, float x, float y, float z);
  void sgl_scale(float x, float y, float z);
  void sgl_translate(float x, float y, float z);
  void sgl_draw();
  void sgl_load_default_pipeline();
  void sgl_v2f_t2f(float x, float y, float u, float v);
  void sgl_v2f_c3b(float x, float y, uint8_t r, uint8_t g, uint8_t b);
  void sgl_enable_texture(void);
  void sgl_texture(sg_image img);
  void sgl_begin_quads(void);
  void sgl_end(void);

  typedef unsigned long mz_ulong;
  int mz_compress2(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong source_len, int level);

  void* calloc(size_t nitems, size_t size);
  void *malloc(size_t size);
  void free(void *ptr);
  void *memset(void *ptr, int x, size_t n);
  void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*));

]])


function _LOG(...)
	print(...)
	local logFile = io.open(lfs.getSaveDirectory() .. "umoulog", "a+")
	if logFile then
		for i, v in ipairs({ ... }) do
			logFile:write(tostring(v) .. "  ")
		end
		logFile:write("\n")
		logFile:close()
	end
end

function quote(str, strip)
	local str2 = str:gsub("//", "/")
	if str2:sub(1, 1) == '"' then
		if strip then
			return str2:sub(2, #str2 - 1)
		else
			return str2
		end
	elseif not strip then
		return '"' .. str2 .. '"'
	else
		return str2
	end
end

function exists(name)
	if type(name) ~= "string" then
		return false
	end
	return os.rename(name, name) and true or false
end

function isFile(name)
	if type(name) ~= "string" then
		return false
	end
	if not exists(name) then
		return false
	end
	local f = io.open(name)
	if f then
		f:close()
		return true
	end
	return false
end

function isDir(name)
	return (exists(name) and not isFile(name))
end

function get_info(levels, start)
	local str = ""
	local levels = levels or 1
	for i = (start or 0) + 1, (start or 0) + levels, 1 do
		local info = debug.getinfo(i + 1, "Sl")
		if info then
			str = str .. string.format("%s:%d", info.short_src, info.currentline) .. "\n"
		end
	end
	return str
end
function print_info(levels)
	print_orig(get_info(levels, 1))
end
-- print = function(...)
--  print_orig(...)
--  print_info(3)
-- end

function ffi_copy(dest, src, size)
	-- print("ffi copy",size)
	-- print_info(4)
	if size then
		ffi.copy(dest, src, size)
	else
		ffi.copy(dest, src)
	end
	-- print("copied")
end
function ffi_string(data, size)
	print("ffi string", size)
	print_info(4)
	local str = ffi.string(data, size)
	print("stringed")
	return str
end

function lfs.saveZip(input, output, printOutput)
	local output = output or input .. ".zip"
	local input = input
	output = output
	if printOutput then
		print("input,output", input, output)
	end
	local function path_concat(...)
		return table.concat({ ... }, "/")
	end
	if printOutput then
		print("BUILDER: Building " .. output)
	end
	local builder = builder_new(self, output)
	assert(builder, "Can't initialize builder")
	local function add_dir(root, dir)
		for i, file in ipairs(list_dir(path_concat(root, dir))) do
			if file ~= ".." and file ~= "." then
				local partial_file_path, full_file_path

				if dir then
					partial_file_path = path_concat(dir, file)
					full_file_path = path_concat(root, dir, file)
				else
					partial_file_path = file
					full_file_path = path_concat(root, file)
				end

				local t = get_type(full_file_path)

				if t == "file" then
					if printOutput then
						print("BUILDER:  +> " .. partial_file_path)
					end
					builder_add(builder, full_file_path, partial_file_path)
				elseif t == "directory" then
					if printOutput then
						print("BUILDER:  /> " .. partial_file_path .. "/")
					end
					add_dir(root, partial_file_path)
				else
					if printOutput then
						print("BUILDER:  ?> " .. partial_file_path)
					end
				end
			end
		end
	end

	add_dir(input, nil)
	builder_close(builder)
  
	local f = fs.open(output, "w+")
	if f then
		f:close()
	end
	print("BUILDER: DONE")
end

function cchar(str)
	local chars = ffi.new("unsigned char[?]", ffi.sizeof("unsigned char") * #str)
	ffi_copy(chars, str)
	return chars
end

function compress(container, format, rawstring, level)
	local dest, dest_len =
		ffi.cast("unsigned char*", ffi.C.calloc(#rawstring, ffi.sizeof("mz_ulong"))),
		ffi.new("mz_ulong[1]", #rawstring * 4)
	local source, source_len =
		ffi.cast("unsigned char*", ffi.C.calloc(#rawstring, ffi.sizeof("mz_ulong"))), ffi.new("mz_ulong", #rawstring)
	ffi_copy(source, rawstring)
	source = ffi.cast("const unsigned char*", source)
	local success = ffi.C.mz_compress2(dest, dest_len, source, source_len, level or 9)
	if success == 0 then
		local out = ffi_string(dest)
		-- ffi.C.free(dest)
		-- ffi.C.free(ffi.cast("unsigned char*",source))
		return out
	else
		error("could not compress string " .. tostring(success))
	end
end

-- text2 = compress("string","deflate",text,6)
-- print("in",text)
-- print("out",text2)
-- os.exit()


-- function setCallbacks()
	-- C.glfwSetPenTabletDataCallback(function(x,y,z,pressure,pitch,yaw,roll)
	--   mouse_penmoved(x,y,z,pressure,pitch,yaw,roll)
	-- end)

	-- _G.proximitycounter=0
	-- C.glfwSetPenTabletProximityCallback(function ( proximity)
	--   proximitycounter = proximitycounter+1
	--   print(string.format("%08x: pen proximity: %d\n",proximitycounter, proximity))
	-- end)
	-- C.glfwSetPenTabletCursorCallback(function ( cursor)
	--   proximitycounter = proximitycounter+1
	--   print(string.format("%08x: pen cursor: %d\n",proximitycounter, cursor))
	-- end)

	-- C.glfwSetWindowFocusCallback(gfx.window,function(window,int)
	--   --TODO GLFW CRASH
	--   -- engine.focus(int==1)
	-- end)

	-- C.glfwSetWindowRefreshCallback(gfx.window,refreshWindow )

	-- C.glfwSetFramebufferSizeCallback(gfx.window,function(window,w,h)
	--     engine.resized = 2
	--     refreshWindow ()
	-- end)

	-- ffi.C.glfwSetScrollCallback(gfx.window, function(target, x, y)
	--   if target == gfx.window then
	--     local scale = mouse.getScale()
	--     mouse_wheelmoved( x*scale, y*scale)
	--   end
	-- end)
-- end

-- function isMinimized()
	--TODO
	--   glfwSetWindowIconifyCallback(window, window_iconify_callback);
	-- The callback function receives changes in the iconification state of the window.

	-- void window_iconify_callback(GLFWwindow* window, int iconified)
	-- {
	--     if (iconified)
	--     {
	--         // The window was iconified
	--     }
	--     else
	--     {
	--         // The window was restored
	--     }
	-- }
-- 	return false
-- end
-- function refreshWindow(window)
-- end

-- WINDOW----------------------------------------------------------------------
-- ----------------------------------------------------------------------------


function lwindow.setMode(width, height, flags)
	print("setMode ", width, height)
	engine.window.new_size.width, engine.window.new_size.height = width, height
	engine.window.set_size = true
	if flags then
		if gfx.window and flags.minwidth or flags.minheight then
			-- ffi.C.glfwSetWindowSizeLimits(gfx.window, flags.minwidth or 0, flags.minheight or 0, -1, -1)
		end
	end

end

function lwindow.setPosition(x, y)
	print("setPosition", x, y)
	if not (x and y) then
		error("bad window position")
	end
	engine.window.new_pos.x, engine.window.new_pos.y = x, y
	engine.window.set_pos = true
end

local windowX, windowY = ffi.new("int[1]"), ffi.new("int[1]")
function lwindow.getPosition()
	-- ffi.C.glfwGetWindowPos(gfx.window, windowX, windowY)
	-- print("get monitor")
	-- print("window monitor",monitor)
	return tonumber(windowX[0]), tonumber(windowY[0])
end

function lwindow.setTitle(title)
	local str = ffi.new("char[?]", #title + 1)
	ffi.fill(str, #title)
	ffi_copy(str, title)
	engine.window.title = str
end

function lwindow.setFullscreen(set)
	ffi.C.toggleFullscreen(engine.window)
end

function lwindow.getFullscreen()
	return engine.window.fullscreen
end

function lwindow.setVsync(sync)
  -- ffi.C.setVsync(sync)
end
function lwindow.getVSync()
	return engine.window.vsync
end

-- FILESYSTEM------------------------------------------------------------------
-- ----------------------------------------------------------------------------
local bytesRead = ffi.new("int [1]")

function lfs.mkdir(...)
	local dir = ""
	-- print_info(3)

	for i, sub in ipairs({ ... }) do
		dir = dir:gsub("//", "/") .. "/" .. sub
		if ZIP_MODE then
			lfs.createDirectory(dir)
		else
			os.execute('mkdir "' .. dir .. '"')
		end
	end
end

function lfs.IsFileDropped()
	return false
end

function lfs.newFile(filename, mode)
	print("zipfilename", lfs.getSaveDirectory() .. filename)
	local file = {
		file = io.open(lfs.getSaveDirectory() .. filename, "w"),
		pos = 0,
	}
	-- print("file",file.file)
	-- os.exit()

	function file:tell()
		return self.pos
	end
	function file:read()
		return self.file:read()
	end
	function file:write(string)
		self.pos = self.pos + #string
		return self.file:write(string)
	end

	function file:flush()
		self.file:flush()
	end
	function file:close()
		self.file:close()
	end

	return file
end

function lfs.getInfo(dir)
	return {
		type = lfs.isFile(dir) and "file" or "directory",
		size = lfs.getSize(dir),
		modtime = lfs.getLastModified(dir),
	}
end

function lfs.isFused()
	return _FUSED
end

function lfs.getLastModified(dir)
	local time 
	if not lfs.isFused() then
    -- print(ENGINE_DIR .. dir)
		time = fs.attr(ENGINE_DIR .. dir, "mtime")
	end
	if not time then
    -- print(SAVE_DIR .. dir)
		time = fs.attr(SAVE_DIR .. dir, "mtime")
	end
	return time or 0
end

function lfs.inSaveDirectory(dir)
	return lfs.getSaveDirectory() .. dir --):gsub("//","/")
end

function lfs.remove(dir,absolute)
	fs.remove(absolute and dir or lfs.inSaveDirectory(dir))
end

function lfs.move(from,to)
  fs.move(from,to)
end

function lfs.getUserDirectory()
	return fs.homedir()
end

function lfs.getSaveDirectory()
	local ext = MAC and "/Library/Application Support/Umoupen/"
		or (WINDOWS and "/AppData/Roaming/Umoupen/" or LINUX and "")
	return (lfs.getUserDirectory()) .. ext
end

function lfs.getSource()
  -- TODO
	return "/path/to/user/ProjectName/umoupen"
end

function lfs.write(data, dir, absolute)
  local dir = absolute and dir or lfs.inSaveDirectory(dir)
  local file, e = io.open(".tmp", "w")
  if file then
    file:write(data)
    file:close()
    os.rename(".tmp", quote(type(dir)=="table" and table.concat(dir) or dir, true))
  else
    _LOG("failed to open file" .. tostring(dir))
  end
end


function lfs.wrapLua(str)
	return string.format("local t = %s; return t", str)
end

function lfs.createDirectory(dir)
	if not lfs.exists(dir) then
		local file = lfs.inSaveDirectory(dir)
		print("lfs.createDirectory", file)
		fs.mkdir(file, true)
	end
end

function lfs.getDirectoryItems(dir,absolute)
	local t = {}
	for name, d in fs.dir(absolute and dir or lfs.inSaveDirectory(dir)) do
		if not name then
			break
		end
		if d:name() ~= ".DS_Store" then
			table.insert(t, {name=d:name(),path=d:path()})
		end
	end
	return t
end

function lfs.read(dir)
	if fs.is(lfs.inSaveDirectory(dir)) then
		local file = io.open(lfs.inSaveDirectory(dir))
    if not file then
      error("invalid file")
    end
		local str = file:read("*a")
		file:close()
		return str
	else
		-- TODO loading assets from executable
		-- print("read file rl")
		-- local str = LoadFileData(dir,bytesRead)
		-- if bytesRead[0]>0 then
		--   return ffi_string(str,bytesRead[0])
		-- else
		--   return nil
		-- end
	end
end

function lfs.getSize(dir)
	return fs.attr(lfs.inSaveDirectory(dir), "size") or 1
end

function lfs.exists(dir,absolute)
  -- print("exists?","'"..lfs.inSaveDirectory(dir).."'",fs.is(lfs.inSaveDirectory(dir)))
  return absolute and fs.is(dir) or fs.is(lfs.inSaveDirectory(dir))
end

function lfs.fileExists(file)
	local f = io.open(file, "r")
	if f then
		f:close()
	end
	return f ~= nil
end

function lfs.isFile(dir,absolute)
	return fs.is(absolute and dir or lfs.inSaveDirectory(dir), "file")
end

function lfs.isDir(dir,absolute)
	return not fs.is(absolute and dir or lfs.inSaveDirectory(dir), "file")
end

function lfs.load(dir)
	-- print("lfs.load",(lfs.inSaveDirectory(dir)):gsub("//","/"))
	return loadfile((lfs.inSaveDirectory(dir)):gsub("//", "/"))
end

function lfs.dofile(dir)
	-- print("lfs.dofile",(lfs.inSaveDirectory(dir)):gsub("//","/"))
	return dofile((lfs.inSaveDirectory(dir)):gsub("//", "/"))
end

-- SOUND -------------------------------------------------------------------------
-- ------------------------------------------------------------------------------
afx.soundsPlaying = {}

local soundSourceMT = {
	__index = {
		seek = function(self, time)
			rl.SetTraceLogLevel(0)
			self.start = time
			self.time = self.start
			rl.StopSound(self.sound)
			local final = mf(self.frameCount - self.sampleSize)
			local init = mc(0, mf(time * self.sampleRate), final)
			self.finish = final / (self.sampleRate * 2)
			if time == 0 then
				self.sound = ffi.gc(rl.LoadSoundFromWave(self.wave), rl.UnloadSound)
			else
				local subwave = ffi.gc(rl.WaveCopy(self.wave), rl.UnloadWave)
				-- time*self.wave.sampleRate
				print("crop", init, final)

				if subwave then
					-- ffi.C.free(subwave.data)
					subwave.frameCount = math.max(0, (final - init))
					local chan = 1
					local size = chan * self.frameCount * (self.sampleSize / 8)
					local offset = (init * self.channels * self.sampleSize / 8)
					local chunk = chan * subwave.frameCount * self.sampleSize / 8
					local start = offset / ffi.sizeof("unsigned char")
					local len = chunk / ffi.sizeof("unsigned char")
					local src = ffi.cast("unsigned char*", self.wave.data) + offset
					ffi.fill(subwave.data, size)
					print("filled", size)
					print("start", offset)
					local chunk = size - offset
					-- local endByte = offset + chunk
					print("end", offset + chunk)

					ffi_copy(subwave.data, src, chunk)

					-- subwave.data = ffi.new("unsigned char[?]",len)
					-- ffi.fill(subwave.data,size)

					-- local data = ffi.cast("unsigned char*",subwave.data)
					-- for i=start,start+len,1 do
					--  data[i] = 0
					-- end
					-- rl.WaveCrop(subwave,init,final)
					print("audio cropped")
					self.sound = ffi.gc(rl.LoadSoundFromWave(subwave), rl.UnloadSound)
					print("seek", time, self.wave.frameCount)
				end
			end

			rl.SetTraceLogLevel(LOG_LEVEL)
		end,
		play = function(self)
			self.time = self.start
			if self.time < self.length then
				rl.PlaySound(self.sound)
				table.add(afx.soundsPlaying, self)
			end
		end,
		stop = function(self)
			rl.StopSound(self.sound)
		end,
		getDuration = function(self)
			return self.length
		end,
	},
}
local soundDataMT = {
	__index = {
		getSample = function(self, sample)
			return self.samples[math.max(0, math.min(sample, self.frameCount))]
		end,
		getSampleCount = function(self)
			return self.frameCount
		end,
		getSampleRate = function(self)
			return self.sampleRate
		end,
	},
}

local function loadTrimWave(file)
	local wave = ffi.C.LoadWave(file)
	print("sample count :")
	-- ffi.C.WaveFormat(wave, wave.sampleRate, wave.sampleSize, 2)
	-- local samples = ffi.C.LoadWaveSamples(wave)
	-- local final = wave.frameCount/2--wave.frameCount
	-- local vol = samples[wave.frameCount]
	-- for i=wave.frameCount,1,-1 do
	--  local samp = samples[i]
	--  if samp~=vol  then
	--    final = i
	--    break
	--  end
	-- end
	-- print("final",final,wave.frameCount)
	-- ffi.C.WaveCrop(wave,0,final)
	-- wave.frameCount=final
	-- ffi.C.UnloadWaveSamples(samples)
	return wave
end

function afx.newSource(file)
	local soundSource = {}
	soundSource.time = 0
	soundSource.start = 0
	soundSource.file = SAVE_DIR .. file
	soundSource.wave = ffi.gc(loadTrimWave(SAVE_DIR .. file), ffi.C.UnloadWave)
	-- ffi.C.WaveCrop(soundSource.wave,0,mf(soundSource.wave.frameCount/2))
	-- soundSource.wave.frameCount = mf(soundSource.wave.frameCount/2)
	soundSource.sound = ffi.gc(ffi.C.LoadSoundFromWave(soundSource.wave), ffi.C.UnloadSound)
	soundSource.frameCount = soundSource.wave.frameCount
	soundSource.sampleRate = soundSource.wave.sampleRate
	soundSource.sampleSize = soundSource.wave.sampleSize
	soundSource.channels = soundSource.wave.channels
	soundSource.length = soundSource.wave.frameCount / (soundSource.wave.sampleRate * 2)

	setmetatable(soundSource, soundSourceMT)
	return soundSource
end

function afx.update(dt)
	for s, sound in ripairs(afx.soundsPlaying) do
		sound.time = sound.time + dt
		trace(sound.time, sound.length)
		if sound.time > sound.length then
			sound.time = 0
			rl.StopSound(sound.sound)
			table.remove(afx.soundsPlaying, s)
			print("sound DONE")
		-- rl.PlaySound(sound.sound)
		elseif sound.time > sound.finish then
			-- print(sound.time,sound.finish)
			-- sound:stop()
			-- sound:seek(sound.finish)
			-- sound:play()
		end
	end
end

function afx.stop()
	for s, sound in ripairs(afx.soundsPlaying) do
		sound.time = 0
		rl.StopSound(sound.sound)
		table.remove(afx.soundsPlaying, s)
	end
end

function afx.setVolume(volume)
	rl.SetMasterVolume(volume)
end

function sfx.newSoundData(file)
	local wave = loadTrimWave(SAVE_DIR .. file)

	local soundData = {
		samples = ffi.C.LoadWaveSamples(wave),
		frameCount = wave.frameCount,
		sampleRate = wave.sampleRate,
	}
	ffi.C.UnloadWave(wave)
	setmetatable(soundData, soundDataMT)
	return soundData
end

-- MATH -------------------------------------------------------------------------
-- ------------------------------------------------------------------------------
local sin, cos = math.sin, math.cos

mat4 = cpml.mat4
vec3 = cpml.vec3
vec2 = cpml.vec2
quat = cpml.quat
utils = cpml.utils
local tmpBez1 = ffi.new(VECTOR_TYPE .. "[10]")
local tmpBez2 = ffi.new(VECTOR_TYPE .. "[10]")

local function det(x1, y1, x2, y2)
	return x1 * y2 - y1 * x2
end
local function ccw(p, q, r)
	return det(q.x - p.x, q.y - p.y, r.x - p.x, r.y - p.y) >= 0
end
local function onSameSide(a, b, c, d)
	local px, py = d.x - c.x, d.y - c.y
	local l = det(px, py, a.x - c.x, a.y - c.y)
	local m = det(px, py, b.x - c.x, b.y - c.y)
	return l * m >= 0
end
local function pointInTriangle(p, a, b, c)
	return onSameSide(p, a, b, c) and onSameSide(p, b, a, c) and onSameSide(p, c, a, b)
end
local function anyPointInTriangle(vertices, p, q, r)
	for v in pairs(vertices) do
		if v ~= p and v ~= q and v ~= r and pointInTriangle(v, p, q, r) then
			return true
		end
	end
	return false
end
local function isEar(p, q, r, vertices)
	return ccw(p, q, r) and not anyPointInTriangle(vertices, p, q, r)
end
local function areCollinear(p, q, r, eps)
	return math.abs(det(q.x - p.x, q.y - p.y, r.x - p.x, r.y - p.y)) <= (eps or 1e-32)
end
local function getIndexOfleftmost(vertices)
	local idx = 1
	for i = 2, #vertices do
		if vertices[i].x < vertices[idx].x then
			idx = i
		end
	end
	return idx
end

local function gammaToLinear(c)
	if c <= 0.04045 then
		return c / 12.92
	else
		return math.pow((c + 0.055) / 1.055, 2.4)
	end
end

local function linearToGamma(c)
	if c <= 0.0031308 then
		return c * 12.92
	else
		return 1.055 * math.pow(c, 1.0 / 2.4) - 0.055
	end
end

function math.round(n, deci)
	deci = 10 ^ (deci or 0)
	return math.floor(n * deci + 0.5) / deci
end
function math.zero(value)
	local newVal = 0
	if value < 0 then
		newVal = 0
	else
		newVal = value
	end
	return newVal
end
function math.sling(a, n, b, s)
	local diff = (n > b) and 1 or ((n < a) and (1 + math.abs(n - a)) * 1.5 or 1)
	return (math.abs(n - a) < 0.001 or math.abs(n - b) < 0.001) and n
		or (easeTo(n, (n < a) and a or ((n > b) and b or n), 2))
end
function math.angle(x1, y1, x2, y2)
	return math.atan2(y2 - y1, x2 - x1)
end
function math.dist(x1, y1, x2, y2)
	return math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
end
function math.dist3(x1, y1, z1, x2, y2, z2)
	return ((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) + (z2 - z1) * (z2 - z1)) ^ 0.5
end
function math.sdist(x1, y1, x2, y2)
	return (x2 - x1) ^ 2 + (y2 - y1) ^ 2
end
function math.ddist(xd, yd)
	return (xd ^ 2 + yd ^ 2) ^ 0.5
end
function math.sign(n)
	return n > 0 and 1 or n < 0 and -1 or 0
end
function mf(num)
	return (type(num) == "number") and math.floor(num) or num
end
function mu(num)
	return (type(num) == "number") and math.ceil(num) or num
end
function mr(num)
	return (type(num) == "number") and math.round(num) or num
end
function math.clamp(low, high, n)
	return math.min(math.max(n, low), high)
end
function mc(low, n, high)
	return math.clamp(low, high, n)
end

function mth.vec3Table(vec)
	return vec and { x = vec.x, y = vec.y, z = vec.z } or { x = 0, y = 0, z = 0 }
end

function mth.vec3Table2(vec)
	return vec and { vec.x, vec.y, vec.z } or { 0, 0, 0 }
end

function mth.triangulate(poly)
	if #poly == 6 then
		return { poly }
	end

	local vertices = {}
	for i = 1, #poly / 2, 1 do
		vertices[i] = { x = poly[i * 2 - 1], y = poly[i * 2] }
	end

	-- assert polygon is oriented counter clockwise
	local r = getIndexOfleftmost(vertices)
	local q = r > 1 and r - 1 or #vertices
	local s = r < #vertices and r + 1 or 1
	if not ccw(vertices[q], vertices[r], vertices[s]) then -- reverse order if polygon is not ccw
		local tmp = {}
		for i = #vertices, 1, -1 do
			tmp[#tmp + 1] = vertices[i]
		end
		vertices = tmp
	end

	local next_idx, prev_idx = {}, {}
	for i = 1, #vertices do
		next_idx[i], prev_idx[i] = i + 1, i - 1
	end
	next_idx[#next_idx], prev_idx[1] = 1, #prev_idx
	local concave = {}
	for i, v in ipairs(vertices) do
		if not ccw(vertices[prev_idx[i]], v, vertices[next_idx[i]]) then
			concave[v] = true
		end
	end

	local triangles = {}
	local n_vert, current, skipped, next, prev = #vertices, 1, 0
	while n_vert > 3 do
		next, prev = next_idx[current], prev_idx[current]
		local p, q, r = vertices[prev], vertices[current], vertices[next]
		if isEar(p, q, r, concave) then
			if not areCollinear(p, q, r) then
				triangles[#triangles + 1] = { p.x, p.y, q.x, q.y, r.x, r.y }
				next_idx[prev], prev_idx[next] = next, prev
				concave[q] = nil
				n_vert, skipped = n_vert - 1, 0
			end
		else
			skipped = skipped + 1
			if not (skipped <= n_vert) then
				print("Cannot triangulate polygon")
				return false
			end
		end
		current = next
	end
	next, prev = next_idx[current], prev_idx[current]
	local p, q, r = vertices[prev], vertices[current], vertices[next]
	triangles[#triangles + 1] = { p.x, p.y, q.x, q.y, r.x, r.y }
	return triangles
end

function mth.gammaToLinear(r, g, b)
	return gammaToLinear(r), gammaToLinear(g), gammaToLinear(b)
end
function mth.linearToGamma(r, g, b)
	return linearToGamma(r), linearToGamma(g), linearToGamma(b)
end

function evaluateBezier(bezier, t)
	local curve = type(bezier) == "table" and bezier.curve or bezier
	tmpBez1[1], tmpBez1[2] = lerp(curve[1], curve[5], t), lerp(curve[2], curve[6], t) -- E
	tmpBez1[3], tmpBez1[4] = lerp(curve[5], curve[7], t), lerp(curve[6], curve[8], t) -- F
	tmpBez1[5], tmpBez1[6] = lerp(curve[7], curve[3], t), lerp(curve[8], curve[4], t) -- G
	tmpBez1[7], tmpBez1[8] = lerp(tmpBez1[3], tmpBez1[5], t), lerp(tmpBez1[4], tmpBez1[6], t) -- J

	tmpBez2[1], tmpBez2[2] = curve[1], curve[2] -- A
	tmpBez2[5], tmpBez2[6] = tmpBez1[1], tmpBez1[2] -- E
	tmpBez2[7], tmpBez2[8] = lerp(tmpBez1[1], tmpBez1[3], t), lerp(tmpBez1[2], tmpBez1[4], t) -- H
	return lerp(tmpBez2[7], tmpBez1[7], t), lerp(tmpBez2[8], tmpBez1[8], t) -- K
end

local function setControlPoint(bezier, i, x, y)
	local curve = bezier.curve
	if i == 1 then
		curve[1], curve[2] = x, y
	elseif i == 4 then
		curve[3], curve[4] = x, y
	elseif i == 2 then
		curve[5], curve[6] = x, y
	elseif i == 3 then
		curve[7], curve[8] = x, y
	end
end
function setControlPoints(bezier, ax, ay, bx, by, cx, cy, dx, dy)
	-- local curve = bezier.curve
	-- curve[1],curve[2]= ax,ay
	--   curve[3],curve[4]= bx,by
	--   curve[5],curve[6]= cx,cy
	--   curve[7],curve[8]= dx,dy
	bezier:setControlPoint(1, ax, ay)
	bezier:setControlPoint(2, bx, by)
	bezier:setControlPoint(3, cx, cy)
	bezier:setControlPoint(4, dx, dy)
end
local function renderBezier(bezier, d)
	local t = {}
	for i = 0, 1, 0.5 / d do
		local x, y = evaluateBezier(bezier, i)
		table.insert(t, x)
		table.insert(t, y)
	end
	return t
end

local bezTable =
	{ __index = {
		render = renderBezier,
		evaluate = evaluateBezier,
		setControlPoint = setControlPoint,
	} }
function mth.newBezierCurve(...)
	local pts = { ... }
	if type(pts[1]) == "table" then
		pts = pts[1]
	end
	local bez = ffi.new(VECTOR_TYPE .. "[10]")
	if not pts[7] then
		bez[1], bez[2] = pts[1], pts[2]
		bez[3], bez[4] = pts[5], pts[6]
		bez[5], bez[6] = pts[3], pts[4]
		bez[7], bez[8] = pts[3], pts[4]
	else
		bez[1], bez[2] = pts[1], pts[2]
		bez[3], bez[4] = pts[7], pts[8]
		bez[5], bez[6] = pts[3], pts[4]
		bez[7], bez[8] = pts[5], pts[6]
	end
	local bezier = { curve = bez }
	return setmetatable(bezier, bezTable)
end

local tmpMat = ffi.new("mat4")
function setMat(b, a)
	local a = a and a.e or tmpMat.e
	a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], a[14], a[15]=
		b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15], b[16]
	return tmpMat
end
axes = {
	x = vec3(1, 0, 0),
	y = vec3(0, 1, 0),
	z = vec3(0, 0, 1),
}
vec3.unit_x = axes.x
vec3.unit_y = axes.y
vec3.unit_z = axes.z

blankVec = vec3(0)
blankMat = mat4()

local tempModel = ffi.new("Model")
local tempRay = ffi.new("Ray")
local tempBBOX = ffi.new("BoundingBox")
tempModel.meshCount = 1
function testTempBBOX(ray, bbox)
	tempBBOX.min.x, tempBBOX.min.y, tempBBOX.min.z = bbox.min.x, bbox.min.y, bbox.min.z
	tempBBOX.max.x, tempBBOX.max.y, tempBBOX.max.z = bbox.max.x, bbox.max.y, bbox.max.z
	tempRay.position.x, tempRay.position.y, tempRay.position.z = ray.position.x, ray.position.y, ray.position.z
	tempRay.direction.x, tempRay.direction.y, tempRay.direction.z = ray.direction.x, ray.direction.y, ray.direction.z
	return C.CheckCollisionRayBox(tempRay, tempBBOX)
end
function testTempModel(ray, mesh)
	tempModel.meshes = ffi.cast("Mesh*", mesh.mesh)
	tempRay.position = ray.position:cast()
	tempRay.direction = ray.direction:cast()
	local info = C.GetCollisionRayModel(tempRay, tempModel)
	return info.hit,
		info.distance,
		vec3(info.position.x, info.position.y, info.position.z),
		vec3(info.normal.x, info.normal.y, info.normal.z),
		info.triangle
end

    
ffi.cdef([[
  typedef struct { cpml_vec2 a, b, c;} HullTriangle;
  typedef struct { float x, y; uint32_t id; } Point;
  typedef struct { Point p1, p2; } Edge;
  typedef struct { Point p1, p2, p3; Edge e1, e2, e3; float cx, cy, cr ; } Triangle;
  typedef struct { Edge edge; uint16_t e; uint16_t t;  } OpenEdge;
]])
  
local ffiPoint, ffiEdge, ffiTriangle

--- Point class
local Point = {}

Point.__index = Point

function Point.new( x, y )
  return ffiPoint( x, y, 0 )
end

function Point:__eq( other )   
  return self.x == other.x and self.y == other.y 
end

function Point:__tostring()
  return ('Point (%s) x: %.2f y: %.2f'):format( self.id, self.x, self.y )
end

function Point:dist2(p)
  local dx, dy = (self.x - p.x), (self.y - p.y)
  return dx * dx + dy * dy
end

function Point:dist(p)
  return math.sqrt(self:dist2(p))
end

function Point:isInCircle(cx, cy, r)
  local dx = (cx - self.x)
  local dy = (cy - self.y)
  return ((dx * dx + dy * dy) <= (r * r))
end

local Edge = {}

Edge.__index = Edge

function Edge.new( p1, p2 )
  return ffiEdge( p1, p2 )
end

function Edge:__eq( other ) 
  return self.p1 == other.p1 and self.p2 == other.p2 
end

function Edge:__tostring()
  return (('Edge :\n  %s\n  %s'):format(tostring(self.p1), tostring(self.p2)))
end

function Edge:same(otherEdge)
  return ((self.p1 == otherEdge.p1) and (self.p2 == otherEdge.p2))
      or ((self.p1 == otherEdge.p2) and (self.p2 == otherEdge.p1))
end

--- Triangle class
local Triangle = {}

Triangle.__index = Triangle
function Triangle:__tostring()
  return (('Triangle :\n  %s\n  %s\n  %s'):format(tostring(self.e1), tostring(self.e2), tostring(self.e3)))
end
function Triangle.new( p1, p2, p3 )
  local tri = ffiTriangle( p1, p2, p3, Edge(p1, p2), Edge(p2, p3), Edge(p3, p1))
  tri.cx,tri.cy,tri.cr = tri:getCircumCircle()
  tri.cr = tri.cr*tri.cr
  return tri
end

setmetatable( Point, {__call = function( _, x, y )
  return Point.new( x, y )
end} )
setmetatable( Edge, {__call = function(_,p1,p2)
  return Edge.new( p1, p2 )
end} )
setmetatable( Triangle, {__call = function( _, p1, p2, p3 )
  return Triangle.new( p1, p2, p3 )
end} )

ffiPoint = ffi.metatype( "Point", Point )
ffiEdge = ffi.metatype( "Edge", Edge )
ffiTriangle = ffi.metatype( "Triangle", Triangle )

::exit::
