local gimpBatch = require("Tree/GimpBatch/gimp_batch")
local nvtt = LoadModule("Tree/nvtt")
local json = require("dkjson")

-- by session we would like to don't extract the same file multiple times
main.treeCacheExtract = main.treeCacheExtract or { }
local cacheExtract = main.treeCacheExtract
local ignoreFilter = "^%[DNT"

if not loadStatFile then
	dofile("statdesc.lua")
end
loadStatFile("passive_skill_stat_descriptions.csd")

local function CalcOrbitAngles(nodesInOrbit)
	local orbitAngles = {}

	if nodesInOrbit == 16 then
		-- Every 30 and 45 degrees, per https://github.com/grindinggear/skilltree-export/blob/3.17.0/README.md
		orbitAngles = { 0, 30, 45, 60, 90, 120, 135, 150, 180, 210, 225, 240, 270, 300, 315, 330 }
	elseif nodesInOrbit == 40 then
		-- Every 10 and 45 degrees
		orbitAngles = { 0, 10, 20, 30, 40, 45, 50, 60, 70, 80, 90, 100, 110, 120, 130, 135, 140, 150, 160, 170, 180, 190, 200, 210, 220, 225, 230, 240, 250, 260, 270, 280, 290, 300, 310, 315, 320, 330, 340, 350 }
	else
		-- Uniformly spaced
		for i = 0, nodesInOrbit do
			orbitAngles[i + 1] = 360 * i / nodesInOrbit
		end
	end

	for i, degrees in ipairs(orbitAngles) do
		orbitAngles[i] = math.rad(degrees)
	end

	return orbitAngles
end

local function bits(int, s, e)
	return bit.band(bit.rshift(int, s), 2 ^ (e - s + 1) - 1)
end
local function toFloat(int)
	local s = (-1) ^ bits(int, 31, 31)
	local e = bits(int, 23, 30) - 127
	if e == -127 then
		return 0 * s
	end
	local m = 1
	for i = 0, 22 do
		m = m + bits(int, i, i) * 2 ^ (i - 23)
	end
	return s * m * 2 ^ e
end
local function getInt(f)
	local int = f:read(4)
	return bytesToInt(int)
end
local function getLong(f)
	local bytes = f:read(8)
	local a, b, c, d, e, f, g, h = bytes:byte(1, 8)
	return a + b * 256 + c * 65536 + d * 16777216 + e * 4294967296 + f * 1099511627776 + g * 281474976710656 + h * 72057594037927936
end
local function getFloat(f)
	return toFloat(getInt(f))
end
local function getUint16(f)
    -- Read 2 bytes
    local bytes = f:read(2)

    -- Convert the 2 bytes to an unsigned integer (little-endian)
    local b1, b2 = bytes:byte(1, 2)
    local uint16 = b1 + b2 * 256

    return uint16
end
local function round_to(num, decimal_places)
    local multiplier = 10 ^ decimal_places
    return math.floor(num * multiplier + 0.5) / multiplier
end

local function extractSheetFiles(allSheets, is4kEnabled, extraFiles)
	local filesToExtract = {}
	local seen = {}
	for _, sheet in ipairs(allSheets) do
		for icon in pairs(sheet.files) do
			if not seen[icon] then
				seen[icon] = true
				table.insert(filesToExtract, icon)
			end
			if is4kEnabled then
				local icon4k = icon:gsub("(.*/)([^/]+)$", "%14k/%2")
				if not seen[icon4k] then
					seen[icon4k] = true
					table.insert(filesToExtract, icon4k)
				end
			end
		end
	end
	if extraFiles then
		for _, file in ipairs(extraFiles) do
			if not seen[file] then
				seen[file] = true
				table.insert(filesToExtract, file)
			end
		end
	end
	if #filesToExtract > 0 then
		main.ggpk:ExtractList(filesToExtract, cacheExtract)
	end
end

local function print_table(t, indent)
    indent = indent or 0
    local prefix = string.rep("  ", indent)

    if type(t) ~= "table" then
        print(prefix .. tostring(t))
        return
    end

    for key, value in pairs(t) do
        if type(value) == "table" then
            print(prefix .. tostring(key) .. ": {")
            print_table(value, indent + 1)
            print(prefix .. "}")
        else
            print(prefix .. tostring(key) .. ": " .. tostring(value))
        end
    end
end

local function newSheet(name, startWidth, saturation)
	return {
		name = name,
		startWidth = startWidth,
		saturation = saturation,
		sprite = { },
		files = {}
	}
end
local function addToSheet(sheet, icon, section, metadata)
	sheet.files[icon] = sheet.files[icon] or {}
	if sheet.files[icon][section] then
		-- check if metadata alias already exists
		if metadata.alias then
			for _, meta in pairs(sheet.files[icon][section]) do
				if meta.alias == metadata.alias then
					return
				end
			end
		else
			for _, meta in pairs(sheet.files[icon][section]) do
				if meta.alias == nil then
					return
				end
			end
		end
	end
	sheet.files[icon][section] = sheet.files[icon][section] or {}
	table.insert(sheet.files[icon][section], metadata)
end

local function calculateDDSPack(sheet, from_base, to_base, is4kEnabled)
	local stackTextures = {}
	local ddsCoords = {}

	for icon, sections in pairsSortByKey(sheet.files) do
		local tex = Texture.new()
		local rc
		if is4kEnabled then
			local icon4k = icon:gsub("(.*/)([^/]+)$", "%14k/%2")
			rc = tex:Load(from_base .. string.lower(icon4k))
		end
		if not rc then
			rc = tex:Load(from_base .. string.lower(icon))
		end

		local info = tex:Info()
		local ident = string.format("%d_%d_%s", info.width, info.height, info.formatStr)

		if not stackTextures[ident] then
			stackTextures[ident] = {}
		end

		table.insert(stackTextures[ident], {
			tex = tex,
			icon = icon,
			sections = sections
		})
	end


	for iden, stackInfo in pairsSortByKey(stackTextures) do
		local stacks = {}
		local file = sheet.name .. "_" .. iden .. ".dds.zst"
		ddsCoords[file] = {}
		for position, stack in ipairs(stackInfo) do
			for _, metadata in pairs(stack.sections) do
				for _, meta in ipairs(metadata) do
					local icon = meta.alias or stack.icon
					ddsCoords[file][icon] = position
				end
			end
			table.insert(stacks, stack.tex)
		end
		local stackTex = Texture.new()
		local rc = stackTex:StackTextures(stacks)
		rc = stackTex:Save(to_base .. file)
	end
	sheet.ddsCoords = ddsCoords
end

local function parseUIImages(file)
	local text
	if main.ggpk.txt[file] then
		text = main.ggpk.txt[file]
	else
		text = convertUTF16to8(getFile(file))
		main.ggpk.txt[file] = text
	end
	
	local images = {}
	
	for line in text:gmatch("[^\r\n]+") do
		local index = 0
		local name = ""
		for field in line:gmatch('"?([^%s"]+)"?') do
			if index == 0 then
				name = string.lower(field)
				images[name] = {}
			elseif index ==1 then
				images[name]["path"] = string.lower(field)
			elseif index == 2 then
				images[name]["x"] = tonumber(field)
			elseif index == 3 then
				images[name]["y"] = tonumber(field)
			elseif index == 4 then
				images[name]["width"] = tonumber(field)
			elseif index == 5 then
				images[name]["height"] = tonumber(field)
			end
			index = index + 1
		end
	end
	printf("UI Images parsed")
	return images
end

--[[
	===== Extraction =====
	Extraction of passives tree from psg file
	workflow:
		- read data file
		- get psg file
		- parse psg file
			- check version (only support version 3 for now)
--]]

-- Set to true if you want to generate assets
local generateAssets = false
local use4kIfPossible = false
-- Find a way to get the default passive tree
local idPassiveTree = 'Default'
-- Find a way to get version
local basePath = GetWorkDir() .. "/../TreeData/"
local version = "0_4"
local path = basePath .. version .. "/"
local fileTree = path .. "tree.lua"

printf("Getting passives tree...")

local rowPassiveTree =  dat("passiveskilltrees"):GetRow("Id", idPassiveTree)

if rowPassiveTree == nil then
	printf("Passive tree not found")
	return
end

local psgFile = rowPassiveTree.PassiveSkillGraph .. ".psg"
local uiImagesFile = "art/uiimages1.txt"

printf("Extracting required assets (psg + ui images)...")
main.ggpk:ExtractList({psgFile, uiImagesFile}, cacheExtract)

-- parse UI Images
--printf("Getting UIImages ...")
local uiImages = parseUIImages(uiImagesFile)
-- uncomment next line if wanna print what we found
-- print_table(uiImages, 0)

--printf("Parsing passives tree " .. idPassiveTree .. " from " .. main.ggpk.oozPath .. psgFile)

local f = io.open(main.ggpk.oozPath .. psgFile, "rb")

-- validate version
local pgb_version = getUint16(f)
if pgb_version ~= 3 then
	printf("Version " .. version .. " not supported")
	return
end

f:read(11)

local psg = { 
	passives = { },
	groups = { },
}

--printf("Parsing passives...")
local passivesCount = getInt(f)

--printf("Passive count: " .. passivesCount)
for i = 1 , passivesCount do
	table.insert(psg.passives, getLong(f))
end

--printf("Parsing groups...")
local groupCount = getInt(f)

--printf("Group count: " .. groupCount)
for i = 1 , groupCount do
	local group = { 
		x = getFloat(f),
		y = getFloat(f),
		flags = getInt(f),
		unk1 = getInt(f),
		unk2 = f:read(1):byte(),
		passives = { },
	}

	local passiveCount = getInt(f)
	for j = 1, passiveCount do
		local passive = {
			id = getInt(f),
			radius = getInt(f),
			position = getInt(f),
			connections = { },
		}

		local connectionCount = getInt(f)

		for k = 1, connectionCount do
			table.insert(passive.connections, {
				id = getInt(f),
				radius = getInt(f),
			})
		end

		table.insert(group.passives, passive)
	end

	table.insert(psg.groups, group)
end

f:close()

--printf("Passives tree " .. idPassiveTree .. " parsed")

-- uncomment next line if wanna print what we found
-- print_table(psg, 0)

--[[
	===== Generation =====
	Generation of passives tree from psg file
	workflow:
		- generate classes
		- generate groups
		- generate nodes
		- generate sprites (with sprite sheet)
		- generate zoom levels
		- generate constants
--]]

-- we use functions to generate a new table and not shared table
local function commonMetadata(alias)
	local metadata = {
		alias = alias,
	}
	return metadata
end

local defaultMaxWidth = 1500
local sheets = {
	newSheet("skills",  defaultMaxWidth, 100),
	newSheet("skills-disabled", defaultMaxWidth, 60),
	newSheet("background", defaultMaxWidth, 100),
	newSheet("group-background", defaultMaxWidth, 100),
	newSheet("mastery-active-effect", defaultMaxWidth, 100),
	newSheet("ascendancy-background", defaultMaxWidth, 100),
	newSheet("oils", defaultMaxWidth, 100),
	newSheet("lines", defaultMaxWidth, 100),
	newSheet("jewel-sockets", defaultMaxWidth, 100),
	newSheet("legion", defaultMaxWidth, 100),
	newSheet("monster-categories", defaultMaxWidth, 100),
}
local sheetLocations = {
	["skills"] = 1,
	["skills-disabled"] = 2,
	["background"] = 3,
	["group-background"] = 4,
	["mastery-active-effect"] = 5,
	["ascendancy-background"] = 6,
	["oils"] = 7,
	["lines"] = 8,
	["jewel-sockets"] = 9,
	["legion"] = 10,
	["monster-categories"] = 11,
}
local connectionArtToDecompose = {
	Character = true,
	CharacterAscendancy = true,
	--AbyssLichAscendancy = true, Currently breaks the GIMP mask script so can't include it atm
	CharacterPlanned = true,
}

local linesFiles = {}
local linesDds = {}
do
	local typeOfConnections = {
		"Normal", "Intermediate", "IntermediateActive"
	}

	for connectionArtId in pairs(connectionArtToDecompose) do
		local connectionArt = dat("PassiveSkillTreeConnectionArt"):GetRow("Id", connectionArtId)
		if connectionArt == nil then
			printf("Connection art " .. connectionArtId .. " not found")
		else
			for _, typeName in ipairs(typeOfConnections) do
				local linesFile = {
					file = connectionArt[typeName],
					mask = connectionArt.Mask,
					extension = ".png",
					basename = connectionArtId .. "_orbit_" .. string.lower(typeName),
					first = connectionArtId .. "LineConnector",
					prefix = connectionArtId .. "Orbit",
					postfix = typeName == "IntermediateActive" and "Active" or typeName,
					total = 10
				}
				table.insert(linesFiles, linesFile)
			end
		end
	end

	for _, lines in ipairs(linesFiles) do
		table.insert(linesDds, lines.file)
		table.insert(linesDds, lines.mask)
	end
end

local function getSheet(sheetLocation)
	return sheets[sheetLocations[sheetLocation]]
end

-- Looking for Background2
--printf("Extracting Background2...")
local bg2 = uiImages["art/2dart/uiimages/common/background2"]
if not bg2 then
	printf("Background2 not found")
	goto final
end

-- for support we needs to _out.dds when .dds
addToSheet(getSheet("background"), bg2.path, "background", commonMetadata("Background2"))

-- add Group Background base ond UIArt from PassiveTree\
--printf("Getting Background Group...")
local uIArt = rowPassiveTree.UIArt

local gBgSmall = uiImages[string.lower(uIArt.GroupBackgroundSmall)].path
addToSheet(getSheet("group-background"), gBgSmall, "groupBackground", commonMetadata("PSGroupBackground1"))

local gBgMedium = uiImages[string.lower(uIArt.GroupBackgroundMedium)].path
addToSheet(getSheet("group-background"), gBgMedium, "groupBackground", commonMetadata("PSGroupBackground2"))

local gBgLarge = uiImages[string.lower(uIArt.GroupBackgroundLarge)].path
addToSheet(getSheet("group-background"), gBgLarge, "groupBackground", commonMetadata("PSGroupBackground3"))

--printf("Getting PassiveFrame")
local pFrameNormal = uiImages[string.lower(uIArt.PassiveFrame.Normal)].path
addToSheet(getSheet("group-background"), pFrameNormal, "frame", commonMetadata("PSSkillFrame"))

local pFrameActive = uiImages[string.lower(uIArt.PassiveFrame.Active)].path
addToSheet(getSheet("group-background"), pFrameActive, "frame", commonMetadata("PSSkillFrameActive"))

local pFrameCanAllocate = uiImages[string.lower(uIArt.PassiveFrame.CanAllocate)].path
addToSheet(getSheet("group-background"), pFrameCanAllocate, "frame", commonMetadata("PSSkillFrameHighlighted"))

--printf("Getting KeystoneFrame")
local kFrameNormal = uiImages[string.lower(uIArt.KeystoneFrame.Normal)].path
addToSheet(getSheet("group-background"), kFrameNormal, "frame", commonMetadata("KeystoneFrameUnallocated"))

local kFrameActive = uiImages[string.lower(uIArt.KeystoneFrame.Active)].path
addToSheet(getSheet("group-background"), kFrameActive, "frame", commonMetadata("KeystoneFrameAllocated"))

local kFrameCanAllocate = uiImages[string.lower(uIArt.KeystoneFrame.CanAllocate)].path
addToSheet(getSheet("group-background"), kFrameCanAllocate, "frame", commonMetadata("KeystoneFrameCanAllocate"))

--printf("Getting NotableFrame")
local nFrameNormal = uiImages[string.lower(uIArt.NotableFrame.Normal)].path
addToSheet(getSheet("group-background"), nFrameNormal, "frame", commonMetadata("NotableFrameUnallocated"))

local nFrameActive = uiImages[string.lower(uIArt.NotableFrame.Active)].path
addToSheet(getSheet("group-background"), nFrameActive, "frame", commonMetadata("NotableFrameAllocated"))

local nFrameCanAllocate = uiImages[string.lower(uIArt.NotableFrame.CanAllocate)].path
addToSheet(getSheet("group-background"), nFrameCanAllocate, "frame", commonMetadata("NotableFrameCanAllocate"))

--printf("Getting GroupBackgroundBlank")
local gBgSmallBlank = uiImages[string.lower(uIArt.GroupBackgroundSmall)].path
addToSheet(getSheet("group-background"), gBgSmallBlank, "groupBackground", commonMetadata("PSGroupBackgroundSmallBlank"))

local gBgMediumBlank = uiImages[string.lower(uIArt.GroupBackgroundMedium)].path
addToSheet(getSheet("group-background"), gBgMediumBlank, "groupBackground", commonMetadata("PSGroupBackgroundMediumBlank"))

local gBgLargeBlank = uiImages[string.lower(uIArt.GroupBackgroundLarge)].path
addToSheet(getSheet("group-background"), gBgLargeBlank, "groupBackground", commonMetadata("PSGroupBackgroundLargeBlank"))

--printf("Getting JewelSocketFrame")
local jFrameNormal = uiImages[string.lower(uIArt.JewelFrame.CanAllocate)].path
addToSheet(getSheet("group-background"), jFrameNormal, "frame", commonMetadata("JewelFrameCanAllocate"))

local jFrameActive = uiImages[string.lower(uIArt.JewelFrame.Active)].path
addToSheet(getSheet("group-background"), jFrameActive, "frame", commonMetadata("JewelFrameAllocated"))

local jFrameCanAllocate = uiImages[string.lower(uIArt.JewelFrame.Normal)].path
addToSheet(getSheet("group-background"), jFrameCanAllocate, "frame", commonMetadata("JewelFrameUnallocated"))

--print("Getting Orbits art")
connectionArtToDecompose[uIArt.ConnectionsArt.Id] = true

--printf("Getting Ascendancy frames")
local ascMiddle = uiImages[string.lower("Art/2DArt/UIImages/InGame/PassiveSkillScreenAscendancyMiddle")].path
addToSheet(getSheet("group-background"), ascMiddle, "frame", commonMetadata("AscendancyMiddle"))

local ascStart = uiImages[string.lower("Art/2DArt/UIImages/InGame/PassiveSkillScreenStartNodeBackgroundInactive")].path
addToSheet(getSheet("group-background"), ascStart, "startNode", commonMetadata("PSStartNodeBackgroundInactive"))

-- adding passive tree assets
addToSheet(getSheet("ascendancy-background"), "art/textures/interface/2d/2dart/uiimages/ingame/passivetree/passivetreemaincircle.dds", "AscendancyBackground", commonMetadata("BGTree"))
addToSheet(getSheet("ascendancy-background"), "art/textures/interface/2d/2dart/uiimages/ingame/passivetree/passivetreemaincircleactive2.dds", "AscendancyBackground", commonMetadata("BGTreeActive"))

-- adding jewel sockets
local jewelArt = dat("PassiveJewelArt")
for jewel in jewelArt:Rows() do
	if jewel.Item.Name:find(ignoreFilter) ~= nil then
		printf("Ignoring jewel socket " .. jewel.Item.Name)
		goto nexttogo
	end
	local asset = uiImages[string.lower(jewel.JewelArt)]
	local name = jewel.Item.Name
	--printf("Adding jewel socket " .. name .. " " .. asset.path .. " to sprite")
	addToSheet(getSheet("jewel-sockets"), asset.path, "jewelpassive", commonMetadata(name))
	:: nexttogo	::
end

-- adding unique jewel sockets
local uniqueJewelArt = dat("PassiveJewelUniqueArt")
for jewel in uniqueJewelArt:Rows() do
	if jewel.WordsKey.Text:find(ignoreFilter) ~= nil then
		printf("Ignoring unique jewel socket " .. jewel.Item.Name)
		goto nexttogo
	end
	local asset = uiImages[string.lower(jewel.JewelArt)]
	local name = jewel.WordsKey.Text
	--printf("Adding unique jewel socket " .. name .. " " .. asset.path .. " to sprite")
	addToSheet(getSheet("jewel-sockets"), asset.path, "jewelpassive", commonMetadata(name))
	:: nexttogo ::
end

-- adding monster types
local monsterCategories = dat("MonsterCategories")
for category in monsterCategories:Rows() do
	if category.Type:find(ignoreFilter) ~= nil then
		printf("Ignoring category" .. category.Type)
		goto nexttogo
	end
	local asset = uiImages[string.lower(category.HudImage)]
	--printf("Adding category " .. category.Type .. " " .. asset.path .. " to sprite")
	local name = category.Type
	addToSheet(getSheet("monster-categories"), asset.path, "monster-categories", commonMetadata(name))
	:: nexttogo	::
end

-- adding legion assets
for legion in dat("AlternatePassiveSkills"):Rows() do
	addToSheet(getSheet("legion"), legion.DDSIcon, "legion", commonMetadata(legion.DDSIcon))
end


local artsToLegion = {
	"Art/2DArt/UIImages/InGame/Abyss/AbyssPassiveSkillScreenJewelCircle1",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenEternalEmpireJewelCircle1",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenEternalEmpireJewelCircle2",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenKalguuranJewelCircle1",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenKalguuranJewelCircle2",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenKaruiJewelCircle1",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenKaruiJewelCircle2",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenMarakethJewelCircle1",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenMarakethJewelCircle2",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenTemplarJewelCircle1",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenTemplarJewelCircle2",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenVaalJewelCircle1",
	"Art/2DArt/UIImages/InGame/PassiveSkillScreenVaalJewelCircle2",
}

for _, art in ipairs(artsToLegion) do
	local asset = uiImages[string.lower(art)]
	addToSheet(getSheet("legion"), asset.path, "legion", commonMetadata(asset.path))
end

local tree = {
	["tree"] = idPassiveTree,
	["min_x"]= 0,
	["min_y"]= 0,
	["max_x"]= 0,
	["max_y"]= 0,
	["classes"] = {},
	["groups"] = { },
	["nodes"]= { },
	["assets"]={},
	["ddsCoords"] = {},
	["jewelSlots"] = {},
	["constants"]= { -- calculate this
		["classes"]= {
			["StrDexIntClass"]= 0,
			["StrClass"]= 1,
			["DexClass"]= 2,
			["IntClass"]= 3,
			["StrDexClass"]= 4,
			["StrIntClass"]= 5,
			["DexIntClass"]= 6
		},
		["characterAttributes"]= {
			["Strength"]= 0,
			["Dexterity"]= 1,
			["Intelligence"]= 2
		},
		["PSSCentreInnerRadius"]= 130,
		["skillsPerOrbit"]= {},
		["orbitAnglesByOrbit"] = {},
		["orbitRadii"]= {
			0, 82, 162, 335, 493, 662, 846, 251, 1080, 1322
		},
	},
	["nodeOverlay"] = {
		Normal = {
			alloc = "PSSkillFrameActive",
			path = "PSSkillFrameHighlighted",
			unalloc = "PSSkillFrame",
		},
		Notable = {
			alloc = "NotableFrameAllocated",
			path = "NotableFrameCanAllocate",
			unalloc = "NotableFrameUnallocated",
		},
		Keystone = {
			alloc = "KeystoneFrameAllocated",
			path = "KeystoneFrameCanAllocate",
			unalloc = "KeystoneFrameUnallocated",
		},
		Socket = {
			alloc = "JewelFrameAllocated",
			path = "JewelFrameCanAllocate",
			unalloc = "JewelFrameUnallocated",
		},
	},
	["connectionArt"] = {
		default = "Character",
		ascendancy = "CharacterAscendancy",
	},
}

printf("Generating classes...")
local ascedancyReplacements = {}
for i, classId in ipairs(psg.passives) do
	local passiveRow = dat("passiveskills"):GetRow("PassiveSkillNodeId", classId)
	if passiveRow == nil then
		printf("Class " .. passiveRow.id .. " not found")
		goto continue
	end

	if passiveRow.Name:find(ignoreFilter) ~= nil then
		--printf("Ignoring class " .. passiveRow.Name)
		goto continue
	end

	local listCharacters = passiveRow.ClassStart
	
	if listCharacters == nil then
		printf("Characters not found")
		goto continue
	end

	for j, character in ipairs(listCharacters) do
		if character.Name:find(ignoreFilter) ~= nil then
			--printf("Ignoring character " .. character.Name)
			goto continue2
		end
		local classDef = {
			["name"] = character.Name,
			["integerId"] = character.IntegerId,
			["base_str"] = character.BaseStrength,
			["base_dex"] = character.BaseDexterity,
			["base_int"] = character.BaseIntelligence,
			["ascendancies"] = {},
			["background"] = {
				["active"] = { width = 2000, height = 2000 },
				["bg"] = { width = 2000, height = 2000 },
				image = "Classes" .. character.Name,
				section = "AscendancyBackground",
				x = 0,
				y = 0,
				width = 1500,
				height = 1500
			}
		}

		-- add assets
		addToSheet(getSheet("ascendancy-background"), character.PassiveTreeImage, "AscendancyBackground", commonMetadata( "Classes" .. character.Name))

		-- We are going to ignore for now, because current tree doesn't use that
		--addToSheet(getSheet("group-background"), uiImages[string.lower(character.SkillTreeBackground)].path, "startNode", commonMetadata( "center" .. string.lower(character.Name)))

		local ascendancies = dat("ascendancy"):GetRowList("Class", character)
		for k, ascendency in ipairs(ascendancies) do
			if ascendency.Name:find(ignoreFilter) ~= nil or ascendency.isDisabled then
				--printf("Ignoring ascendency " .. ascendency.Name .. " for class " .. character.Name)
				goto continue3
			end
			if ascendency.Replace then
				ascedancyReplacements[ascendency.Replace.Name] = ascendency.Name
			end
			table.insert(classDef.ascendancies, {
				["id"] = ascendency.Name,
				["name"] = ascendency.Name,
				["internalId"] = ascendency.Id,
				["replace"] = ascendency.Replace and ascendency.Replace.Name or nil,
				["background"] = {
					image = "Classes" .. ascendency.Name,
					section = "AscendancyBackground",
					x = 0,
					y = 0,
					width = 1500,
					height = 1500
				}
			})

			-- add assets
			addToSheet(getSheet("ascendancy-background"), ascendency.PassiveTreeImage, "AscendancyBackground", commonMetadata( "Classes" .. ascendency.Name))

			--printf("Getting Ascendancy frames " .. ascendency.Name)
			local ascFrameNormal = uiImages[string.lower(ascendency.UIArt.PassiveFrame.CanAllocate)].path
			addToSheet(getSheet("group-background"), ascFrameNormal, "frame", commonMetadata(ascendency.Name .. "FrameSmallCanAllocate"))

			local ascFrameActive = uiImages[string.lower(ascendency.UIArt.PassiveFrame.Normal)].path
			addToSheet(getSheet("group-background"), ascFrameActive, "frame", commonMetadata(ascendency.Name .. "FrameSmallNormal"))

			local ascFrameCanAllocate = uiImages[string.lower(ascendency.UIArt.PassiveFrame.Active)].path
			addToSheet(getSheet("group-background"), ascFrameCanAllocate, "frame", commonMetadata(ascendency.Name .. "FrameSmallAllocated"))

			local ascFrameLargeNormal = uiImages[string.lower(ascendency.UIArt.NotableFrame.Normal)].path
			addToSheet(getSheet("group-background"), ascFrameLargeNormal, "frame", commonMetadata(ascendency.Name .. "FrameLargeNormal"))

			local ascFrameLargeCanAllocate = uiImages[string.lower(ascendency.UIArt.NotableFrame.CanAllocate)].path
			addToSheet(getSheet("group-background"), ascFrameLargeCanAllocate, "frame", commonMetadata(ascendency.Name .. "FrameLargeCanAllocate"))

			local ascFrameLargeAllocated = uiImages[string.lower(ascendency.UIArt.NotableFrame.Active)].path
			addToSheet(getSheet("group-background"), ascFrameLargeAllocated, "frame", commonMetadata(ascendency.Name .. "FrameLargeAllocated"))
			:: continue3 ::
		end

		if #classDef.ascendancies == 0 then
			--printf("No ascendancies found for class " .. character.Name)
			goto continue2
		end
		table.insert(tree.classes,classDef)
		:: continue2 ::
	end
	:: continue ::
end


-- for now we are hardcoding attributes id
local base_attributes = {
	[26297] = {}, -- str
	[14927] = {}, -- dex
	[57022] = {}--int
}

for id, _ in pairs(base_attributes) do
	local base = dat("passiveskills"):GetRow("PassiveSkillNodeId", id)
	if base == nil then
		printf("Base attribute " .. id .. " not found")
		goto continue
	end

	if base.Name:find(ignoreFilter) ~= nil then
		printf("Ignoring base attribute " .. base.Name)
		goto continue
	end

	local attribute = {
		["name"] = base.Name,
		["icon"] = base.Icon,
		["stats"] = {},
	}

	-- Stats
	if base.Stats ~= nil then
		local parseStats = {}
		for k, stat in ipairs(base.Stats) do
			parseStats[stat.Id] = { min = base["Stat" .. k], max = base["Stat" .. k] }
		end
		local out, orders = describeStats(parseStats)
		for k, line in ipairs(out) do
			table.insert(attribute["stats"], line)
		end
	end

	addToSheet(getSheet("skills"), base.Icon, "normalActive", commonMetadata(nil))
	base_attributes[id] = attribute
	:: continue ::
end

printf("Generating tree groups...")

local orbitsConstants = { }
local ascendancyGroups = {}
local missingStatInfo = {}
for i, group in ipairs(psg.groups) do
	local groupIsAscendancy = false
	local treeGroup = {
		["x"] = round_to(group.x, 2),
		["y"] = round_to(group.y, 2),
		["orbits"] ={},
		["nodes"] = {}
	}

	local orbits = { }
	for j, passive in ipairs(group.passives) do
		local node = {
			["skill"] = passive.id,
			["group"] = i,
			["orbit"] = passive.radius,
			["orbitIndex"] = passive.position,
			["connections"] = {},	
		}

		-- Get Information from passive Skill
		local passiveRow = dat("passiveskills"):GetRow("PassiveSkillNodeId", passive.id)
		if passiveRow == nil then
			--printf("Passive skill " .. passive.id .. " not found")
		else
			if passiveRow.Name == "" then
				--printf("Ignoring passive skill " .. passive.id .. " No name")
				goto exitNode
			end
			if passiveRow.Name:find(ignoreFilter) ~= nil and passiveRow.Name ~= "[DNT] Kite Fisher" and passiveRow.Name ~= "[DNT] Troller" and passiveRow.Name ~= "[DNT] Spearfisher" and passiveRow.Name ~= "[DNT] Angler" and passiveRow.Name ~= "[DNT] Whaler" then
				--printf("Ignoring passive skill " .. passiveRow.Name)
				goto exitNode
			end
			--printf("Passive skill " .. passiveRow.Name .. "(id: " .. passiveRow.Id .. ") found")
			node["name"] = escapeGGGString(passiveRow.Name)
			node["icon"] = passiveRow.Icon
			if passiveRow.FlavourText ~= "" then
				node["flavourText"] = passiveRow.FlavourText:gsub('\r',''):gsub('\n','\\n')
			end
			if passiveRow.Keystone then
				node["isKeystone"] = true
				addToSheet(getSheet("skills"), passiveRow.Icon, "keystoneActive", commonMetadata(nil))
				addToSheet(getSheet("skills-disabled"), passiveRow.Icon, "keystoneInactive", commonMetadata(nil))
			elseif passiveRow.Notable then
				node["isNotable"] = true
				addToSheet(getSheet("skills"), passiveRow.Icon, "notableActive", commonMetadata(nil))
				addToSheet(getSheet("skills-disabled"), passiveRow.Icon, "notableInactive", commonMetadata(nil))
			elseif passiveRow.IsOnlyImage then
				node["isOnlyImage"] = true
			elseif passiveRow.JewelSocket then
				node["isJewelSocket"] = true
				addToSheet(getSheet("skills"), passiveRow.Icon, "socketActive", commonMetadata(nil))
				addToSheet(getSheet("skills-disabled"), passiveRow.Icon, "socketInactive", commonMetadata(nil))
			else
				addToSheet(getSheet("skills"), passiveRow.Icon, "normalActive", commonMetadata(nil))
				addToSheet(getSheet("skills-disabled"), passiveRow.Icon, "normalInactive", commonMetadata(nil))
			end

			-- Ascendancy
			if passiveRow.Ascendancy ~= nil then
				groupIsAscendancy = true
				if passiveRow.Ascendancy.Name:find(ignoreFilter) ~= nil or passiveRow.Ascendancy.isDisabled then
					--printf("Ignoring node ascendancy " .. passiveRow.Ascendancy.Name)
					goto exitNode
				end
				node["ascendancyName"] = passiveRow.Ascendancy.Name
				node["isAscendancyStart"] = passiveRow.AscendancyStart or nil
				if node["isAscendancyStart"] then
					node["name"] = passiveRow.Ascendancy.Name
				end

				-- support for jewel sockets in ascendancy
				if passiveRow.JewelSocket then
					node["containJewelSocket"] = true

					local uioverride = passiveRow.Ascendancy.UIArt.JewelFrame
					if uioverride then
						local uiSocketNormal = uiImages[string.lower(uioverride.Normal)]
						addToSheet(getSheet("group-background"), uiSocketNormal.path, "frame", commonMetadata(nil))

						local uiSocketActive = uiImages[string.lower(uioverride.Active)]
						addToSheet(getSheet("group-background"), uiSocketActive.path, "frame", commonMetadata(nil))

						local uiSocketCanAllocate = uiImages[string.lower(uioverride.CanAllocate)]
						addToSheet(getSheet("group-background"), uiSocketCanAllocate.path, "frame", commonMetadata(nil))

						node.nodeOverlay = {
							alloc = uiSocketActive.path,
							path = uiSocketCanAllocate.path,
							unalloc = uiSocketNormal.path,
						}
						
					else
						--printf("Jewel socket not found for ascendancy " .. passiveRow.Ascendancy.Name)
					end
				elseif node["isOnlyImage"] == nil then
					local typeFrame = node["isNotable"] and "Large" or "Small"
					node.nodeOverlay = {
						alloc = passiveRow.Ascendancy.Name .. "Frame" .. typeFrame .. "Allocated",
						path = passiveRow.Ascendancy.Name .. "Frame" .. typeFrame .. "CanAllocate",
						unalloc = passiveRow.Ascendancy.Name .. "Frame" .. typeFrame .. "Normal",
					}
				end

				ascendancyGroups = ascendancyGroups or {}
				ascendancyGroups[passiveRow.Ascendancy.Name] = ascendancyGroups[passiveRow.Ascendancy.Name] or { }
				ascendancyGroups[passiveRow.Ascendancy.Name].startId = passiveRow.AscendancyStart and passive.id or ascendancyGroups[passiveRow.Ascendancy.Name].startId
				ascendancyGroups[passiveRow.Ascendancy.Name][i] = true
			end

			-- enable custom frameArt by node
			if passiveRow.FrameArt ~= nil and node["isOnlyImage"] == nil then
				local frameArt = passiveRow.FrameArt
				if frameArt ~= nil then
					local uiNormal = uiImages[string.lower(frameArt.Normal)]
					addToSheet(getSheet("group-background"), uiNormal.path, "frame", commonMetadata(nil))

					local uiActive = uiImages[string.lower(frameArt.Active)]
					addToSheet(getSheet("group-background"), uiActive.path, "frame", commonMetadata(nil))

					local uiCanAllocate = uiImages[string.lower(frameArt.CanAllocate)]
					addToSheet(getSheet("group-background"), uiCanAllocate.path, "frame", commonMetadata(nil))

					node.nodeOverlay = {
						alloc = uiActive.path,
						path = uiCanAllocate.path,
						unalloc = uiNormal.path,
					}
				end
			end

			-- Enable Ascendancy Unlock
			if passiveRow.AscendancyUnlock ~= nil then
				node.unlockConstraint = {
					ascendancy = passiveRow.AscendancyUnlock.Name,
					nodes = {}
				}

				for id, refNode in ipairs(passiveRow.ConstraintNode) do
					--printf(" - adding node " .. refNode.Name .. " to unlock constraint")
					node.unlockConstraint.nodes[id] = refNode.PassiveSkillNodeId
				end

				-- enable the alternative connectionArt for this node
				node.connectionArt = "CharacterPlanned"
			end

			-- Stats
			if passiveRow.Stats ~= nil then
				node["stats"] = node["stats"] or {}
				local parseStats = {}
				local totalStats = 0
				local namesStats = ""
				for k, stat in ipairs(passiveRow.Stats) do
					parseStats[stat.Id] = { min = passiveRow["Stat" .. k], max = passiveRow["Stat" .. k] }
					totalStats = totalStats + 1
					namesStats = namesStats .. stat.Id .. " | "
				end
				local out, orders, missing = describeStats(parseStats)
				if #out < totalStats then
					table.insert(missingStatInfo, "====================================")
					table.insert(missingStatInfo,"Stats not found for passive " .. passiveRow.Name .. " " .. passive.id)
					table.insert(missingStatInfo,"Stats found: " .. totalStats)
					table.insert(missingStatInfo,namesStats)
					table.insert(missingStatInfo,"Stats out: " .. #out)
					for k, line in ipairs(out) do
						table.insert(missingStatInfo,line)
					end
					table.insert(missingStatInfo,"Missing: ")
					for k, _ in pairs(missing) do
						if k ~= 1 then
							table.insert(missingStatInfo,k)
						end
					end
					table.insert(missingStatInfo,"====================================")
				end
				for k, line in ipairs(out) do
					table.insert(node["stats"], line)
				end
			end

			-- support for images
			if passiveRow.MasteryGroup ~= nil then
				node["activeEffectImage"] = passiveRow.MasteryGroup.MasteryArt.Effect

				local uiEffect = uiImages[string.lower(passiveRow.MasteryGroup.MasteryArt.Effect)]
				addToSheet(getSheet("mastery-active-effect"), uiEffect.path, "masteryActiveEffect", commonMetadata(passiveRow.MasteryGroup.MasteryArt.Effect))
			end

			-- if the passive is "Attribute" we are going to add values
			if passiveRow.Attribute == true then
				node["options"] = {}
				node["isAttribute"] = true
				for attId, value in pairs(base_attributes) do
					table.insert(node["options"], {
						["id"] = attId,
						["name"] = base_attributes[attId].name,
						["icon"] = base_attributes[attId].icon,
						["stats"] = base_attributes[attId].stats,
					})
				end
			end

			-- support for granted skills
			if passiveRow.GrantedSkill ~= nil then
				node["stats"] = node["stats"] or {}

				for _, gemEffect in pairs(passiveRow.GrantedSkill.GemEffects) do
					local skillName = passiveRow.GrantedSkill.BaseItemType.Name
					table.insert(node["stats"], "Grants Skill: " .. skillName)

					-- -- include the stat description
					local statDescription =string.sub(string.lower(gemEffect.GrantedEffect.ActiveSkill.StatDescription), 1, -5)
					local handle = NewFileSearch("ggpk/" .. statDescription ..".csd")
					local almostOnce = false
					while handle do
						almostOnce = true
						--print(statDescription:gsub("Data/StatDescriptions", "") .. ".csd")
						-- loadStatFile(statDescription:gsub("Data/StatDescriptions/", "") .. ".csd")
						if not handle:NextFile() then
							break
						end
					end
					if not almostOnce then
						table.insert(missingStatInfo, "===================================>Missing stat" .. statDescription)
					end
				end
			end

			-- support for Passive Points Granted
			if passiveRow.PassivePointsGranted > 0 then
				node["stats"] = node["stats"] or {}
				table.insert(node["stats"], "Grants ".. passiveRow.PassivePointsGranted .." Passive Skill Point")
			end

			-- support for Weapon points granted
			if passiveRow.WeaponPointsGranted > 0 then
				node["stats"] = node["stats"] or {}
				table.insert(node["stats"],  passiveRow.WeaponPointsGranted .." Passive Skill Points become Weapon Set Skill Points")
			end

			-- support for oils
			local bResult = dat("blightcraftingresults"):GetRow("PassiveSkillsKey", passiveRow)

			if bResult ~= nil then
				node["recipe"] = {}
				local bCraftRecipe = dat("blightcraftingrecipes"):GetRow("BlightCraftingResultsKey", bResult)
				if bCraftRecipe ~= nil then
					for _, item in ipairs(bCraftRecipe.Recipe) do
						table.insert(node["recipe"], item.NameShort)

						-- add to sprite sheet
						addToSheet(getSheet("oils"), item.Oil.ItemVisualIdentityKey.DDSFile, "oil", commonMetadata(item.NameShort))
					end
				end
			end

			-- support for switchnodes
			local switchNode = dat("classpassiveskilloverrides"):GetRow("OriginalNode", passiveRow)
			if switchNode ~= nil then
				node["isSwitchable"] = true
				local nodeInfo = {
					["id"] = switchNode.SwitchedNode.PassiveSkillNodeId,
					["name"] = switchNode.SwitchedNode.Name,
					["icon"] = switchNode.SwitchedNode.Icon,
					["stats"] = {},
				}

				-- add to assets
				addToSheet(getSheet("skills"), switchNode.SwitchedNode.Icon, "normalActive", commonMetadata(nil))
				addToSheet(getSheet("skills-disabled"), switchNode.SwitchedNode.Icon, "normalInactive", commonMetadata(nil))

				-- Stats
				if switchNode.SwitchedNode.Stats ~= nil then
					local parseStats = {}
					for k, stat in ipairs(switchNode.SwitchedNode.Stats) do
						parseStats[stat.Id] = { min = switchNode.SwitchedNode["Stat" .. k], max = switchNode.SwitchedNode["Stat" .. k] }
					end
					local out, orders = describeStats(parseStats)
					for k, line in ipairs(out) do
						table.insert(nodeInfo["stats"], line)
					end
				end

				node["options"] = {
					[switchNode.Character.Name] = nodeInfo
				}
			end

			if passiveRow.Ascendancy and ascedancyReplacements[passiveRow.Ascendancy.Name] then
				node["isSwitchable"] = true
				local ascendancyRow = dat("ascendancy"):GetRow("Name", ascedancyReplacements[passiveRow.Ascendancy.Name])

				local nodeInfo = {
					["ascendancyName"] = ascedancyReplacements[passiveRow.Ascendancy.Name]
				}

				local switchNode = dat("ascendancypassiveskilloverrides"):GetRow("OriginalNode", passiveRow)
				if switchNode then
					nodeInfo.id = switchNode.SwitchedNode.PassiveSkillNodeId
					nodeInfo.name = switchNode.SwitchedNode.Name
					nodeInfo.icon = switchNode.SwitchedNode.Icon
					nodeInfo.stats = {}

					-- add to assets
					addToSheet(getSheet("skills"), switchNode.SwitchedNode.Icon, "normalActive", commonMetadata(nil))
					addToSheet(getSheet("skills-disabled"), switchNode.SwitchedNode.Icon, "normalInactive", commonMetadata(nil))

					-- Stats
					if switchNode.SwitchedNode.Stats ~= nil then
						local parseStats = {}
						for k, stat in ipairs(switchNode.SwitchedNode.Stats) do
							parseStats[stat.Id] = { min = switchNode.SwitchedNode["Stat" .. k], max = switchNode.SwitchedNode["Stat" .. k] }
						end
						local out, orders = describeStats(parseStats)
						for k, line in ipairs(out) do
							table.insert(nodeInfo["stats"], line)
						end
					end
				end

				if passiveRow.JewelSocket and ascendancyRow.UIArt.JewelFrame then
					-- override the jewel socket assets if any
					local uioverride = ascendancyRow.UIArt.JewelFrame
					
					local uiSocketNormal = uiImages[string.lower(uioverride.Normal)]
					addToSheet(getSheet("group-background"), uiSocketNormal.path, "frame", commonMetadata(nil))

					local uiSocketActive = uiImages[string.lower(uioverride.Active)]
					addToSheet(getSheet("group-background"), uiSocketActive.path, "frame", commonMetadata(nil))

					local uiSocketCanAllocate = uiImages[string.lower(uioverride.CanAllocate)]
					addToSheet(getSheet("group-background"), uiSocketCanAllocate.path, "frame", commonMetadata(nil))

					nodeInfo.nodeOverlay = {
						alloc = uiSocketActive.path,
						path = uiSocketCanAllocate.path,
						unalloc = uiSocketNormal.path,
					}
				elseif node["isOnlyImage"] == nil then
					local typeFrame = nodeInfo["isNotable"] and "Large" or "Small"
					nodeInfo.nodeOverlay = {
						alloc = ascendancyRow.Name .. "Frame" .. typeFrame .. "Allocated",
						path = ascendancyRow.Name .. "Frame" .. typeFrame .. "CanAllocate",
						unalloc = ascendancyRow.Name .. "Frame" .. typeFrame .. "Normal",
					}
				end



				node["options"] = {
					[ascedancyReplacements[passiveRow.Ascendancy.Name]] = nodeInfo
				}
			end

			-- classStartName
			if #passiveRow.ClassStart > 0 then
				node["classesStart"] = {}
				for _, classStart in ipairs(passiveRow.ClassStart) do
					if classStart.Name:find(ignoreFilter) == nil then
						table.insert(node["classesStart"], classStart.Name)
					end
				end
			end

			-- Multiple Choice and MultipleChoiceOption support
			if passiveRow.MultipleChoice then
				node["isMultipleChoice"] = true
			end
			if passiveRow.MultipleChoiceOption then
				node["isMultipleChoiceOption"] = true
			end

			-- Support for Smith of Kitava
			if passiveRow["FreeAllocate"] == true then
				node["isFreeAllocate"] = true
			end

			if passiveRow["ApplyToArmour?"] == true then
				node["applyToArmour"] = true
			end
		end
		
		for k, connection in ipairs(passive.connections) do
			table.insert(node.connections, {
				id = connection.id,
				orbit = connection.radius,
			})
		end

		orbits[passive.radius + 1] = true
		orbitsConstants[passive.radius + 1] = math.max(orbitsConstants[passive.radius + 1] or 1, passive.position)
		tree.nodes[passive.id] = node
		table.insert(treeGroup.nodes, passive.id)
		:: exitNode ::
	end

	for orbit, _ in pairs(orbits) do
		table.insert(treeGroup.orbits, orbit - 1)
	end

	if #treeGroup.nodes > 0 then
		tree.groups[i] = treeGroup

		if not groupIsAscendancy then
			tree.min_x = math.min(tree.min_x, group.x)
			tree.min_y = math.min(tree.min_y, group.y)
			tree.max_x = math.max(tree.max_x, group.x)
			tree.max_y = math.max(tree.max_y, group.y)
		end
	else
		--("Group " .. i .. " is empty")
	end
end

MakeDir(basePath .. version)
-- write file with missing Stats
if #missingStatInfo > 0 then
	local file = io.open(basePath .. version .. "/missingStats.txt", "w")
	for _, line in ipairs(missingStatInfo) do
		file:write(line .. "\n")
	end
	file:close()
end

-- Generating jewel slots
printf("Generating jewel slots...")
local jewelSlots = dat("passivejewelslots")
for jewelSlot in jewelSlots:Rows() do
	table.insert(tree.jewelSlots, jewelSlot.Passive.PassiveSkillNodeId)
end

-- updating skillsPerOrbit
--printf("Updating skillsPerOrbit...")
for i, orbit in ipairs(orbitsConstants) do
	-- only numbers base on 12
	orbit = i == 1 and orbit or math.ceil(orbit / 12) * 12
	tree.constants.skillsPerOrbit[i] = orbit
end

-- calculate the orbit radius
for orbit, skillsInOrbit in ipairs(tree.constants.skillsPerOrbit) do
	tree.constants.orbitAnglesByOrbit[orbit] = CalcOrbitAngles(skillsInOrbit)
end

-- Update position of ascendancy base on min / max 
-- get the orbit radius + hard-coded value, calculate the angle of the class start
-- translate the ascendancy to the new position in arc position
local widthTree, heightTree = tree.max_x - tree.min_x, tree.max_y - tree.min_y
local radiusTree = (math.max(widthTree, heightTree) + 700) / 2
local arcAngle = { [0] = 0, [1] = 0, [2] = 12, [3] = 24, [4] = 36, [5] = 48, [6] = 60, [7] = 72, [8] = 84, [9] = 96 }
for i, classId in ipairs(psg.passives) do
	local nodeStart = tree.nodes[classId]
	local group = tree.groups[nodeStart.group]
	local angleToCenter = math.atan2(group.y, group.x)
	local hardCoded = radiusTree + 2800

	-- calculate how many ascendancies in that place?
	local total = 0
	local classes = {}

	for _, class in ipairs(tree.classes) do
		for _, nodeClasses in ipairs(nodeStart.classesStart) do
			if nodeClasses == class.name then
				total = total + #class.ascendancies
				table.insert(classes, class)
			end
		end
	end

	local startAngle = angleToCenter - math.rad(arcAngle[total] / 2)
	local angleStep = math.rad(arcAngle[total] / (total - 1)) or 0
	local j = 1
	for _, class in ipairs(classes) do
		for _, ascendancy in ipairs(class.ascendancies) do
			--printf("Positioning ascendancy " .. ascendancy.name .. " for class " .. class.name)
			
			local angle = startAngle + (j - 1) * angleStep
			local cX = hardCoded * math.cos(angle)
			local cY = hardCoded * math.sin(angle)

			local info = ascendancyGroups[ascendancy.id]
			if info == nil then
				printf("Ascendancy group " .. ascendancy.id .. " not found")
				goto continuepositioning
			end
			local ascendancyNode = tree.nodes[info.startId]
			if ascendancyNode == nil then
				printf("Ascendancy node " .. ascendancy.id .. " not found")
				goto continuepositioning
			end

			ascendancy.background.x = cX
			ascendancy.background.y = cY

			local groupAscendancy = tree.groups[ascendancyNode.group]

			local innerRadious = dat("ascendancy"):GetRow("Id", ascendancy.internalId).distanceTree

			local newInnerX = cX + math.cos(angleToCenter) * innerRadious
			local newInnerY = cY + math.sin(angleToCenter) * innerRadious

			local nodeAngle = tree.constants.orbitAnglesByOrbit[ascendancyNode.orbit + 1][ascendancyNode.orbitIndex + 1]
			local orbitRadius = tree.constants.orbitRadii[ascendancyNode.orbit + 1]
			local newX = newInnerX - math.sin(nodeAngle) * orbitRadius
			local newY = newInnerY + math.cos(nodeAngle) * orbitRadius

			local offsetX = newX - groupAscendancy.x
			local offsetY = newY - groupAscendancy.y

			-- now update the whole groups with the offset
			for groupId, value in pairs(info) do
				if type(value) == "boolean" then
					local group = tree.groups[groupId]
					group.x = group.x + offsetX
					group.y = group.y + offsetY

					-- recalculate min / max
					tree.min_x = math.min(tree.min_x, group.x - hardCoded / 2)
					tree.min_y = math.min(tree.min_y, group.y - hardCoded / 2)
					tree.max_x = math.max(tree.max_x, group.x + hardCoded / 2)
					tree.max_y = math.max(tree.max_y, group.y + hardCoded / 2)
				end
			end

			j = j + 1
			:: continuepositioning ::
		end
	end
end

printf("Fixing replace ascendancies position...")
for from, to in pairs(ascedancyReplacements) do
	local fromAscendancy
	local toAscendancy
	for _, class in ipairs(tree.classes) do
		for _, ascendancy in ipairs(class.ascendancies) do
			if ascendancy.name == from then
				fromAscendancy = ascendancy
			end
			if ascendancy.name == to then
				toAscendancy = ascendancy
			end
		end
	end

	if fromAscendancy == nil then
		printf("From ascendancy " .. from .. " not found")
		goto continuereplace
	end
	if toAscendancy == nil then
		printf("To ascendancy " .. to .. " not found")
		goto continuereplace
	end

	fromAscendancy.replaceBy = to

	toAscendancy.background.x = fromAscendancy.background.x
	toAscendancy.background.y = fromAscendancy.background.y

	:: continuereplace ::
end


--printf("Generating sprite info...")
extractSheetFiles(sheets, use4kIfPossible, linesDds)
for i, sheet in ipairs(sheets) do
	--printf("Calculating sprite dimensions for " .. sheet.name)
	calculateDDSPack(sheet, main.ggpk.oozPath, basePath .. version .. "/", use4kIfPossible)

	for file, fileInfo in pairs(sheet.ddsCoords) do
		tree.ddsCoords[file] = fileInfo
	end
end

printf("Convert DDS line files to PNG...")
nvtt.ExportDDSToPng(main.ggpk.oozPath, basePath .. version .. "/", "lines", linesDds, true)

-- change extension from dds to png
for _, lines in ipairs(linesFiles) do
	lines.file = lines.file:gsub(".dds", ".png")
	lines.mask = lines.mask:gsub(".dds", ".png")
end

gimpBatch.extract_lines_from_image("lines_extract", linesFiles, main.ggpk.oozPath, basePath .. version .. "/", GetScriptPath() .. "/Tree/GimpBatch/extract_lines.scm", generateAssets)

--printf("generate lines info into assets")
-- Generate sprites
for _, lines in ipairs(linesFiles) do
	local curveOrbitFile = 9
	for i = 0, lines.total - 1 do
		local name
		local middle
		if i == 0 then
			name = lines.first .. lines.postfix
			middle = 0
		elseif i == 3 then
			curveOrbitFile = curveOrbitFile - 1
			middle = curveOrbitFile
			curveOrbitFile = curveOrbitFile - 1
			name = lines.prefix .. i .. lines.postfix
		elseif i == 7 then
			middle = 7
			name = lines.prefix .. i .. lines.postfix
		else
			name = lines.prefix .. i .. lines.postfix
			middle = curveOrbitFile

			curveOrbitFile = curveOrbitFile - 1
		end

		tree.assets[name] = {
			lines.basename .. middle .. lines.extension
		}
	end
end

printf("Generating lua tree file")
local out, err = io.open(fileTree, "w")
if out == nil then
	printf("Error opening file " .. fileTree)
	printf(err)
	return
end
out:write('return ')
writeLuaTable(out, tree, 1)
out:close()
--("File " .. fileTree .. " generated")

local fileTreeJson = fileTree:gsub(".lua", ".json")
printf("Generating JSON tree file")
local out, err = io.open(fileTreeJson, "w")
if out == nil then
	printf("Error opening file " .. fileTreeJson)
	printf(err)
	return
end
out:write(json.encode(tree))
out:close()
printf("Skill tree generated")
:: final ::