local Blur = {}

-- Canvas cache to avoid recreating canvases every frame
local canvasCache = {}
local MAX_CACHE_SIZE = 20

-- Quad cache to avoid recreating quads every frame
local quadCache = {}
local MAX_QUAD_CACHE_SIZE = 20

-- Quad cache to avoid recreating quads every frame
local quadCache = {}
local MAX_QUAD_CACHE_SIZE = 20

--- Build Gaussian blur shader with given parameters
---@param taps number -- Number of samples (must be odd, >= 3)
---@param offset number
---@param offset_type string -- "weighted" or "center"
---@param sigma number
---@return love.Shader
local function buildShader(taps, offset, offset_type, sigma)
  taps = math.floor(taps)
  sigma = sigma >= 1 and sigma or (taps - 1) * offset / 6
  sigma = math.max(sigma, 1)

  local steps = (taps + 1) / 2

  local g_offsets = {}
  local g_weights = {}
  for i = 1, steps, 1 do
    g_offsets[i] = offset * (i - 1)
    g_weights[i] = math.exp(-0.5 * (g_offsets[i] - 0) ^ 2 * 1 / sigma ^ 2)
  end

  -- Calculate offsets and weights for sub-pixel samples
  local offsets = {}
  local weights = {}
  for i = #g_weights, 2, -2 do
    local oA, oB = g_offsets[i], g_offsets[i - 1]
    local wA, wB = g_weights[i], g_weights[i - 1]
    wB = oB == 0 and wB / 2 or wB
    local weight = wA + wB
    offsets[#offsets + 1] = offset_type == "center" and (oA + oB) / 2 or (oA * wA + oB * wB) / weight
    weights[#weights + 1] = weight
  end

  local code = { [[
    extern vec2 direction;
    vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {]] }

  local norm = 0
  if #g_weights % 2 == 0 then
    code[#code + 1] = "vec4 c = vec4( 0.0 );"
  else
    local weight = g_weights[1]
    norm = norm + weight
    code[#code + 1] = ("vec4 c = %f * texture2D(tex, tc);"):format(weight)
  end

  local tmpl = "c += %f * ( texture2D(tex, tc + %f * direction)+ texture2D(tex, tc - %f * direction));\n"
  for i = 1, #offsets, 1 do
    local offset = offsets[i]
    local weight = weights[i]
    norm = norm + weight * 2
    code[#code + 1] = tmpl:format(weight, offset, offset)
  end
  code[#code + 1] = ("return c * vec4(%f) * color; }"):format(1 / norm)

  local shader = table.concat(code)
  return love.graphics.newShader(shader)
end

--- Get or create a canvas from cache
---@param width number
---@param height number
---@return love.Canvas
local function getCanvas(width, height)
  local key = string.format("%dx%d", width, height)

  if not canvasCache[key] then
    canvasCache[key] = {}
  end

  local cache = canvasCache[key]

  for i, canvas in ipairs(cache) do
    if not canvas.inUse then
      canvas.inUse = true
      return canvas.canvas
    end
  end

  local canvas = love.graphics.newCanvas(width, height)
  table.insert(cache, { canvas = canvas, inUse = true })

  if #cache > MAX_CACHE_SIZE then
    table.remove(cache, 1)
  end

  return canvas
end

--- Release a canvas back to the cache
---@param canvas love.Canvas
local function releaseCanvas(canvas)
  for _, sizeCache in pairs(canvasCache) do
    for _, entry in ipairs(sizeCache) do
      if entry.canvas == canvas then
        entry.inUse = false
        return
      end
    end
  end
end

--- Get or create a quad from cache
---@param x number
---@param y number
---@param width number
---@param height number
---@param sw number -- Source width
---@param sh number -- Source height
---@return love.Quad
local function getQuad(x, y, width, height, sw, sh)
  local key = string.format("%d,%d,%d,%d,%d,%d", x, y, width, height, sw, sh)

  if not quadCache[key] then
    quadCache[key] = {}
  end

  local cache = quadCache[key]

  for i, entry in ipairs(cache) do
    if not entry.inUse then
      entry.inUse = true
      return entry.quad
    end
  end

  local quad = love.graphics.newQuad(x, y, width, height, sw, sh)
  table.insert(cache, { quad = quad, inUse = true })

  if #cache > MAX_QUAD_CACHE_SIZE then
    table.remove(cache, 1)
  end

  return quad
end

--- Release a quad back to the cache
---@param quad love.Quad
local function releaseQuad(quad)
  for _, keyCache in pairs(quadCache) do
    for _, entry in ipairs(keyCache) do
      if entry.quad == quad then
        entry.inUse = false
        return
      end
    end
  end
end

--- Create a blur effect instance
---@param quality number -- Quality level (1-10, higher = better quality but slower)
---@return table -- Blur effect instance
function Blur.new(quality)
  quality = math.max(1, math.min(10, quality or 5))

  -- Map quality to shader parameters
  -- Quality 1: 3 taps (fastest, lowest quality)
  -- Quality 5: 7 taps (balanced)
  -- Quality 10: 15 taps (slowest, highest quality)
  local taps = 3 + (quality - 1) * 1.5
  taps = math.floor(taps)
  if taps % 2 == 0 then
    taps = taps + 1 -- Ensure odd number
  end

  local offset = 1.0
  local offset_type = "weighted"
  local sigma = -1

  local shader = buildShader(taps, offset, offset_type, sigma)

  local instance = {
    shader = shader,
    quality = quality,
    taps = taps,
  }

  return instance
end

--- Apply blur to a region of the screen
---@param blurInstance table -- Blur effect instance from Blur.new()
---@param intensity number -- Blur intensity (0-100)
---@param x number -- X position
---@param y number -- Y position
---@param width number -- Width
---@param height number -- Height
---@param drawFunc function -- Function to draw content to be blurred
function Blur.applyToRegion(blurInstance, intensity, x, y, width, height, drawFunc)
  if intensity <= 0 or width <= 0 or height <= 0 then
    drawFunc()
    return
  end

  intensity = math.max(0, math.min(100, intensity))

  -- Intensity 0-100 maps to 0-5 passes
  local passes = math.ceil(intensity / 20)
  passes = math.max(1, math.min(5, passes))

  local canvas1 = getCanvas(width, height)
  local canvas2 = getCanvas(width, height)

  local prevCanvas = love.graphics.getCanvas()
  local prevShader = love.graphics.getShader()
  local prevColor = { love.graphics.getColor() }
  local prevBlendMode = love.graphics.getBlendMode()

  love.graphics.setCanvas(canvas1)
  love.graphics.clear()
  love.graphics.push()
  love.graphics.origin()
  love.graphics.translate(-x, -y)
  drawFunc()
  love.graphics.pop()

  love.graphics.setShader(blurInstance.shader)
  love.graphics.setColor(1, 1, 1, 1)
  love.graphics.setBlendMode("alpha", "premultiplied")

  for i = 1, passes do
    love.graphics.setCanvas(canvas2)
    love.graphics.clear()
    blurInstance.shader:send("direction", { 1 / width, 0 })
    love.graphics.draw(canvas1, 0, 0)

    love.graphics.setCanvas(canvas1)
    love.graphics.clear()
    blurInstance.shader:send("direction", { 0, 1 / height })
    love.graphics.draw(canvas2, 0, 0)
  end

  love.graphics.setCanvas(prevCanvas)
  love.graphics.setShader()
  love.graphics.setBlendMode(prevBlendMode)
  love.graphics.draw(canvas1, x, y)

  love.graphics.setShader(prevShader)
  love.graphics.setColor(unpack(prevColor))

  releaseCanvas(canvas1)
  releaseCanvas(canvas2)
end

--- Apply backdrop blur effect (blur content behind a region)
---@param blurInstance table -- Blur effect instance from Blur.new()
---@param intensity number -- Blur intensity (0-100)
---@param x number -- X position
---@param y number -- Y position
---@param width number -- Width
---@param height number -- Height
---@param backdropCanvas love.Canvas -- Canvas containing the backdrop content
function Blur.applyBackdrop(blurInstance, intensity, x, y, width, height, backdropCanvas)
  if intensity <= 0 or width <= 0 or height <= 0 then
    return
  end

  intensity = math.max(0, math.min(100, intensity))

  local passes = math.ceil(intensity / 20)
  passes = math.max(1, math.min(5, passes))

  local canvas1 = getCanvas(width, height)
  local canvas2 = getCanvas(width, height)

  local prevCanvas = love.graphics.getCanvas()
  local prevShader = love.graphics.getShader()
  local prevColor = { love.graphics.getColor() }
  local prevBlendMode = love.graphics.getBlendMode()

  love.graphics.setCanvas(canvas1)
  love.graphics.clear()
  love.graphics.setColor(1, 1, 1, 1)
  love.graphics.setBlendMode("alpha", "premultiplied")

  local backdropWidth, backdropHeight = backdropCanvas:getDimensions()
  local quad = getQuad(x, y, width, height, backdropWidth, backdropHeight)
  love.graphics.draw(backdropCanvas, quad, 0, 0)

  love.graphics.setShader(blurInstance.shader)

  for i = 1, passes do
    love.graphics.setCanvas(canvas2)
    love.graphics.clear()
    blurInstance.shader:send("direction", { 1 / width, 0 })
    love.graphics.draw(canvas1, 0, 0)

    love.graphics.setCanvas(canvas1)
    love.graphics.clear()
    blurInstance.shader:send("direction", { 0, 1 / height })
    love.graphics.draw(canvas2, 0, 0)
  end

  love.graphics.setCanvas(prevCanvas)
  love.graphics.setShader()
  love.graphics.setBlendMode(prevBlendMode)
  love.graphics.draw(canvas1, x, y)

  love.graphics.setShader(prevShader)
  love.graphics.setColor(unpack(prevColor))

  releaseCanvas(canvas1)
  releaseCanvas(canvas2)
  releaseQuad(quad)
end

--- Clear canvas cache (call on window resize)
function Blur.clearCache()
  canvasCache = {}
  quadCache = {}
end

return Blur
