local M = {}

local rust = require('fff.rust')
local list_separator = require('fff.list_separator')
local scrollbar = require('fff.scrollbar')
local list_renderer = require('fff.picker_ui.list_renderer')
local combo_renderer = require('fff.picker_ui.combo_renderer')
local file_picker = require('fff.file_picker')
local layout = require('fff.layout')
local picker_ui_state = require('fff.picker_ui.picker_ui_state')

-- Parent module reference (set by picker_ui.lua during initialization).
-- Allows renderer functions to call back into the main picker module.
---@type table
local P = nil

function M.init(parent_module) P = parent_module end

-- Convenience alias
local S = picker_ui_state.state

local function get_prompt_position() return layout.resolve_prompt_position(S.config) end

local function shrink_path(path, max_width)
  local config = S.config or {}
  local strategy = config.layout and config.layout.path_shorten_strategy or 'middle_number'
  return rust.shorten_path(path, max_width, strategy)
end

local function format_file_display(item, max_width)
  local filename = item.name
  if type(filename) ~= 'string' then filename = filename and tostring(filename) or '' end
  local dir_path = item.directory or ''
  if type(dir_path) ~= 'string' then dir_path = dir_path and tostring(dir_path) or '' end

  if dir_path == '' and item.relative_path then
    local parent_dir = vim.fn.fnamemodify(item.relative_path, ':h')
    if parent_dir ~= '.' and parent_dir ~= '' then dir_path = parent_dir end
  end

  local filename_width = vim.fn.strdisplaywidth(filename)
  local base_width = filename_width + 1
  local path_max_width = math.max(max_width - base_width, 0)

  if dir_path == '' or path_max_width == 0 then return filename, '' end
  local display_path = shrink_path(dir_path, path_max_width)

  return filename, display_path
end

--- Adjust scroll for bottom prompt to eliminate gaps.
function M.scroll_to_bottom()
  if not S.list_win or not vim.api.nvim_win_is_valid(S.list_win) then return end

  local win_height = vim.api.nvim_win_get_height(S.list_win)
  local buf_lines = vim.api.nvim_buf_line_count(S.list_buf)

  vim.api.nvim_win_call(S.list_win, function()
    local view = vim.fn.winsaveview()
    local bottom_topline = math.max(1, buf_lines - win_height + 1)
    local cursor_line = vim.api.nvim_win_get_cursor(S.list_win)[1]

    if cursor_line >= bottom_topline then
      view.topline = bottom_topline
    elseif cursor_line < view.topline then
      view.topline = math.max(1, cursor_line - 1)
    elseif cursor_line >= view.topline + win_height then
      view.topline = bottom_topline
    end
    vim.fn.winrestview(view)
  end)
end

--- Render the grep empty state.
local function render_grep_empty_state(ctx)
  list_separator.hide()

  local config = ctx.config
  local win_width = ctx.win_width
  local win_height = ctx.win_height
  local prompt_position = ctx.prompt_position

  local content = {}
  local hl_cmds = {}

  table.insert(content, '')
  table.insert(content, '  Start typing to search file contents...')
  table.insert(content, '')
  table.insert(content, '  Tips:')
  table.insert(content, '    "pattern *.rs"    search only in Rust files')
  table.insert(content, '    "pattern /src/"   limit search to src/ directory')
  table.insert(content, '    "!test pattern"   exclude test files')
  table.insert(content, '')

  if prompt_position == 'bottom' then
    local empty_needed = math.max(0, win_height - #content)
    for _ = 1, empty_needed do
      table.insert(content, 1, string.rep(' ', win_width + 5))
      for _, h in ipairs(hl_cmds) do
        h.row = h.row + 1
      end
    end
  end

  vim.api.nvim_set_option_value('modifiable', true, { buf = S.list_buf })
  vim.api.nvim_buf_set_lines(S.list_buf, 0, -1, false, content)
  vim.api.nvim_set_option_value('modifiable', false, { buf = S.list_buf })

  vim.api.nvim_buf_clear_namespace(S.list_buf, S.ns_id, 0, -1)

  if prompt_position == 'bottom' then M.scroll_to_bottom() end
  for _, h in ipairs(hl_cmds) do
    pcall(
      vim.api.nvim_buf_set_extmark,
      S.list_buf,
      S.ns_id,
      h.row,
      h.col_start,
      { end_col = h.col_end, hl_group = h.hl }
    )
  end

  for i = 0, #content - 1 do
    local line = content[i + 1]
    if
      line and (line:match('^%s+Start typing') or line:match('^%s+Tips') or line:match('^%s+"') or line:match('^%s+!'))
    then
      pcall(
        vim.api.nvim_buf_set_extmark,
        S.list_buf,
        S.ns_id,
        i,
        0,
        { end_row = i + 1, end_col = 0, hl_group = 'Comment' }
      )
    end
    if line and (line:match('^%s+[╭╰│]') or line:match('[╮╯│]%s*$')) then
      pcall(
        vim.api.nvim_buf_set_extmark,
        S.list_buf,
        S.ns_id,
        i,
        0,
        { end_row = i + 1, end_col = 0, hl_group = config.hl.border or 'FloatBorder' }
      )
    end
  end
end

--- Build rendering context with all necessary data.
local function build_render_context()
  local config = S.config or {}
  local items = S.filtered_items
  local win_height = vim.api.nvim_win_get_height(S.list_win)
  local win_width = vim.api.nvim_win_get_width(S.list_win)
  local prompt_position = get_prompt_position()

  local win_info = vim.fn.getwininfo(S.list_win)[1]
  local text_offset = win_info and win_info.textoff or 2
  local text_width = win_width - text_offset

  local combo_boost_score_multiplier = config.history and config.history.combo_boost_score_multiplier or 100
  local separator = nil
  local combo_info = nil
  if S.mode ~= 'grep' and not S.suggestion_source then
    combo_info = combo_renderer.detect(
      items,
      file_picker,
      combo_boost_score_multiplier,
      S.next_search_force_combo_boost or config.history.min_combo_count == 0
    )
  end
  S.next_search_force_combo_boost = false

  if combo_info and S.combo_visible then
    separator = {
      idx = combo_info.idx,
      text = combo_info.text,
      text_hl = config.hl.combo_header,
      border_hl = config.hl.border,
    }
    S.combo_initial_cursor = combo_info.idx
  end

  local display_start = 1
  local display_end = #items

  if separator and (display_end - display_start + 1) >= win_height then
    if separator.idx == display_start then
      display_end = display_end - 1
    else
      display_start = display_start + 1
    end
  end

  local iter_start, iter_end, iter_step
  if prompt_position == 'bottom' then
    iter_start, iter_end, iter_step = display_end, display_start, -1
  else
    iter_start, iter_end, iter_step = display_start, display_end, 1
  end

  return {
    config = config,
    items = items,
    cursor = S.cursor,
    win_height = win_height,
    win_width = win_width,
    max_path_width = text_width,
    debug_enabled = config and config.debug and config.debug.show_scores,
    prompt_position = prompt_position,
    separator = separator,
    combo_info = combo_info,
    display_start = display_start,
    display_end = display_end,
    iter_start = iter_start,
    iter_end = iter_end,
    iter_step = iter_step,
    renderer = S.suggestion_source and P.get_suggestion_renderer() or S.renderer,
    query = S.query,
    selected_files = S.selected_files,
    selected_items = S.selected_items,
    mode = S.mode,
    format_file_display = format_file_display,
    suggestion_source = S.suggestion_source,
  }
end

local function finalize_render(separator_line, ctx)
  if ctx.separator and separator_line then
    local arrow = ctx.prompt_position == 'bottom' and '↓' or '↑'

    local list_cfg = vim.api.nvim_win_get_config(S.list_win)
    local screen_row = list_cfg.row + separator_line

    list_separator.update({
      list_win = S.list_win,
      row = screen_row,
      text = arrow .. ' ' .. ctx.separator.text,
      text_hl = ctx.separator.text_hl,
      border_hl = ctx.separator.border_hl,
    })
  else
    list_separator.hide()
  end

  if ctx.mode ~= 'grep' and not ctx.suggestion_source then
    scrollbar.render(S.layout, ctx.config, S.list_win, S.pagination, ctx.prompt_position)
  end
end

function M.render_list()
  if not P.state.active then return end

  local ctx = build_render_context()
  if S.mode == 'grep' and #ctx.items == 0 then
    S.line_to_item = {}
    S.item_to_lines = {}
    S.last_render_ctx = nil
    render_grep_empty_state(ctx)
    return
  end

  local item_to_lines, separator_line = list_renderer.render(ctx, S.list_buf, S.list_win, S.ns_id)
  S.item_to_lines = item_to_lines
  S.last_render_ctx = ctx

  local line_to_item = {}
  for item_idx, mapping in pairs(item_to_lines) do
    for line = mapping.first, mapping.last do
      line_to_item[line] = item_idx
    end
  end
  S.line_to_item = line_to_item

  if ctx.prompt_position == 'bottom' then M.scroll_to_bottom() end

  finalize_render(separator_line, ctx)
end

local function rerender_cursor_rows(old_cursor, new_cursor)
  if S.suggestion_source then return false end

  local ctx = S.last_render_ctx
  if not ctx or ctx.items ~= S.filtered_items then return false end
  if ctx.renderer and not ctx.renderer.supports_cursor_rerender then return false end

  local old_lines = S.item_to_lines[old_cursor]
  local new_lines = S.item_to_lines[new_cursor]
  if not old_lines or not new_lines then return false end

  local renderer = ctx.renderer or require('fff.picker_ui.file_renderer')
  local entries = {
    { item_idx = old_cursor, lines = old_lines },
    { item_idx = new_cursor, lines = new_lines },
  }

  for _, entry in ipairs(entries) do
    entry.item = ctx.items[entry.item_idx]
    if not entry.item then return false end

    local line_idx = entry.lines.last
    entry.line = vim.api.nvim_buf_get_lines(S.list_buf, line_idx - 1, line_idx, false)[1]
    if not entry.line then return false end
  end

  ctx.cursor = new_cursor
  for _, entry in ipairs(entries) do
    vim.api.nvim_buf_clear_namespace(S.list_buf, S.ns_id, entry.lines.first - 1, entry.lines.last)
    renderer.apply_highlights(entry.item, ctx, entry.item_idx, S.list_buf, S.ns_id, entry.lines.last, entry.line)
  end

  vim.api.nvim_win_set_cursor(S.list_win, { new_lines.last, 0 })
  return true
end

function M.render_after_cursor_move(old_cursor)
  if old_cursor == S.cursor then return false end
  if old_cursor and rerender_cursor_rows(old_cursor, S.cursor) then return true end
  M.render_list()
  return true
end

return M
