local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local math = _tl_compat and _tl_compat.math or math; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table; local ast_traversal = require("cerulean.ast_traversal")
local doc = require("cerulean.doc")
local expr_doc = require("cerulean.expr_doc")
local fmt_logger = require("cerulean.fmt_logger")
local parser = require("cerulean.parser")
local render_builders = require("cerulean.render_builders")
local render_context = require("cerulean.render_context")
local source = require("cerulean.source")
local stmt_doc = require("cerulean.stmt_doc")



local block_doc = {}

local function node_end_line(node)







   local last = node.yend or 0
   for _, c in ipairs(node.trailing_comments or {}) do
      if c.yend > last then
         last = c.yend
      end
   end
   return last
end

local function max_source_line(node)
   local max_line = node_end_line(node)

   local function set_max_line(child)
      local child_end = node_end_line(child)
      if child_end > max_line then
         max_line = child_end
      end
   end

   ast_traversal.walk_descendants(node, set_max_line)
   return max_line
end

local function verbatim_extent(
   stmt, block_dangling)



   local last = max_source_line(stmt)
   for _, comment in ipairs(block_dangling) do
      if comment.y <= last then
         last = math.max(last, comment.yend)
      end
   end
   return last
end

local function block_min_source_line(block)
   local min_line = block.y

   local function consider_comments(candidate)
      for _, comment in ipairs(candidate.leading_comments or {}) do
         if comment.y < min_line then
            min_line = comment.y
         end
      end
   end

   consider_comments(block)
   ast_traversal.walk_descendants(block, consider_comments)
   return min_line
end

local function block_surface_range(block)
   return source.Range.new(block_min_source_line(block), max_source_line(block))
end

local function stmt_child_blocks(stmt)
   local result = {}
   if stmt.body ~= nil then
      table.insert(result, stmt.body)
   end
   for _, if_block in ipairs(stmt.if_blocks or {}) do
      table.insert(result, if_block.body)
   end
   return result
end

local function stmt_child_surface_ranges(stmt)
   local result = {}
   for _, child_block in ipairs(stmt_child_blocks(stmt)) do
      table.insert(result, block_surface_range(child_block))
   end
   return result
end

local function stmt_overlaps_fmt_off(
   stmt, fmt_off_regions)

   local stmt_range = source.Range.new(stmt.y, max_source_line(stmt))
   local own_ranges = source.subtract_many(
   { stmt_range }, stmt_child_surface_ranges(stmt))

   for _, range in ipairs(own_ranges) do
      if fmt_off_regions:includes_range(range.first_line, range.last_line) then
         return true
      end
   end







   local last_line = max_source_line(stmt)
   for _, child_block in ipairs(stmt_child_blocks(stmt)) do
      for _, child in ipairs(child_block) do
         if
max_source_line(child) == last_line and
            stmt_overlaps_fmt_off(child, fmt_off_regions) then

            return true
         end
      end
   end
   return false
end




local Run = {}






function Run.new(
   first_line,
   last_line,
   first_stmt,
   last_stmt)

   local self = setmetatable({}, { __index = Run })
   self.first_line = first_line
   self.last_line = last_line
   self.first_stmt = first_stmt
   self.last_stmt = last_stmt
   return self
end




local Segment = {}




function Segment.frozen(run)
   local self = setmetatable({}, { __index = Segment })
   self.run = run
   return self
end

function Segment.structural(stmt)
   local self = setmetatable({}, { __index = Segment })
   self.stmt = stmt
   return self
end




local function back_extended_first_line(
   stmt, previous_boundary, fmt_off_regions)

   local first_line = stmt.y
   for _, comment in ipairs(stmt.leading_comments or {}) do
      if
comment.y > previous_boundary and fmt_off_regions:includes_line(comment.y) then

         first_line = math.min(first_line, comment.y)
      end
   end
   return first_line
end




local function gap_fully_frozen(
   fmt_off_regions, after, before)

   for line = after + 1, before - 1 do
      if not fmt_off_regions:includes_line(line) then
         return false
      end
   end
   return true
end




local function build_run(
   block,
   start_idx,
   previous_boundary,
   fmt_off_regions,
   block_dangling)

   local first_stmt = block[start_idx]
   local first_line = back_extended_first_line(
   first_stmt, previous_boundary, fmt_off_regions)

   local end_idx = start_idx
   local last_line = verbatim_extent(first_stmt, block_dangling)
   for candidate_idx = start_idx + 1, #block do
      local candidate = block[candidate_idx]
      if not stmt_overlaps_fmt_off(candidate, fmt_off_regions) then
         break
      end
      local next_first = back_extended_first_line(
      candidate, last_line, fmt_off_regions)

      if not gap_fully_frozen(fmt_off_regions, last_line, next_first) then
         break
      end
      end_idx = candidate_idx
      last_line = verbatim_extent(candidate, block_dangling)
   end
   return Run.new(first_line, last_line, first_stmt, block[end_idx]), end_idx
end

local function code_precedes_on_line(
   source_lines, line, column)

   return source_lines[line]:sub(1, column - 1):match("%S") ~= nil
end


local function next_structural_stmt(segments, idx)
   local next_segment = segments[idx + 1]
   if next_segment == nil then
      return nil
   end
   return next_segment.stmt
end




local function last_line_structural_col(
   block, run, next_structural)

   if next_structural ~= nil and next_structural.y == run.last_line then
      return next_structural.x
   end
   if block.yend == run.last_line then
      return block.close_x
   end
   return nil
end

local function collect_run_lines(
   source_lines,
   run,
   code_precedes_run,
   last_line_cut_col)

   local result = {}



   local start_col = 1
   if code_precedes_run then
      start_col = run.first_stmt.x
   end



   if run.first_line == run.last_line then
      local line_str = source_lines[run.last_line]
      local stop = #line_str
      if last_line_cut_col ~= nil then
         stop = last_line_cut_col - 1
      end
      local frozen = line_str:sub(start_col, stop):gsub("%s+$", "")
      if frozen ~= "" then
         table.insert(result, frozen)
      end
      return result
   end

   table.insert(result, source_lines[run.first_line]:sub(start_col))
   for line = run.first_line + 1, run.last_line do
      table.insert(result, source_lines[line])
   end





   if last_line_cut_col ~= nil then
      local truncated = (
      source_lines[run.last_line]:sub(1, last_line_cut_col - 1):gsub("%s+$", ""))

      if truncated == "" then
         table.remove(result)
      else
         result[#result] = truncated
      end
   end

   return result
end

local function blank_before_fmt_off_start(
   stmt, fmt_off_regions)

   for _, comment in ipairs(stmt.leading_comments or {}) do
      if fmt_off_regions:includes_line(comment.y) then
         return comment.blank_line_before
      end
   end
   return stmt.blank_line_before
end



local function group_segments(
   block,
   fmt_off_regions,
   block_dangling)

   local segments = {}
   local previous_boundary = 0
   local consumed = 0
   for idx, stmt in ipairs(block) do
      if idx > consumed then
         if stmt_overlaps_fmt_off(stmt, fmt_off_regions) then
            local run, run_end_idx = build_run(
            block, idx, previous_boundary, fmt_off_regions, block_dangling)

            consumed = run_end_idx
            previous_boundary = run.last_line
            table.insert(segments, Segment.frozen(run))
         else
            previous_boundary = stmt.yend
            table.insert(segments, Segment.structural(stmt))
         end
      end
   end
   return segments
end

local function render_block_doc(ctx, block)
   local parts = {}
   local verbatim_boundary = 0
   local line_before = false

   local segments = group_segments(
   block, ctx.fmt_off_regions, block.dangling_comments or {})

   for idx, segment in ipairs(segments) do
      if segment.run ~= nil then
         local run = segment.run
         local pre_fmt_off = {}
         for _, comment in ipairs(run.first_stmt.leading_comments or {}) do
            if not ctx.fmt_off_regions:includes_line(comment.y) then
               table.insert(pre_fmt_off, comment)
            end
         end
         local code_precedes_run = run.first_line == run.first_stmt.y and
         code_precedes_on_line(
         ctx.source_lines, run.first_line, run.first_stmt.x)

         local verbatim_lines = collect_run_lines(
         ctx.source_lines,
         run,
         code_precedes_run,
         last_line_structural_col(
         block, run, next_structural_stmt(segments, idx)))


         if #pre_fmt_off > 0 or #verbatim_lines > 0 then
            line_before = render_builders.append_comment_docs(
            parts, pre_fmt_off, line_before)

            if line_before then
               table.insert(parts, doc.hardline())
            end
            if blank_before_fmt_off_start(run.first_stmt, ctx.fmt_off_regions) then
               table.insert(parts, doc.hardline())
            end
            if #verbatim_lines > 0 then
               table.insert(parts, doc.raw(verbatim_lines))
            end
         end
         verbatim_boundary = run.last_line
      else
         local stmt = segment.stmt
         line_before = render_builders.append_comment_docs(
         parts, stmt.leading_comments or {}, line_before)


         table.insert(
         parts,
         render_builders.append_lines_pre_node(
         stmt, line_before, doc.stmt_sep_line()))


         local node_doc = stmt_doc.render_statement_doc(ctx, stmt)
         if node_doc == nil then
            fmt_logger.info(
            "[structural] blocked at line %s: could not render doc for %s",
            tostring(stmt.y),
            stmt.kind)

            return nil
         end
         table.insert(parts, node_doc)
         table.insert(parts, render_builders.trailing_comment_doc(stmt))
      end

      line_before = true
   end





   local dangling = {}
   for _, comment in ipairs(block.dangling_comments or {}) do
      if comment.y > verbatim_boundary then
         table.insert(dangling, comment)
      end
   end
   render_builders.append_comment_docs(parts, dangling, line_before)

   return doc.concat(parts)
end

function block_doc.make_context(
   fmt_off_regions, source_lines)

   return render_context.RenderContext.new(
   render_block_doc, expr_doc.render, fmt_off_regions, source_lines)

end

function block_doc.render_block(
   ctx, block, max_width, indent_width)

   local block_doc_node = render_block_doc(ctx, block)
   if block_doc_node == nil then
      return nil
   end

   local function get_prelude()
      if block.hashbang then
         return doc.concat({ doc.text(block.hashbang), doc.hardline() })
      else
         return doc.text("")
      end
   end

   local full_doc = doc.concat({ get_prelude(), block_doc_node, doc.hardline() })
   fmt_logger.debug("doc IR:\n%s", full_doc)
   local rendered = full_doc:render(max_width, indent_width)
   fmt_logger.debug("render output:\n%s", rendered)
   return rendered
end

return block_doc
