local sim = ac.getSim()

if sim.isVRMode or sim.isCustomVideoMode then
  ac.warn('Curved PP-filter is only available in single or triple screen mode')
  ac.setFilterActive(false)
  return
end

local canvasDebug ---@type ui.ExtraCanvas
local cameraRotation = vec2(-2.3, 1)
local cameraZoom = 0
local tripleState = ac.getTripleConfiguration()
local gAppliedFOV = -1
local gOptimalFOV = -1
local gFOVTrimmed = false
local shaderData = nil
local settingsClock = -1
local hoveredScreen = -1
local hoveredState = {1, 1, 1}

local config = ac.storage{
  originalAspectRatio = true,
  curvedPixelWidth = sim.windowWidth,
  curvedPixelHeight = sim.windowHeight,
  curvedSizeInches = 34,
  curvedRadius = 4,
  curvedPosBase = vec3(0, 0, 0),
  eyePosBase = vec3(0, 0, 0),
  flatActive = false,
  flatLeftWidth = 0.33,
  flatLeftHeight = 1,
  flatLeftGap = 0,
  flatLeftY = 0,
  flatLeftAngle = 45,
  flatRightWidth = 0.33,
  flatRightHeight = 1,
  flatRightGap = 0,
  flatRightY = 0,
  flatRightAngle = 45,
  pixelDensity = 1.2
}

ac.setMIPs(3)
ac.setPixelDensity(config.pixelDensity)

if config.eyePosBase.z >= 0 then
  config.eyePosBase.z = -ac.INIConfig.load(ac.getFolder(ac.FolderID.ACDocuments)..'/cfg/triple_screen.ini'):get('SPINNERS', 'SP_DISTANCE', 1300) / 1000
end

local function saveSettings()
  return stringify{
    originalAspectRatio = config.originalAspectRatio,
    curvedPixelWidth = config.curvedPixelWidth,
    curvedPixelHeight = config.curvedPixelHeight,
    curvedSizeInches = config.curvedSizeInches,
    curvedRadius = config.curvedRadius,
    curvedPosBase = config.curvedPosBase,
    eyePosBase = config.eyePosBase,
    flatActive = config.flatActive,
    flatLeftWidth = config.flatLeftWidth,
    flatLeftHeight = config.flatLeftHeight,
    flatLeftGap = config.flatLeftGap,
    flatLeftY = config.flatLeftY,
    flatLeftAngle = config.flatLeftAngle,
    flatRightWidth = config.flatRightWidth,
    flatRightHeight = config.flatRightHeight,
    flatRightGap = config.flatRightGap,
    flatRightY = config.flatRightY,
    flatRightAngle = config.flatRightAngle,
  }
end

local function getCurvedFlatWidth()
  return vec2.tmp():set(config.curvedPixelWidth, config.curvedPixelHeight):normalize():scale(config.curvedSizeInches * 0.0254).x
end

local function getCurvedWidth()
  local angle = 2 * math.asin(math.min(1, getCurvedFlatWidth() / (2 * config.curvedRadius)))
  return angle * config.curvedRadius
end

local function loadSettings(data)
  table.assign(config, stringify.tryParse(data, {}))
end

local function cross2d(a, b)
  return a.x * b.y - a.y * b.x
end

local function vecItem(vec, index)
  if index == 0 then return vec.x end
  if index == 1 then return vec.y end
  return vec.z
end

local ca, cb, cc, cp, cnor, cpos, cmor = vec3(), vec3(), vec3(), vec3(), vec3(), vec3(), vec3()
local ckp, cka, ckb, ckc, ckg, cma = vec2(), vec2(), vec2(), vec2(), vec2(), math.abs
local function quadIntersect(ro, rd, v0, v1, v2, v3, d)
  local a = ca:set(v1):sub(v0)
  local b = cb:set(v3):sub(v0)
  local c = cc:set(v2):sub(v0)
  local p = cp:set(ro):sub(v0)

  local nor = cnor:set(a):cross(b)
  local t = -p:dot(nor) / rd:dot(nor)
  if t < 0 then return false end

  local pos = cpos:set(p):addScaled(rd, t)
  local mor = cmor:set(cma(nor.x), cma(nor.y), cma(nor.z))
  local id = (mor.x > mor.y and mor.x > mor.z) and 0 or (mor.y > mor.z) and 1 or 2
  local idu = (id + 1) % 3
  local idv = (id + 2) % 3

  local kp = ckp:set(vecItem(pos, idu), vecItem(pos, idv))
  local ka = cka:set(vecItem(a, idu), vecItem(a, idv))
  local kb = ckb:set(vecItem(b, idu), vecItem(b, idv))
  local kc = ckc:set(vecItem(c, idu), vecItem(c, idv))

  local kg = ckg:set(kc):sub(kb):sub(ka)
  local k0 = cross2d(kp, kb)
  local k1 = cross2d(kp, kg) - vecItem(nor, id)

  local v = -k0 / k1
  local u = cross2d(kp, ka) / k1
  return u >= 0 and u <= 1 and v >= 0 and v <= 1
end

local cos = vec3()
local function getOnscreenPos(x, y, curvedAngle, curvedRadius, curvedHeight, curvedPos, flatProjectData)
  if flatProjectData then
    return cos:set(x == -1
      and (y == -1 and flatProjectData.gFlatLeft0 or flatProjectData.gFlatLeft1)
      or (y == -1 and flatProjectData.gFlatRight2 or flatProjectData.gFlatRight3))
  else
    local u = (x - 0.5) * curvedAngle
    return cos:set(math.sin(u) * curvedRadius, (0.5 - y) * curvedHeight, math.cos(u) * curvedRadius):add(curvedPos)
  end
end

local function reprojectFn(x, y, curvedAngle, curvedRadius, curvedHeight, curvedPos, corners, flatProjectData)
  local onScreen = getOnscreenPos(x, y, curvedAngle, curvedRadius, curvedHeight, curvedPos, flatProjectData)
  return quadIntersect(config.eyePosBase, onScreen:sub(config.eyePosBase):normalize(), corners[1], corners[2], corners[3], corners[4])
end

local function reprojectTripleFn(x, y, curvedAngle, curvedRadius, curvedHeight, curvedPos, tripleData, flatProjectData)
  local onScreen = getOnscreenPos(x, y, curvedAngle, curvedRadius, curvedHeight, curvedPos, flatProjectData)
  local dir = onScreen:sub(config.eyePosBase):normalize()
  return quadIntersect(config.eyePosBase, dir, tripleData.gTripleCornerL0, tripleData.gTripleCornerL1, tripleData.gTripleCornerL2, tripleData.gTripleCornerL3)
    or quadIntersect(config.eyePosBase, dir, tripleData.gTripleCornerR0, tripleData.gTripleCornerR1, tripleData.gTripleCornerR2, tripleData.gTripleCornerR3)
    or quadIntersect(config.eyePosBase, dir, tripleData.gTripleCornerC0, tripleData.gTripleCornerC1, tripleData.gTripleCornerC2, tripleData.gTripleCornerC3)
end

local fc = {vec3(), vec3(), vec3(), vec3()}
local function computeFlatCorners(aspectRatio, fov, distanceMult)
  local flatSize = vec2(1 / aspectRatio, 1) * math.sin(fov) * -config.eyePosBase.z
  local flatPosZ = math.cos(fov) * -config.eyePosBase.z
  distanceMult = distanceMult and distanceMult / flatPosZ or 1
  fc[1]:set(-flatSize.x, flatSize.y, flatPosZ):scale(distanceMult)
  fc[2]:set(-flatSize.x, -flatSize.y, flatPosZ + 0.0001):scale(distanceMult)
  fc[3]:set(flatSize.x, -flatSize.y, flatPosZ + 0.0001):scale(distanceMult)
  fc[4]:set(flatSize.x, flatSize.y, flatPosZ):scale(distanceMult)
  return fc
end

local function getFlatPrecomputedData(curvedAngle, curvedRadius, curvedWidth, curvedHeight, curvedPos)
  local lRef = getOnscreenPos(0, 0.5, curvedAngle, curvedRadius, curvedHeight, curvedPos):clone()
  local lY, lX = -math.sin(math.rad(config.flatLeftAngle)), -math.cos(math.rad(config.flatLeftAngle))
  local lH = curvedHeight * config.flatLeftHeight / 2
  local lW = curvedWidth * config.flatLeftWidth
  local rRef = getOnscreenPos(1, 0.5, curvedAngle, curvedRadius, curvedHeight, curvedPos)
  local rY, rX = -math.sin(math.rad(config.flatRightAngle)), math.cos(math.rad(config.flatRightAngle))
  local rH = curvedHeight * config.flatRightHeight / 2
  local rW = curvedWidth * config.flatRightWidth
  local lS = config.flatLeftWidth / (1 + config.flatLeftWidth + config.flatRightWidth)
  local rS = config.flatRightWidth / (1 + config.flatLeftWidth + config.flatRightWidth)
  return {
    gFlatLeftShare = lS,
    gFlatRightShare = rS,
    gFlatXMul = 1 / (1 - lS - rS),
    gFlatLeft0 = vec3(lX * (lW + config.flatLeftGap), lH + config.flatLeftY * curvedHeight, lY * (lW + config.flatLeftGap)):add(lRef),
    gFlatLeft1 = vec3(lX * (lW + config.flatLeftGap), -lH + config.flatLeftY * curvedHeight, lY * (lW + config.flatLeftGap)):add(lRef),
    gFlatLeft2 = vec3(lX * (0 + config.flatLeftGap), -lH + config.flatLeftY * curvedHeight, lY * (0 + config.flatLeftGap)):add(lRef),
    gFlatLeft3 = vec3(lX * (0 + config.flatLeftGap), lH + config.flatLeftY * curvedHeight, lY * (0 + config.flatLeftGap)):add(lRef),
    gFlatRight0 = vec3(rX * (0 + config.flatRightGap), rH + config.flatRightY * curvedHeight, rY * (0 + config.flatRightGap)):add(rRef),
    gFlatRight1 = vec3(rX * (0 + config.flatRightGap), -rH + config.flatRightY * curvedHeight, rY * (0 + config.flatRightGap)):add(rRef),
    gFlatRight2 = vec3(rX * (rW + config.flatRightGap), -rH + config.flatRightY * curvedHeight, rY * (rW + config.flatRightGap)):add(rRef),
    gFlatRight3 = vec3(rX * (rW + config.flatRightGap), rH + config.flatRightY * curvedHeight, rY * (rW + config.flatRightGap)):add(rRef),
    gHoveredLeft = hoveredState[1],
    gHoveredMiddle = hoveredState[2],
    gHoveredRight = hoveredState[3],
  }
end

local function getCurvedAspectRatio(mainAspectRatio)
  if config.originalAspectRatio then
    return mainAspectRatio
  end
  return config.curvedPixelHeight / config.curvedPixelWidth
end

local function findFittingFOV()
  local aspectRatio = sim.windowHeight / sim.windowWidth
  local curvedWidth = getCurvedWidth()
  local curvedAngle = curvedWidth / config.curvedRadius
  local curvedHeight = (getCurvedAspectRatio(aspectRatio) * config.curvedRadius * curvedAngle)
  local curvedPos = config.curvedPosBase - vec3(0, 0, config.curvedRadius)
  local distanceMult = config.curvedPosBase.z + 0.5

  local flatProjectData
  if config.flatActive then
    flatProjectData = getFlatPrecomputedData(curvedAngle, config.curvedRadius, curvedWidth, curvedHeight, curvedPos)
  end

  local a = math.rad(1) / 2
  local b = math.rad(179) / 2
  for _ = 1, 12 do
    local c = (a + b) / 2
    local corners = computeFlatCorners(aspectRatio, c, distanceMult)
    if reprojectFn(0, 0, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, corners) 
        and reprojectFn(0, 1, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, corners)
        and reprojectFn(1, 1, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, corners)
        and reprojectFn(1, 0, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, corners)
        and (not flatProjectData
          or reprojectFn(-1, -1, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, corners, flatProjectData)
          and reprojectFn(-1, 2, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, corners, flatProjectData)
          and reprojectFn(2, 2, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, corners, flatProjectData)
          and reprojectFn(2, -1, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, corners, flatProjectData)) then
      b = c
    else
      a = c
    end
  end
  return math.deg((a + b) / 2) * 2
end

local function isTripleFits()
  if not shaderData.values or not shaderData.values.gTripleCornerR0 then
    return nil
  end

  local aspectRatio = sim.windowHeight / sim.windowWidth
  local curvedWidth = getCurvedWidth()
  local curvedAngle = curvedWidth / config.curvedRadius
  local curvedHeight = (getCurvedAspectRatio(aspectRatio) * config.curvedRadius * curvedAngle)
  local curvedPos = config.curvedPosBase - vec3(0, 0, config.curvedRadius)

  local flatProjectData
  if config.flatActive then
    flatProjectData = getFlatPrecomputedData(curvedAngle, config.curvedRadius, curvedWidth, curvedHeight, curvedPos)
  end

  -- do 
  --   return reprojectTripleFn(0, 0, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, shaderData.values) 
  -- end

  return reprojectTripleFn(0, 0, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, shaderData.values) 
      and reprojectTripleFn(0, 1, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, shaderData.values)
      and reprojectTripleFn(1, 1, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, shaderData.values)
      and reprojectTripleFn(1, 0, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, shaderData.values)
      and (not flatProjectData
        or reprojectTripleFn(-1, -1, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, shaderData.values, flatProjectData)
        and reprojectTripleFn(-1, 2, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, shaderData.values, flatProjectData)
        and reprojectTripleFn(2, 2, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, shaderData.values, flatProjectData)
        and reprojectTripleFn(2, -1, curvedAngle, config.curvedRadius, curvedHeight, curvedPos, shaderData.values, flatProjectData))
end

local function refreshComputedValues(useOptimalFOV)
  local windowSize = vec2(sim.windowWidth, sim.windowHeight)
  local aspectRatio = sim.windowHeight / sim.windowWidth
  local curvedWidth = getCurvedWidth()
  local curvedAngle = curvedWidth / config.curvedRadius
  local curvedHeight = (getCurvedAspectRatio(aspectRatio) * config.curvedRadius * curvedAngle)
  local curvedPos = config.curvedPosBase - vec3(0, 0, config.curvedRadius)

  local cacheKey = 1
  local shaderValues = {
    iResolution = windowSize,
    gCamera = cameraRotation,
    gCameraZoom = cameraZoom,
    gSceneScale = math.max(cameraZoom, 1) * 10,
    gCurvedRadius = config.curvedRadius,
    gCurvedAngle = curvedAngle,
    gCurvedHeight = curvedHeight,
    gCurvedPos = curvedPos,
    gEyePos = config.eyePosBase,
  }

  if config.flatActive then
    table.assign(shaderValues, getFlatPrecomputedData(curvedAngle, config.curvedRadius, curvedWidth, curvedHeight, curvedPos))
    cacheKey = bit.bor(cacheKey, 2)
  end

  if tripleState and sim.isTripleScreenAvailable then
    gOptimalFOV = -1

    local function bv3(x, y, z)
      return vec3(x, y, z + config.eyePosBase.z)
    end

    table.assign(shaderValues, {
      gTripleX0 = tripleState.screens[1].xFrom,
      gTripleX1 = tripleState.screens[1].xTo,
      gTripleCornerL0 = bv3(tripleState.screens[0].left.x, tripleState.screens[0].height / 2, tripleState.screens[0].left.y),
      gTripleCornerL1 = bv3(tripleState.screens[0].left.x, -tripleState.screens[0].height / 2, tripleState.screens[0].left.y),
      gTripleCornerL2 = bv3(tripleState.screens[0].right.x, -tripleState.screens[0].height / 2, tripleState.screens[0].right.y),
      gTripleCornerL3 = bv3(tripleState.screens[0].right.x, tripleState.screens[0].height / 2, tripleState.screens[0].right.y),
      gTripleCornerC0 = bv3(tripleState.screens[1].left.x, tripleState.screens[1].height / 2, tripleState.screens[1].left.y),
      gTripleCornerC1 = bv3(tripleState.screens[1].left.x, -tripleState.screens[1].height / 2, tripleState.screens[1].left.y),
      gTripleCornerC2 = bv3(tripleState.screens[1].right.x, -tripleState.screens[1].height / 2, tripleState.screens[1].right.y),
      gTripleCornerC3 = bv3(tripleState.screens[1].right.x, tripleState.screens[1].height / 2, tripleState.screens[1].right.y),
      gTripleCornerR0 = bv3(tripleState.screens[2].left.x, tripleState.screens[2].height / 2, tripleState.screens[2].left.y),
      gTripleCornerR1 = bv3(tripleState.screens[2].left.x, -tripleState.screens[2].height / 2, tripleState.screens[2].left.y),
      gTripleCornerR2 = bv3(tripleState.screens[2].right.x, -tripleState.screens[2].height / 2, tripleState.screens[2].right.y),
      gTripleCornerR3 = bv3(tripleState.screens[2].right.x, tripleState.screens[2].height / 2, tripleState.screens[2].right.y),
    })
    cacheKey = bit.bor(cacheKey, 4)
  else
    gOptimalFOV = findFittingFOV()
    gFOVTrimmed = gOptimalFOV > 140
    gOptimalFOV = math.min(gOptimalFOV, 140)
    -- gOptimalFOV = -1
    local distanceMult = config.curvedPosBase.z + 0.5
    local fov = math.rad(useOptimalFOV and gOptimalFOV ~= -1 and gOptimalFOV or sim.cameraFOV) / 2
    local corners = computeFlatCorners(aspectRatio, fov, distanceMult)
    table.assign(shaderValues, {
      gFlatCorner0 = corners[1],
      gFlatCorner1 = corners[2],
      gFlatCorner2 = corners[3],
      gFlatCorner3 = corners[4],
    })
  end

  shaderValues.gHighlightMissingData = os.preciseClock() < settingsClock or gFOVTrimmed
  shaderValues.gFOVTrimmed = gFOVTrimmed

  shaderData = {
    p1 = vec2(),
    p2 = ui.windowSize(),
    blendMode = render.BlendMode.Opaque,
    textures = {
      txSource = 'dynamic::screen'
    },
    values = shaderValues,
    defines = {
      USE_SIDE_MONITORS = config.flatActive,
      TRIPLE_MODE = tripleState and sim.isTripleScreenAvailable
    },
    shader = 'filter.fx',
    cacheKey = cacheKey
  }
end

local autoFitTriple = false
local configureBoth = false

local function settingsWindow()
  settingsClock = os.preciseClock() + 0.5

  hoveredState[1] = math.applyLag(hoveredState[1], hoveredScreen <= 0 and 1 or 0, 0.8, ui.deltaTime())
  hoveredState[2] = math.applyLag(hoveredState[2], (hoveredScreen == -1 or hoveredScreen == 1) and 1 or 0, 0.8, ui.deltaTime())
  hoveredState[3] = math.applyLag(hoveredState[3], (hoveredScreen == -1 or hoveredScreen == 2) and 1 or 0, 0.8, ui.deltaTime())
  hoveredScreen = -1

  refreshComputedValues()
  local windowSize = vec2(sim.windowWidth, sim.windowHeight)
  if not canvasDebug or canvasDebug:size() ~= windowSize then
    canvasDebug = ui.ExtraCanvas(windowSize)
  end

  canvasDebug:updateWithShader({
    textures = shaderData.textures,
    values = shaderData.values,
    defines = {
      DEBUG_VIEW = true,
      USE_SIDE_MONITORS = config.flatActive,
      TRIPLE_MODE = tripleState and sim.isTripleScreenAvailable
    },
    shader = 'filter.fx',
    cacheKey = bit.bor(shaderData.cacheKey, 8),
    async = true
  })

  ui.drawImage(canvasDebug, vec2(), ui.windowSize() - vec2(0, 10), ui.ImageFit.Fill)

  ui.drawRectFilledMultiColor(vec2(ui.windowSize().x - 400, 0), ui.windowSize() - vec2(320, 10), 
    rgbm.colors.transparent, rgbm(0, 0, 0, 0.5), rgbm(0, 0, 0, 0.5), rgbm.colors.transparent)
  ui.drawRectFilled(vec2(ui.windowSize().x - 320, 0), ui.windowSize() - vec2(0, 10), rgbm(0, 0, 0, 0.5))

  local cur = ui.getCursor()
  ui.invisibleButton('##drag', ui.availableSpace())
  ui.setItemAllowOverlap()
  if ui.itemHovered() then
    if ui.mouseDown() then
      cameraRotation:addScaled(ui.mouseDelta():mul(vec2(-1, 1)), 0.02)
      cameraRotation.y = math.clampN(cameraRotation.y, -math.pi / 2, math.pi / 2)
    end
    cameraZoom = math.clampN(cameraZoom - ui.mouseWheel() * 0.4, -1, 10)
  end

  ui.setCursor(cur)
  ui.setCursorX(ui.availableSpaceX() - 320)
  ui.pushItemWidth(320)
  ui.beginGroup()
  local newValue

  ui.header('Monitors')
  ui.beginGroup()
  if ui.checkbox('Use AC aspect ratio', config.originalAspectRatio) then
    config.originalAspectRatio = not config.originalAspectRatio
  end
  ui.setNextItemWidth(158)
  newValue = ui.slider('##width', config.curvedPixelWidth, 640, 12000, 'Width: %.0f px', 3)
  if ui.itemEdited() then config.curvedPixelWidth = newValue end
  ui.sameLine(0, 4)
  ui.setNextItemWidth(158)
  newValue = ui.slider('##height', config.curvedPixelHeight, 640, 12000, 'Height: %.0f px', 3)
  if ui.itemEdited() then config.curvedPixelHeight = newValue end
  ui.endGroup()
  if ui.itemHovered() then
    hoveredScreen = 1
    ui.setTooltip('Dimensions of a curved monitor. Only ratio is needed, so you can use the ratio (such as “16, 9”) instead of pixel dimensions.\n\nHold Shift while dragging a slider to increase accuracy. Hold Ctrl and click on a slider to set the exact number with keyboard.')
  end

  ui.setNextItemWidth(158)
  newValue = ui.slider('##size', config.curvedSizeInches, 5, 500, 'Size: %.1f inches')
  if ui.itemEdited() then config.curvedSizeInches = newValue end
  if ui.itemHovered() then
    hoveredScreen = 1
    ui.setTooltip('Size of the curved monitor’s diagonal in inches. Used together with ratio computed from Width and Height to estimate the actual screen width.\n\nHold Shift while dragging a slider to increase accuracy. Hold Ctrl and click on a slider to set the exact number with keyboard.')
  end
  ui.sameLine(0, 4)
  ui.setNextItemWidth(158)
  newValue = ui.slider('##radius', config.curvedRadius * 1000, 500, 6e3, 'Radius: %.0fR') / 1000
  if ui.itemEdited() then config.curvedRadius = newValue end
  if ui.itemHovered() then
    hoveredScreen = 1
    ui.setTooltip('Radius of the curved monitor’s curvature in millimeters. The exact value should be mentioned in tech specifications.\n\nHold Shift while dragging a slider to increase accuracy. Hold Ctrl and click on a slider to set the exact number with keyboard.')
  end

  ui.offsetCursorY(12)

  if configureBoth then
    ui.header('Positions')
    ui.beginGroup()
    ui.setNextItemWidth(104)
    newValue = ui.slider('##eye.x', config.eyePosBase.x * 100, -50, 50, 'Eye: %.1f cm') / 100
    if ui.itemEdited() then config.eyePosBase = vec3(newValue, config.eyePosBase.y, config.eyePosBase.z) end  
    ui.sameLine(0, 4)
    ui.setNextItemWidth(104)
    newValue = ui.slider('##eye.y', config.eyePosBase.y * 100, -50, 50, '%.1f cm') / 100
    if ui.itemEdited() then config.eyePosBase = vec3(config.eyePosBase.x, newValue, config.eyePosBase.z) end
    ui.sameLine(0, 4)
    ui.setNextItemWidth(104)
    newValue = ui.slider('##eye.z', config.eyePosBase.z * 100, -50, 50, '%.1f cm') / 100
    if ui.itemEdited() then config.eyePosBase = vec3(config.eyePosBase.x, config.eyePosBase.y, newValue) end
    ui.endGroup()
    if ui.itemHovered() then
      ui.setTooltip('Position of a viewer (marked as a red dot in a visualization).')
    end

    ui.beginGroup()
    ui.setNextItemWidth(104)
    newValue = ui.slider('##pos.x', config.curvedPosBase.x * 100, -50, 50, 'Mon.: %.1f cm') / 100
    if ui.itemEdited() then config.curvedPosBase = vec3(newValue, config.curvedPosBase.y, config.curvedPosBase.z) end
    ui.sameLine(0, 4)
    ui.setNextItemWidth(104)
    newValue = ui.slider('##pos.y', config.curvedPosBase.y * 100, -50, 50, '%.1f cm') / 100
    if ui.itemEdited() then config.curvedPosBase = vec3(config.curvedPosBase.x, newValue, config.curvedPosBase.z) end
    ui.sameLine(0, 4)
    ui.setNextItemWidth(104)
    newValue = ui.slider('##pos.z', config.curvedPosBase.z * 100, 20, 180, '%.1f cm') / 100
    if ui.itemEdited() then config.curvedPosBase = vec3(config.curvedPosBase.x, config.curvedPosBase.y, newValue) end
    ui.endGroup()
    if ui.itemHovered() then
      hoveredScreen = 1
      ui.setTooltip('Offset of the curved monitor.')
    end
  else
    ui.header('Eye position')
    ui.beginGroup()
    ui.setNextItemWidth(ui.availableSpaceX())
    newValue = ui.slider('##eye.x', config.eyePosBase.x * 100, -200, 200, '%.1f cm') / 100
    if ui.itemEdited() then config.eyePosBase = vec3(newValue, config.eyePosBase.y, config.eyePosBase.z) end  
    ui.setNextItemWidth(ui.availableSpaceX())
    newValue = ui.slider('##eye.y', config.eyePosBase.y * 100, -200, 200, '%.1f cm') / 100
    if ui.itemEdited() then config.eyePosBase = vec3(config.eyePosBase.x, newValue, config.eyePosBase.z) end
    ui.setNextItemWidth(ui.availableSpaceX())
    newValue = ui.slider('##eye.z', -config.eyePosBase.z * 100, 0, 750, '%.1f cm', 2) / 100
    if ui.itemEdited() then config.eyePosBase = vec3(config.eyePosBase.x, config.eyePosBase.y, -newValue) end
    ui.endGroup()
    if ui.itemHovered() then
      ui.setTooltip('Position of a viewer (marked as a red dot in a visualization).')
    end
  end

  ui.offsetCursorY(12)
  ui.pushFont(ui.Font.Small)
  ui.pushStyleColor(ui.StyleColor.Text, rgbm.colors.transparent)
  if ui.checkbox('Additional monitors', config.flatActive) then
    config.flatActive = not config.flatActive
  end
  if ui.itemHovered() then
    ui.setTooltip('Use this option if you have two extra flat monitors on sides and want to project an accurate image on them as well.')
  end
  ui.popStyleColor()
  ui.sameLine(24)
  ui.header('Additional monitors')
  ui.popFont()

  if not config.flatActive then
    ui.pushDisabled()
  end
  
  ui.setNextItemWidth(158)
  newValue = ui.slider('##lw', config.flatLeftWidth * 100, 1, 200, 'Left width: %.1f%%')
  if ui.itemEdited() then config.flatLeftWidth = newValue / 100 end
  if ui.itemHovered() then hoveredScreen = 0 ui.setTooltip('Width relative to width of the curved monitor.') end
  ui.sameLine(0, 4)
  ui.setNextItemWidth(158)
  newValue = ui.slider('##lh', config.flatLeftHeight * 100, 1, 200, 'Left height: %.1f%%')
  if ui.itemEdited() then config.flatLeftHeight = newValue / 100 end  
  if ui.itemHovered() then hoveredScreen = 0 ui.setTooltip('Height relative to height of the curved monitor.') end
  ui.setNextItemWidth(158)
  newValue = ui.slider('##lg', config.flatLeftGap * 100, 0, 20, 'Left gap: %.1f cm')
  if ui.itemEdited() then config.flatLeftGap = newValue / 100 end
  if ui.itemHovered() then hoveredScreen = 0 ui.setTooltip('Distance between the curved monitor and the left one.') end
  ui.sameLine(0, 4)
  ui.setNextItemWidth(158)
  newValue = ui.slider('##lo', config.flatLeftY * 100, -50, 50, 'Left Y offset: %.1f%%')
  if ui.itemEdited() then config.flatLeftY = newValue / 100 end
  if ui.itemHovered() then hoveredScreen = 0 ui.setTooltip('Vertical offset relative to height of the curved monitor.') end
  newValue = ui.slider('##la', config.flatLeftAngle, 0, 90, 'Left angle: %.1f°')
  if ui.itemEdited() then config.flatLeftAngle = newValue  end
  if ui.itemHovered() then hoveredScreen = 0 ui.setTooltip('Angle at which left monitor is turned.') end
  
  ui.setNextItemWidth(158)
  newValue = ui.slider('##rw', config.flatRightWidth * 100, 1, 200, 'Right width: %.1f%%')
  if ui.itemEdited() then config.flatRightWidth = newValue / 100 end
  if ui.itemHovered() then hoveredScreen = 2 ui.setTooltip('Width relative to width of the curved monitor.') end
  ui.sameLine(0, 4)
  ui.setNextItemWidth(158)
  newValue = ui.slider('##rh', config.flatRightHeight * 100, 1, 200, 'Right height: %.1f%%')
  if ui.itemEdited() then config.flatRightHeight = newValue / 100 end  
  if ui.itemHovered() then hoveredScreen = 2 ui.setTooltip('Height relative to height of the curved monitor.') end
  ui.setNextItemWidth(158)
  newValue = ui.slider('##rg', config.flatRightGap * 100, 0, 20, 'Right gap: %.1f cm')
  if ui.itemEdited() then config.flatRightGap = newValue / 100 end
  if ui.itemHovered() then hoveredScreen = 2 ui.setTooltip('Distance between the curved monitor and the right one.') end
  ui.sameLine(0, 4)
  ui.setNextItemWidth(158)
  newValue = ui.slider('##ro', config.flatRightY * 100, -50, 50, 'Right Y offset: %.1f%%')
  if ui.itemEdited() then config.flatRightY = newValue / 100 end
  if ui.itemHovered() then hoveredScreen = 2 ui.setTooltip('Vertical offset relative to height of the curved monitor.') end
  newValue = ui.slider('##ra', config.flatRightAngle, 0, 90, 'Right angle: %.1f°')
  if ui.itemEdited() then config.flatRightAngle = newValue  end
  if ui.itemHovered() then hoveredScreen = 2 ui.setTooltip('Angle at which right monitor is turned.') end

  if not config.flatActive then
    ui.popDisabled()
  end

  if ui.button('Export settings', vec2(158, 0)) then
    os.saveFileDialog({
      defaultFolder = ac.getFolder(ac.FolderID.Documents),
      fileTypes = { { name = 'Curved Monitor config', mask = '*.cfg' } },
      defaultExtension = '.cfg'
    }, function (err, filename)
      if filename then
        io.save(filename, saveSettings())
      end
    end)
  end
  if ui.itemHovered() then ui.setTooltip('Save settings to a file to quicky apply them to a different PC.') end
  ui.sameLine(0, 4)
  if ui.button('Import settings', vec2(158, 0)) then
    os.openFileDialog({
      defaultFolder = ac.getFolder(ac.FolderID.Root),
      fileTypes = { { name = 'Curved Monitor config', mask = '*.cfg' } },
    }, function (err, filename)
      if filename then
        loadSettings(io.load(filename))
      end
    end)
  end
  if ui.itemHovered() then ui.setTooltip('Load previously saved settings.') end
  if sim.isTripleScreenAvailable then
    if ui.button('Auto-fit triple configuration', vec2(320, 0), autoFitTriple and ui.ButtonFlags.Active or 0) then
      autoFitTriple = true
    end
    if ui.itemHovered() then ui.setTooltip('Adjust triple screen configuration automatically to better fit the screens.') end
    ui.offsetCursorY(12)
    ui.textWrapped('\t If your curved screen is too large for AC triple setup configuration tool, consider dividing all dimensions by, for example, 10 (until we’ll rewrite that tool so it could handle screens wider than 3 m)')
    ui.drawIcon(ui.Icons.Warning, ui.itemRectMin() + 2, ui.itemRectMin() + 14)
  else
    autoFitTriple = false
  end
  if ui.button('Change pixel density', vec2(320, 0)) then
    local pixelDensity = config.pixelDensity
    local suggestedDensity = -1

    local canv = ui.ExtraCanvas(1, 1, render.TextureFormat.R32.Float)
    local res = ac.getUI().windowSize
    canv:updateSceneWithShader({
      textures = shaderData.textures,
      values = table.assign({ gSample1 = vec2(0.5, 0.5), gSample2 = vec2(0.5, 0.5) + 1 / res, gResolution = res }, shaderData.values),
      defines = {
        DENSITY_COMPUTE = true,
        USE_SIDE_MONITORS = config.flatActive,
        TRIPLE_MODE = tripleState and sim.isTripleScreenAvailable
      },
      shader = 'filter.fx',
      cacheKey = bit.bor(shaderData.cacheKey, 16),
    })
    canv:accessData(function (err, data)
      suggestedDensity = data and 1 / data:floatValue(0, 0) or math.nan
    end)
    
    ui.modalDialog('Pixel density', function ()
      ui.setNextItemWidth(-0.1)
      pixelDensity = ui.slider('##density', pixelDensity * 100, 40, 240, '%.0f%%') / 100
      ui.offsetCursorY(4)
      ui.alignTextToFramePadding()
      ui.textWrapped('Suggested density: %s' % (suggestedDensity < 0 and 'computing…' or math.isnan(suggestedDensity) and 'N/A' 
        or math.round(100 * suggestedDensity)..'%'))
      ui.sameLine()
      if ui.button('Apply suggested', suggestedDensity > 0 and ui.ButtonFlags.None or ui.ButtonFlags.Disabled) then
        pixelDensity = suggestedDensity
      end
      ui.newLine()
      ui.offsetCursorY(4)
      local r = ui.modernButton('Apply', vec2(ui.availableSpaceX() / 2 - 4, 40), ui.ButtonFlags.Confirm, ui.Icons.Confirm)
      if r then
        config.pixelDensity = pixelDensity
        ac.setPixelDensity(pixelDensity)
      end
      ui.sameLine(0, 8)
      return ui.modernButton('Cancel', vec2(-0.1, 40), ui.ButtonFlags.Cancel, ui.Icons.Cancel) or r
    end)
  end
  if ui.itemHovered() then ui.setTooltip('Increased pixel density might help with image quality, but performance will be decreased.') end
  ui.endGroup()

  if gOptimalFOV > 0 then
    local msg, color = string.format('Optimal FOV: %.1f°', gOptimalFOV), rgbm.colors.white
    ui.drawRectFilled(vec2(40, 40) - 8, ui.measureText(msg) + 48, rgbm(0, 0, 0, 0.8), 2)
    ui.drawText(msg, vec2(40, 40), color)
  end
end

ui.addSettings({
  id = 'curved_settings',
  icon = 'icon.png',
  name = 'Curved Monitor Filter',
  size = {default = vec2(960, 540), min = vec2(480, 270)},
}, function ()
  gAppliedFOV = -1
  settingsWindow()
end)

ac.onTripleConfigurationChanged(function ()
  gAppliedFOV = -1
end)

local tripleAvailable = false
local tripleAdjustmentVelocity = 0
local tripleFitTarget
local tripleFitHits = 0
local tripleMajorJump = false

local function runTripleAutoFit()
  local fits = isTripleFits()
  if fits == nil then return end
  
  if tripleFitTarget == nil then
    tripleFitTarget = not fits
    tripleFitHits = 0
    tripleAdjustmentVelocity = 0
    tripleMajorJump = false
  end

  if tripleFitTarget == fits then
    if not tripleMajorJump then
      tripleMajorJump = true
      ac.setTripleWidth(ac.getTripleWidth() - 0.05)
    else
      tripleFitHits = tripleFitHits + 1
      if tripleFitHits > 10 then
        autoFitTriple = false
      end
    end
  end

  if fits then
    tripleAdjustmentVelocity = math.min(tripleAdjustmentVelocity, 0) - 0.001
  else
    tripleAdjustmentVelocity = math.max(tripleAdjustmentVelocity, 0) + 0.001
  end
  ac.setTripleWidth(ac.getTripleWidth() + tripleAdjustmentVelocity)
end

function script.update()
  if settingsClock ~= -1 and settingsClock < os.preciseClock() then
    settingsClock = -1
    gAppliedFOV = -1
  end
  if tripleAvailable ~= sim.isTripleScreenAvailable then
    tripleAvailable = not tripleAvailable
    gAppliedFOV = -1
  end
  if not shaderData or gAppliedFOV ~= sim.cameraFOV then
    gAppliedFOV = sim.cameraFOV
    refreshComputedValues(false)
    if not sim.isTripleScreenAvailable then
      if gOptimalFOV ~= -1 then
        ac.setFixedFOV(gOptimalFOV)
      else
        ac.setFixedFOV(nil)
      end
    elseif autoFitTriple then
      runTripleAutoFit()
    else
      tripleFitTarget = nil
    end
  end
  ui.renderShader(shaderData)
end
