local module = {}

-- PARTS TABLE
local parts = {}
_G.Storage:WaitForChild("Parts")
local function added(object)
	if object.Parent:IsA("Folder") then
		local id = object:GetAttribute("PartId")
		if id then parts[id] = object end
	end
end
for _,object in pairs(_G.Storage.Parts:GetDescendants()) do
	added(object)
end
_G.Storage.Parts.DescendantAdded:Connect(function(object)
	if object:GetAttribute("PartId") then
		warn(string.format("%i:%s, you are late!",object:GetAttribute("PartId"),object:GetFullName()))
	end
	added(object)
end)
function module.get_template_part(part)
	local id = part:GetAttribute("PartId")
	if not id then return end
	local template = parts[id]
	return template
end
function module.get_part_by_id(id)
	return parts[id]
end

function module.change_material(part,material)
	if part.Material == Enum.Material.Glass then
		part.Transparency = part:GetAttribute("OriginalTransparency",part.Transparency)
	end
	if material == Enum.Material.Glass then
		part:SetAttribute("OriginalTransparency",part.Transparency)
		part.Transparency = 0.5
	end
	part.Material = material
end

-- PART METHODS
function module.apply_material(part,material)
	if material then
		part:SetAttribute("Material",material.Name)
		if part:IsA("Model") then
			for _,subpart in pairs(part:GetDescendants()) do
				if subpart:IsA("BasePart") then
					module.change_material(subpart,material)
				end
			end
		else
			module.change_material(part,material)
		end
	end
end
function module.apply_edit(part,index,value)
	part[index].Value = value
end
function module.get_size(part)
	local nodes = module.get_nodes_class(part)
	if nodes == 1 then
		return part:GetAttribute("Size")
	elseif nodes == 2 then
		return Vector3.new(part.Size.Z,0,0) -- Yeah the bloody irony I know right?
	elseif nodes >= 3 then
		return Vector3.new(part:FindFirstChildWhichIsA("Part").Size.X,0,0)
	end
end
function module.get_thickness(part)
	return module.get_size(part).X -- WHAT?!
end
function module.apply_size(part,size)
	part:SetAttribute("Size",size)
	local nodes = module.get_nodes_class(part)
	if nodes == 1 then
		if part:IsA("Model") then
			part:ScaleTo(size.X)
		elseif part:IsA("BasePart") then
			if size.Y == 0 or size.Z == 0 then return end -- Since this is not for normal parts it's a poly size
			part.Size = size
		end
	elseif nodes == 2 then
		part.Size = Vector3.new(part.Size.X,size.X,size.X) -- Yeah, this is serious
	elseif nodes >= 3 then
		for _,item in pairs(part:GetChildren()) do
			item.Size = Vector3.new(size.X,item.Size.Y,item.Size.Z)
		end
	end
end
function module.apply_color(part,color)
	part:SetAttribute("Color",color)
	if part:IsA("Model") then
		for _,subpart in pairs(part:GetDescendants()) do
			if subpart:IsA("BasePart") then
				subpart.Color = color
			end
		end
	else
		part.Color = color
	end
end
local function Weld(part1,part2,temporary) -- Make a BUILDING weld 
	local weld = Instance.new("WeldConstraint")
	weld.Name = "BuiltWeldConstraint"
	weld.Parent = part1
	weld.Part0 = part1
	weld.Part1 = part2
	if temporary then weld.Name = "WeldConstraint" end -- Or not.
	return weld
end
function module.weld(part,connection,temporary)
	local attach = part
	if part:IsA("Model") then attach = attach.PrimaryPart end
	local new_weld = Weld(attach,connection,temporary)
	new_weld.Parent = part
	return new_weld
end
function module.form_triangle(pa,pb,p1,p2,p3,s2)
	local s1 = pa.Size.X
	s2 = s2 or s1*0.5
	s2 = 0
	local p12 = p2-p1
	local p23 = p3-p2
	local p13 = p3-p1
	local p12l = p12.Magnitude
	local p23l = p23.Magnitude
	local p13l = p13.Magnitude
	local bottom
	local bot_len
	local totop
	local root
	local tip
	local far
	local t
	if p12l > p23l and p12l > p13l then -- TRUE: p12 largest FALSE: p12 smaller than p23l or p13l
		tip = p3
		root = p1
		far = p2
		bottom = p12
		bot_len = p12l
		totop = p13
		--[[pa.Color = Color3.new(1,0,0)
		pb.Color = Color3.new(1,0,0)]]
	elseif p23l > p13l then -- TRUE: p23 must be largest FALSE: p13 largest
		tip = p1
		root = p2
		far = p3
		bottom = p23
		bot_len = p23l
		totop = -p12
		--[[pa.Color = Color3.new(0,1,0)
		pb.Color = Color3.new(0,1,0)]]
	else -- p13 largest
		tip = p2
		root = p1
		far = p3
		bottom = p13
		bot_len = p13l
		t = pa
		pa = pb
		pb = t
		--[[pa.Color = Color3.new(0,0,1)
		pb.Color = Color3.new(0,0,1)]]
		totop = p12
	end
	bottom = bottom.Unit
	local coeff = totop:Dot(bottom)
	local mid = root+coeff*bottom
	local cross = totop:Cross(bottom).Unit
	local top = (mid-tip)
	pa.Size = Vector3.new(s1,top.Magnitude,coeff)
	pb.Size = Vector3.new(s1,top.Magnitude,bot_len - coeff)
	top = top.Unit
	pa.CFrame = CFrame.fromMatrix(s2*cross+(root+tip)*0.5,cross,-top,bottom)
	pb.CFrame = CFrame.fromMatrix(s2*cross+(tip+far)*0.5,-cross,-top,-bottom)
end
function module.get_tri_positions1(pa,pb)
	local ca = pa.CFrame
	local ra = ca.Rotation
	local cb = pb.CFrame.Position
	local sa = pa.Size
	local lb = pb.Size.Z
	local lh = sa.Y
	local fb = Vector3.new(0,lh,-lb)
	return (cb - ra * (fb * 0.5)) -- FOR THIRD RETURN VALUE OF BELOW
end
function module.get_tri_positions2(pa,pb)
	local ca = pa.CFrame
	local ra = ca.Rotation
	ca = ca.Position
	local sa = pa.Size
	local la = sa.Z
	local lh = sa.Y
	local fa = Vector3.new(0,lh,la)
	return (ca + ra * (fa * -0.5)),(ca + ra * (fa * 0.5))
end
-- apply data positions of parts
local form_triangle = module.form_triangle
local number_to_string = {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"}
local string_to_number = {}
for num,str in pairs(number_to_string) do
	string_to_number[str] = num
end
--; for i = 1,10 do number_to_string[i] = tostring(i) end
local mod = CFrame.Angles(0,0,0)
function module.apply_nodes(part,nodes)
	if #nodes < module.get_nodes_class(part) then return true end
	if #nodes == 1 then
		part:PivotTo(nodes[1])
	elseif #nodes == 2 then
		part.CFrame = nodes[1]*mod
		part.Size = Vector3.new(nodes[2],part.Size.Y,part.Size.Z)
		local first = part:FindFirstChild("1")
		local second = part:FindFirstChild("2")
		local p1,p2 = module.get_stick_ends(part)
		local offset = Vector3.new(nodes[2]*0.5,0,0)
		if first then 
			first.Position = nodes[1]:PointToWorldSpace(offset) 
			if first:FindFirstChild("WeldConstraint") then
				first.WeldConstraint.Enabled = true
			end
		end
		if second then
			second.Position = nodes[1]:PointToWorldSpace(-offset)
			if second:FindFirstChild("WeldConstraint") then
				second.WeldConstraint.Enabled = true
			end
		end
	elseif #nodes >= 3 then
		local template = part["A"]
		for i=1,(#nodes-2)*2 do
			local name = number_to_string[i]
			if not part:FindFirstChild(name) then
				local piece = template:Clone()
				piece.Parent = part
				piece.Name = name
			end
		end
		for i=1,(#nodes-2) do
			form_triangle(part[number_to_string[i*2-1]],part[number_to_string[i*2]],nodes[1],nodes[i+1],nodes[i+2])  
		end
	end
end
-- get the poly 'type' of a part
function module.get_nodes_class(part)
	return part:GetAttribute("Nodes") or 1
end
-- get data positions of parts
function module.get_nodes(part)
	local nodes = part:GetAttribute("Nodes")
	if not nodes then
		return {part:GetPivot()}
	elseif nodes == 2 then
		local first = part:FindFirstChild("1")
		local second = part:FindFirstChild("2")
		if first and second then
			local offset = second.Position - first.Position
			return {CFrame.lookAt(first.Position,second.Position,first.CFrame.UpVector)*CFrame.Angles(0,math.pi*0.5,0) + offset * 0.5,offset.Magnitude}
		else
			return {part.CFrame,part.Size.X}
		end
	elseif nodes >= 3 then
		local i = -1
		local p1 = part["A"]
		local p2 = part["B"]
		local t = {module.get_tri_positions2(p1,p2)}
		while true do
			i = i + 2
			local pn1 = part:FindFirstChild(number_to_string[i])
			if not pn1 then return t end
			local pn2 = part:FindFirstChild(number_to_string[i+1])
			if not pn2 then return t end
			table.insert(t,module.get_tri_positions1(p1,p2))
		end
		return t
	end
end
-- shift a poly part along its normal axis while keeping static node positions
function module.apply_shift(part,shift)
	if module.get_nodes_class(part) < 3 then return end
	local previous = part:GetAttribute("Shift") or 0
	if shift == 0 and previous == 0 then part:SetAttribute("Shift",nil) return end
	part:SetAttribute("Shift",shift)
	shift = shift - previous
	for _,item in pairs(part:GetChildren()) do
		if item:IsA("BasePart") and not item:HasTag("Node") then
			local neg = (string_to_number[item.Name] % 2 == 0)
			if neg then neg = -1 else neg = 1 end
			item.Position = item.Position + item.CFrame.RightVector * item.Size.X * 0.5 * shift * neg
		end
	end
end
-- stupid name, it just tries to shift in the direction based on how we're looking
function module.attempt_shift(part,shift,look)
	local template = part:FindFirstChild("A")
	if not template then return end
	module.apply_shift(part,math.sign(template.CFrame.RightVector:Dot(look)) * shift)
end
-- get the cframe of a single connection
function module.eval_connection(connection)
	local connected = connection.connected
	local relative = CFrame.identity
	if connected then relative = connected:GetPivot() end
	return relative:ToWorldSpace(connection.CFrame)
end
function module.get_stick_ends(part)
	local nodes = module.get_nodes(part)
	if #nodes ~= 2 then error("Part is not a stick",2) end
	local cframe = nodes[1]
	local offset = Vector3.new(nodes[2]*0.5,0,0)
	return cframe:PointToWorldSpace(offset),cframe:PointToWorldSpace(-offset)
end
function module.get_points(part)
	local class = module.get_nodes_class(part)
	if class == 1 then return {}
	elseif class == 2 then return {module.get_stick_ends(part)}
	else return module.get_nodes(part) end
end
-- apply driven connections by attachments in the placement engine
function module.eval_connections(part,connections)
	local class = module.get_nodes_class(part)
	local cframes = {}
	for _,connection in ipairs(connections) do
		table.insert(cframes,module.eval_connection(connection))
	end
	local nodes = {}
	if class == 1 then
		nodes = cframes
	elseif class == 2 then
		if #cframes == 2 then
			local to = cframes[2].Position - cframes[1].Position
			nodes = {CFrame.lookAt(cframes[1].Position,cframes[2].Position,cframes[1].UpVector)*CFrame.Angles(0,math.pi*0.5,0) + (to * 0.5), to.Magnitude}
		end
	elseif class == 3 then
		for _,cframe in ipairs(cframes) do table.insert(nodes,cframe.Position) end
	end
	return nodes
end

function module.apply_internal_welds(part)
	if part:IsA("Model") then
		local main = part.PrimaryPart
		for _,item in pairs(part:GetChildren()) do
			if item:IsA("BasePart") and item ~= main then Weld(item,main,true) end
		end
	end
end

-- PART STATE METHODS
local _state = {}
_state.__index = _state
function _state.apply_material(this)
	if this.material then module.apply_material(this.part,this.material) end
end
function _state.apply_size(this)
	if this.size then module.apply_size(this.part,this.size) end
end
function _state.apply_color(this)
	if this.color then module.apply_color(this.part,this.color) end
end
function _state.apply_edit(this,index,value)
	local edit = this.part:FindFirstChild(index)
	if not edit then error("Could not find edit "..index,2) end
	module.apply_edit(this.part,index,value)
end
function _state.apply_edits(this)
	for index,value in pairs(this.edits) do
		this:apply_edit(index,value)
	end
end
function _state.attempt_shift(this,...)
	module.attempt_shift(this.part,...)
end
function _state.apply_internal_welds(this)
	if module.get_nodes_class(this.part) > 1 then -- Weld together parts inside of poly
		module.apply_internal_welds(this.part)
	end
end
function _state.apply_welds(this)
	this:apply_internal_welds()
	for index,connection in pairs(this.welds) do
		local target = this.part
		if connection.name then target = this.part[connection.name] end
		if target:IsA("Model") then target = target.PrimaryPart end
		if (not this.owner) or (_G.Methods.getOwnedOrNil(this.owner,connection.part)) then
			module.weld(target,connection.part)
		end
	end
end
function _state.apply_connections(this)
	this.nodes = module.eval_connections(this.part,this.connections)
end
function _state.apply_nodes(this)
	module.apply_nodes(this.part,this.nodes)
end
function _state.apply_shift(this)
	if this.look then this:attempt_shift(this.shift or 0,this.look) 
	else module.apply_shift(this.part,this.shift or 0) end
end
function _state.apply(this,part,options)
	options = options or {}
	this.part = part or this.part
	if this.color then this:apply_color() end
	if this.size then this:apply_size() end
	if this.material then this:apply_material() end
	if this.edits and not options.no_edit then this:apply_edits() end
	if this.connections then this:apply_connections() end
	if this.nodes then this:apply_nodes() end
	if this.welds and not options.no_weld then this:apply_welds() else this:apply_internal_welds() end
	if this.shift then this:apply_shift() end
end
function _state.create(this)
	if not this.id then error("No id",2) end
	local template = parts[this.id]:Clone()
	template.Parent = game.Workspace.Objects
	this:apply(template)
	return template
end
function _state.get_nodes_class(this)
	return module.get_nodes_class(this.part)
end
function _state.take(this,existing)
	this.color = existing:GetAttribute("Color")
	this.material = existing:GetAttribute("Material")
	this.size = existing:GetAttribute("Size")
	this.shift = existing:GetAttribute("Shift")
	this.edits = {}
	for _,edit in existing:GetChildren() do
		if edit:HasTag("Edit") then
			if edit.Value ~= (edit:GetAttribute("Default") or nil) then
				this.edits[edit.Name] = edit.Value
			end
		end
	end
	this.nodes = module.get_nodes(existing)
	return this
end
function _state.update(this)
	this:take(this.part)
	return this
end
function _state.copy(this,existing)
	this:take(existing)
	this.id = existing:GetAttribute("PartId")
	this.part = parts[this.id]:Clone()
	this:apply(nil,{no_weld = true})
	return this
end
function module.copy(existing)
	return module.new():copy(existing)
end
function module.new(original)
	return setmetatable(_G.Union({
		material = nil, -- Enum.Material SERIAL
		color = nil, -- Color3.new() SERIAL
		size = nil, -- Vector3.new() SERIAL
		shift = nil, -- 1 -> 0 -> -1 SERIAL
		connections = {}, -- {[n]={cframe=,connection=}} for placement engine etc
		welds = {}, -- {{name=,touched=},...},... SERIAL
		edits = {}, -- ? SERIAL
		owner = nil, -- player
		part = nil, -- part being worked with (applied)
		id = nil -- part id SERIAL
	},original or {}),_state)
end

-- SERIALISATION
--[[local function serialisation_factory(match_string)
	local serialisation_table = {}
	for index in match_string:gmatch("[^,]+") do
		table.insert(serialisation_table,index)
	end
	return {
		serialise = function(this)
			local t = {}
			for _,key in pairs(serialisation_table) do
				table.insert(t,this[key])
			end
			return table.unpack(t)
		end,
		deserialise = function(this,...)
			local t = {...}
			for index,key in pairs(serialisation_table) do
				this[key] = t[index]
			end
		end,
	}
end
local state_serialiser = serialisation_factory("id,connections,color,size,shift,material,edits,welds")]]
_state.serialise = function(this) --state_serialiser.serialise
	local color
	if this.color then color = {this.color.R,this.color.G,this.color.B} end
	return this.id,this.connections,color,this.size,this.shift,this.material,this.edits,this.welds,this.look
end
_state.deserialise = function(this,id,connections,color,size,shift,material,edits,welds,look) --state_serialiser.deserialise
	if color then this.color = Color3.new(table.unpack(color)) end
	this.id = id
	this.connections = connections
	this.size = size
	this.shift = shift
	this.material = material
	this.edits = edits
	this.welds = welds
	this.look = look
end

return module 