------------------------------------------------------------
-- 0. paths & helpers
------------------------------------------------------------
local vec , cs = modlib.vector , modlib.minetest.colorspec
local file     = modlib.file
local decode   = modlib.minetest.decode_png
local convert  = modlib.minetest.convert_png_to_argb8
local bit      = bit or require("bit")

local wpath = minetest.get_worldpath()
local wname = wpath:match("[^/\\]+$") or "world"
local ddir  = wpath .. "/goxel_tools"
core.mkdir(ddir)

local palfile = ddir .. "/" .. wname .. ".gpl"
local trim = function(s) return (s:gsub("^%s*(.-)%s*$", "%1")) end

------------------------------------------------------------
-- 1. palette helpers
------------------------------------------------------------
local texdirs={}
for _,m in ipairs(minetest.get_modnames()) do
  texdirs[#texdirs+1] = minetest.get_modpath(m).."/textures/"
end
local png_cache={}
local function find_tex(t)
  if type(t)~="string" or t=="" then return nil end
  local base=t:match("^[^%^%[]+") or t
  for _,d in ipairs(texdirs) do local p=d..base if file.exists(p) then return p end end
end
local function avg_png(path)
  if png_cache[path] then return unpack(png_cache[path]) end
  local fh=assert(io.open(path,"rb")); local img=decode(fh); fh:close(); convert(img)
  local sum=vec.new{r=0,g=0,b=0}; local a=0
  for _,px in ipairs(img.data) do
    local c=cs.from_number(px)
    local lin=vec.pow_scalar({r=c.r,g=c.g,b=c.b},2)
    sum=vec.add(sum, vec.multiply_scalar(lin,c.a)); a=a+c.a
  end
  if a==0 then a=1 end
  local avg=vec.new(sum):divide_scalar(a):apply(math.sqrt):add_scalar(0.5):floor()
  png_cache[path]={avg.r,avg.g,avg.b}; return avg.r,avg.g,avg.b
end
local function node_colour(def)
  local t=def.tiles or def.tile_images or {}
  if type(t)=="string" then t={t} end
  local sr,sg,sb,n=0,0,0,0
  for i=1,math.min(6,#t) do
    local e=t[i]; local name=(type(e)=="table") and e.name or e
    local p=find_tex(name)
    if p then local r,g,b=avg_png(p); sr,sg,sb,n=sr+r,sg+g,sb+b,n+1 end
  end
  if n==0 then return 127,127,127 end
  return math.floor(sr/n+0.5),math.floor(sg/n+0.5),math.floor(sb/n+0.5)
end

------------------------------------------------------------
-- 2. /save_palette  (ensures unique colours)
------------------------------------------------------------
minetest.register_chatcommand("save_palette",{
  privs={server=true},
  description="Generate palette with unique RGB entries for goxel_tools",
  func=function(pn)
    local used={}  -- 3‑level sparse set used[r][g][b]=true
    local function mark(r,g,b) used[r]=used[r] or {}; used[r][g]=used[r][g] or {}; used[r][g][b]=true end
    local function is_used(r,g,b) return used[r] and used[r][g] and used[r][g][b] end

    local f=assert(io.open(palfile,"w"))
    f:write("GIMP Palette\nName: "..wname.."\n#\n")

    local names={}; for n in pairs(minetest.registered_nodes) do names[#names+1]=n end
    table.sort(names)

    for _,node in ipairs(names) do
      local r,g,b=node_colour(minetest.registered_nodes[node])

      -- nudge to uniqueness
      local rr,gg,bb=r,g,b
      local step=1
      while is_used(rr,gg,bb) do
        rr = (rr + step) % 256
        if rr==r then
          gg = (gg + step) % 256
          if gg==g then bb = (bb + step) % 256 end
        end
      end
      mark(rr,gg,bb)
      f:write(("%3d %3d %3d %s\n"):format(rr,gg,bb,node))
    end
    f:close()
    minetest.chat_send_player(pn,"Unique palette saved to "..palfile)
  end})

------------------------------------------------------------
-- 3. palette loader & nearest colour
------------------------------------------------------------
local function load_palette()
  if not file.exists(palfile) then return nil end
  local pal={}
  for l in io.lines(palfile) do
    local r,g,b,n=l:match("(%d+)%s+(%d+)%s+(%d+)%s+(.+)")
    if r then pal[#pal+1]={r=tonumber(r),g=tonumber(g),b=tonumber(b),name=trim(n)} end
  end
  return (#pal>0) and pal or nil
end
local function nearest(r,g,b,pal,cache)
  local key=bit.bor(bit.lshift(r,16),bit.lshift(g,8),b)
  if cache[key] then return cache[key] end
  local best,d=pal[1].name,math.huge
  for _,e in ipairs(pal) do
    local dr,dg,db=r-e.r,g-e.g,b-e.b
    local dist=dr*dr+dg*dg+db*db
    if dist<d then d,best=dist,e.name end
  end
  cache[key]=best; return best
end

------------------------------------------------------------
-- 4. txt -> schematic (X Y Z  →  X Z Y)
------------------------------------------------------------
local function voxels_to_schem(vox)
  local minp,maxp={x=1e9,y=1e9,z=1e9},{x=-1e9,y=-1e9,z=-1e9}
  local pts={}
  for _,v in ipairs(vox) do
    local x=v.x
    local z=v.y
    local y=v.z
    pts[#pts+1]={x=x,y=y,z=z,node=v.node}
    minp.x,maxp.x=math.min(minp.x,x),math.max(maxp.x,x)
    minp.y,maxp.y=math.min(minp.y,y),math.max(maxp.y,y)
    minp.z,maxp.z=math.min(minp.z,z),math.max(maxp.z,z)
  end
  local sz={x=maxp.x-minp.x+1,y=maxp.y-minp.y+1,z=maxp.z-minp.z+1}
  local vol=sz.x*sz.y*sz.z
  local data={}
  for i=1,vol do data[i]={name="ignore",prob=254} end
  local function idx(x,y,z) return ((z-1)*sz.y + (y-1))*sz.x + x end
  for _,p in ipairs(pts) do
    local x=p.x-minp.x+1; local y=p.y-minp.y+1; local z=p.z-minp.z+1
    data[idx(x,y,z)]={name=p.node,prob=255}
  end
  return {size=sz,data=data}
end

------------------------------------------------------------
-- 5. convert / load / cache
------------------------------------------------------------
local schem_cache={}
local function convert_goxel(base,player)
  local txt=ddir.."/"..base..".txt"; if not file.exists(txt) then return nil end
  local pal=load_palette(); if not pal then minetest.chat_send_player(player,"Run /save_palette first."); return nil end
  local vox,lookup={},{}
  for l in io.lines(txt) do
    local gx,gy,gz,hex=l:match("([%-0-9]+)%s+([%-0-9]+)%s+([%-0-9]+)%s+([0-9a-fA-F]+)")
    if gx then
      local r=tonumber(hex:sub(1,2),16); local g=tonumber(hex:sub(3,4),16); local b=tonumber(hex:sub(5,6),16)
      vox[#vox+1]={x=tonumber(gx),y=tonumber(gy),z=tonumber(gz),
                   node=nearest(r,g,b,pal,lookup)}
    end
  end
  if #vox==0 then minetest.chat_send_player(player,"No voxels parsed."); return nil end
  local schem=voxels_to_schem(vox)
  local raw,err=minetest.serialize_schematic(schem,"lua",{lua_use_comments=false})
  if not raw then minetest.chat_send_player(player,"serialize failed: "..(err or "nil")); return nil end
  local f=assert(io.open(ddir.."/"..base..".lua","wb")); f:write(raw); f:close()
  schem_cache[base]=schem; return schem
end
local function load_lua(base)
  local path=ddir.."/"..base..".lua"; if not file.exists(path) then return nil end
  local raw=assert(io.open(path,"rb")):read("*a")
  local ok,sc=pcall(minetest.deserialize_schematic,raw,"lua")
  if ok then schem_cache[base]=sc; return sc end
end
local function get_schem(base,player)
  return schem_cache[base] or load_lua(base) or convert_goxel(base,player)
end

------------------------------------------------------------
-- 6. /place_goxel
------------------------------------------------------------
minetest.register_chatcommand("place_goxel",{
  privs={server=true}, params="<name> [x y z]",
  description="Place Goxel export (auto‑convert & cache)",
  func=function(pn,param)
    local a={}; for w in param:gmatch("%S+") do a[#a+1]=w end
    if #a==0 then minetest.chat_send_player(pn,"Usage: /place_goxel name [x y z]"); return end
    local name=a[1]
    local pos
    if #a>=4 then pos={x=tonumber(a[2]),y=tonumber(a[3]),z=tonumber(a[4])}
    else local p=minetest.get_player_by_name(pn); if not p then return end; pos=vector.round(p:get_pos()) end
    local schem=get_schem(name,pn); if not schem then return end
    minetest.place_schematic(pos,schem)
    minetest.chat_send_player(pn,"Placed "..name.." at "..minetest.pos_to_string(pos))
  end})
