local partStateModule = require(script.Parent.PartStateModule)
local saves = {}
local id_parts = {}
local parts_loaded
spawn(function()
	game.ReplicatedStorage:WaitForChild("Parts")
	local function added(object)
		if object.Parent:IsA("Folder") then
			local id = object:GetAttribute("PartId")
			if id then id_parts[id] = object end
		end
	end
	for _,object in pairs(_G.Storage.Parts:GetDescendants()) do
		added(object)
	end
	_G.Storage.Parts.DescendantAdded:Connect(added)
	wait(1)
	parts_loaded = true
end)
do
	local pairs = pairs
	local ipairs = ipairs
	local isa = Instance.new("Part").IsA
	local no_cframe = {}
	local cframe_of = function(object) 
		if isa(object,"BasePart") then
			return object.CFrame
		elseif isa(object,"Model") then
			return (object.PrimaryPart or no_cframe)["CFrame"] 
		end 
	end
	local cframe_set = function(object,cframe)
		if isa(object,"Model") then
			object:SetPrimaryPartCFrame(cframe)
		elseif isa(object,"BasePart") then
			object.CFrame = cframe
		end
	end
	local weld_is_build = {["BuiltWeldConstraint"] = true}
	local insert = table.insert
	local typeof = typeof
	local is_table = {["table"]=true}
	local id_module = _G.Methods.Ids
	local colorPart = _G.Methods.colorPart
	local ts = tostring
	local tn = tonumber
	local format = string.format
	local concat = table.concat
	local cframe_new = CFrame.new
	local vector3_new = Vector3.new
	local table_create = table.create
	local form_triangle = partStateModule.form_triangle
	local get_nodes = partStateModule.get_nodes
	local apply_nodes = partStateModule.apply_nodes
	local apply_shift = partStateModule.apply_shift
	local apply_size = partStateModule.apply_size
	local apply_material = partStateModule.apply_material
	local apply_internal_welds = partStateModule.apply_internal_welds
	local hexxie = require(script.Parent.HexxieModule)
	local hd = hexxie.decode
	local he = hexxie.encode
	local hexxie_cframe_match = string.rep("(...........)",12)
	local hexxie_position_match = string.rep("(...........)",3)
	local default_cframe_format = string.rep("%s,",10).."%s"
	local id_attach = {}
	local compress_cframe_decode = function(s)
		if not s then return nil end
		local a,b,c,d,e,f,g,h,i,j,k,l = s:match(hexxie_cframe_match)
		return cframe_new(hd(a),hd(b),hd(c),hd(d),hd(e),hd(f),hd(g),hd(h),hd(i),hd(j),hd(k),hd(l))
	end
	local compress_cframe_encode = function(c)
		local a,b,c,d,e,f,g,h,i,j,k,l = c:GetComponents()
		return concat({he(a),he(b),he(c),he(d),he(e),he(f),he(g),he(h),he(i),he(j),he(k),he(l)})
	end
	local compress_position_decode = function(s)
		local a,b,c = s:match(hexxie_position_match)
		return vector3_new(hd(a),hd(b),hd(c))
	end
	local compress_position_encode = function(v)
		return concat({he(v.X),he(v.Y),he(v.Z)})
	end
	local position
	local http = game:GetService("HttpService")
	local http_json_encode = http.JSONEncode
	local http_json_decode = http.JSONDecode
	local json_decode = function(value)
		return http_json_decode(http,value)
	end
	local json_encode = function(value)
		return http_json_encode(http,value)
	end
	local ID_INDEX = 1
	local CFRAMES_INDEX = 2
	local EDIT_INDEX = 3
	saves["4"] = {
		decode = function(raw,options)
			--print(raw)
			repeat wait() until parts_loaded
			local final = {errors = {}}
			local function err(text)
				warn(text)
				table.insert(final.errors,text)
			end
			options = options or {}
			local _compress_cframe_decode = compress_cframe_decode
			local _compress_position_decode = compress_position_decode
			if options.mirror then
				_compress_cframe_decode = function(str) 
					local cf = compress_cframe_decode(str)
					local x,y,z = cf:ToEulerAnglesYXZ()
					return CFrame.fromEulerAnglesYXZ(x,-y,-z)+Vector3.new(cf.X,-cf.Y,cf.Z)
				end
				_compress_position_decode = function(str)
					local pos = compress_position_decode(str)
					return vector3_new(-pos.X,pos.Y,pos.Z)
				end
			end
			local compressed = raw:match("\"compress\"%:true")
			if compressed then raw = hexxie.rle_decode(raw) end
			local data = _G.Http:JSONDecode(raw)
			_G.Union(options,data.options or {})
			local physics = options.physics
			local template_cframe = Instance.new("CFrameValue")
			local weld_template = Instance.new("WeldConstraint")
			local built_weld_template = weld_template:Clone()
			built_weld_template.Name = "BuiltWeldConstraint"
			local model = Instance.new("Model")
			final.model = model
			local parts = {}
			local editActions = {
				c = function(edit,part)
					local color = Color3.new(unpack(edit))
					colorPart(part,color)
				end,
				h = function(edit,part)
					local hinge = part:FindFirstChild("Hinge")
					if hinge then hinge.CFrame = compress_cframe_decode(edit) end
				end,
				r = function(edit,part)
					local slider = part:FindFirstChild("Slider")
					if slider then slider.CFrame = compress_cframe_decode(edit) end
				end,
				m = function(edit,part)
					if edit then apply_material(part,Enum.Material[edit]) end
				end,
				s = function(edit,part)
					apply_size(part,Vector3.new(unpack(edit)))
				end,
				S = function(edit,part)
					apply_shift(part,edit)
				end,
			}
			local poly_to_internally_weld = {}
			local data_cframes = data.cframes
			local data_edits = data.edits
			for id,object in pairs(data.objects) do
				local template = id_parts[object[ID_INDEX]]
				if not template then err("Missing part with id "..tostring(id)) continue end
				local instance = template:Clone()
				instance.Parent = model
				parts[id] = instance
				
				local nodes = {}
				local transform_ids = object[CFRAMES_INDEX]
				-- BEGIN PARITY 4.1 (CAN BE REMOVED LATER)
				if typeof(transform_ids) ~= "table" then transform_ids = {transform_ids} end
				-- END PARITY 4.1
				if #transform_ids == 1 then
					insert(nodes,compress_cframe_decode(data_cframes[transform_ids[1]]))
				elseif #transform_ids == 2 then
					insert(nodes,compress_cframe_decode(data_cframes[transform_ids[1]]))
					insert(nodes,hd(data_cframes[transform_ids[2]]))
				else
					for _,id in ipairs(transform_ids) do
						insert(nodes,compress_position_decode(data_cframes[id]))
					end
					insert(poly_to_internally_weld,instance)
				end
				apply_nodes(instance,nodes)
				-- Edit part
				if object[EDIT_INDEX] then
					for _,edit_id in pairs(object[EDIT_INDEX]) do
						local key,value = unpack(data_edits[edit_id]);
						(editActions[key] or function(value,instance)
							local edit = instance:FindFirstChild(key)
							if not edit then
								err(string.format("No key %s for %s",key,instance:GetFullName()))
							else
								if typeof(value) == "table" then value = _G.Http:JSONEncode(value) end
								edit.Value = value
							end
						end)(value,instance)
					end
				end
				-- Change anchoring
				if not options.physics then
					if instance:IsA("Model") then
						instance.PrimaryPart.Anchored = true
					else
						instance.Anchored = true
					end
				end
			end
			if options.physics then
				for _,weld in pairs(data.welds) do
					local w = built_weld_template:Clone()
					local at,bt = unpack(weld)
					local ap = parts[at[1]]
					local bp = parts[bt[1]]
					w.Parent = ap
					if ap:IsA("Model") then
						if at[2] then
							ap = ap:FindFirstChild(at[2]) or ap.PrimaryPart
						else
							ap = ap.PrimaryPart
							err("Missing subpart reference for weld on model A")
						end
					end
					if bp:IsA("Model") then
						if bt[2] then
							bp = bp:FindFirstChild(bt[2]) or bp.PrimaryPart
						else
							bp = bp.PrimaryPart
							err("Missing subpart reference for weld on model B")
						end
					end
					w.Part0 = ap
					w.Part1 = bp
				end
				for _,poly in ipairs(poly_to_internally_weld) do
					apply_internal_welds(poly)
				end
			else
				for _,part in pairs(model:GetDescendants()) do
					if part:IsA("BasePart") then part.CanCollide = false part.Anchored = true end
				end
			end
			local root = parts[1]
			if not root then error("Fatal: Missing root part; implying #parts = 0") end
			if root:IsA("Model") then root = root.PrimaryPart end
			model.PrimaryPart = root
			final.model = model
			return final
		end,
		encode = function(root,options)
			repeat wait() until parts_loaded
			local final = {errors = {}}
			local function err(text)
				warn(text)
				table.insert(final.errors,text)
			end
			id_module.forEachConnectedObject(root,function(object)
				if object:IsA("VehicleSeat") then root = object end
			end)
			final.root = root

			if not options then options = {} end

			local origin = cframe_of(root)
			if not origin then err("Root CFrame could not be encoded, it is probably broken") return final end

			local ids = {}

			local cframes = {}
			local edits = {}
			local welds = {}
			local objects = {}
			local data = {
				options = options,
				objects = objects,
				cframes = cframes,
				edits = edits,
				welds = welds
			}

			local should_optimise_cframe = not not options.optimise_cframe
			local cframe_epsilon = options.cframe_epsilon or 0.1 
			local optimise_edit = {}
			local optimise_cframe = {}

			local function cframe_id(c)
				local existing = optimise_cframe[c]
				if not existing then
					insert(cframes,c)
					local len = #cframes
					optimise_cframe[c] = len
					return len
				else
					return existing
				end
			end

			local function edit_id(key,value)
				local str
				if type(value) == "table" then
					str = json_encode(value)
				elseif type(value) ~="string" then
					str =  tostring(value)
				else
					str = value
				end
				local tab = optimise_edit[key]
				if not tab then tab = {} optimise_edit[key] = tab end
				if tab[str] then
					return tab[str]
				else
					insert(edits,{key,value})
					tab[str] = #edits
					return #edits
				end
			end

			local errors = {}
			
			local index = 0
			id_module.forEachConnectedObject(root,function(object)
				index = index + 1
				ids[object] = index

				local id = object:GetAttribute("PartId") or object.Name
				if not id then insert(errors,string.format("Part without an ID! %s",ts(object))) end

				-- ENCODE TRANSFORM
				local transform = get_nodes(object)
				if #transform >= 3 then
					for index,position in pairs(transform) do
						transform[index] = cframe_id(compress_position_encode(origin:PointToObjectSpace(position)))
					end
				elseif #transform == 2 then
					transform[1] = cframe_id(compress_cframe_encode(origin:ToObjectSpace(transform[1])))
					transform[2] = cframe_id(he(transform[2]))
				elseif #transform == 1 then
					transform[1] = cframe_id(compress_cframe_encode(origin:ToObjectSpace(transform[1])))
				end

				-- ENCODE EDITS
				local edits = {}
				for index,edit in pairs(object:GetChildren()) do
					if edit:HasTag("Edit") or edit:HasTag("EditHidden") then
						if edit:HasTag("JSON") then
							insert(edits,edit_id(edit.Name,json_decode(edit.Value)))
						else
							local value = edit.Value
							if (edit:GetAttribute("Default") ~= value) then
								insert(edits,edit_id(edit.Name,value))
							end
						end
					end
				end

				-- ENCODE CANDID DISPLACEMENT
				local hinge = object:FindFirstChild("Hinge")
				if hinge then insert(edits,edit_id("h",compress_cframe_encode(origin:ToObjectSpace(hinge.CFrame)))) end
				local slider = object:FindFirstChild("Slider")
				if slider then insert(edits,edit_id("r",compress_cframe_encode(origin:ToObjectSpace(slider.CFrame)))) end
				
				-- ENCODE PROPERTIES
				local color = object:GetAttribute("Color") -- TODO HEXXIE OPTIMISE COLOUR AND SIZE
				if color then insert(edits,edit_id("c",{color.R,color.G,color.B})) end
				local material = object:GetAttribute("Material")
				if material then insert(edits,edit_id("m",material)) end
				local size = object:GetAttribute("Size")
				if size then insert(edits,edit_id("s",{size.X,size.Y,size.Z})) end
				local shift = object:GetAttribute("Shift")
				if shift then insert(edits,edit_id("S",shift)) end

				-- INSERT OBJECT
				local data = {id,transform}
				if #edits > 0 then insert(data,edits) end

				insert(objects,data)
			end)

			-- ENCODE WELDS
			for object,id in pairs(ids) do
				for _,weld in pairs(object:GetDescendants()) do
					if weld_is_build[weld.Name] then
						if weld.Part0 and weld.Part1 then
							local first = {ids[weld.Part0]}
							if not first[1] then 
								first[1] = ids[weld.Part0.Parent]
								first[2] = weld.Part0.Name
								if not first[1] then err("There was a stray first weld! "..tostring(weld:GetFullName())) continue end
							end
							local second = {ids[weld.Part1]}
							if not second[1] then 
								second[1] = ids[weld.Part1.Parent]
								second[2] = weld.Part1.Name
								if not second[1] then err("There was a stray second weld! "..tostring(weld:GetFullName())) continue end
							end
							insert(data.welds,{first,second})
						else
							err("There was a dead weld!"..tostring(weld:GetFullName()))
						end
					end
				end 
			end

			local json = _G.Http:JSONEncode(data)
			local result = json
			if options.compress then result=hexxie.rle_encode(json) end
			local check = hexxie.rle_decode(result)
			result = "v4save"..result
			final.data = result
			return final
		end,
	}
end
return saves