class Syntropy::Router

def [](path)

def [](path)
  get_route_entry(path)
end

def add_route(fn)

def add_route(fn)
  kind = route_kind(fn)
  rel_path = path_rel(fn)
  canonical_path = path_canonical(rel_path, kind)
  entry = { kind:, fn:, canonical_path: }
  entry[:handle_subtree] = true if (kind == :module) && !!(fn =~ /\+\.rb$/)
  @routes[canonical_path] = entry
  @files[fn] = entry
end

def calc_route_proc_with_hooks(entry, proc)

def calc_route_proc_with_hooks(entry, proc)
  compose_up_tree_hooks(entry[:fn], proc)
end

def compose_up_tree_hooks(path, proc)

def compose_up_tree_hooks(path, proc)
  parent = File.dirname(path)
  proc = hook_wrap_if_exists(File.join(parent, '_hook.rb'), proc)
  proc = error_handler_wrap_if_exists(File.join(parent, '_error.rb'), proc)
  return proc if parent == @root
  compose_up_tree_hooks(parent, proc)
end

def error_handler_wrap_if_exists(error_handler_fn, proc)

def error_handler_wrap_if_exists(error_handler_fn, proc)
  return proc if !File.file?(error_handler_fn)
  ref = path_rel(error_handler_fn).gsub(/\.rb$/, '')
  error_proc = @module_loader.load(ref)
  proc do |req|
    proc.(req)
  rescue StandardError => e
    error_proc.(req, e)
  end
end

def file_watcher_loop

def file_watcher_loop
  wf = @opts[:watch_files]
  period = wf.is_a?(Numeric) ? wf : 0.1
  Syntropy.file_watch(@machine, @root, period: period) do |event, fn|
    handle_changed_file(event, fn)
  rescue Exception => e
    p e
    p e.backtrace
    exit!
  end
end

def find_index_route(path)

def find_index_route(path)
  m = path.match(INDEX_OPT_EXT_RE)
  return nil if !m
  @routes[m[1]]
end

def find_route_entry(path)

def find_route_entry(path)
  return NOT_FOUND if path =~ FORBIDDEN_RE
  @routes[path] || find_index_route(path) || find_up_tree_module(path) || NOT_FOUND
end

def find_up_tree_module(path)

def find_up_tree_module(path)
  parent_path = path_parent(path)
  return nil if !parent_path
  entry = @routes[parent_path]
  return entry if entry && entry[:handle_subtree]
  find_up_tree_module(parent_path)
end

def get_route_entry(path, use_cache: true)

def get_route_entry(path, use_cache: true)
  if use_cache
    cached = @cache[path]
    return cached if cached
  end
  entry = find_route_entry(path)
  set_cache(path, entry) if use_cache && entry[:kind] != :not_found
  entry
end

def handle_added_file(fn)

def handle_added_file(fn)
  add_route(fn)
  @cache.clear # TODO: remove only relevant cache entries
end

def handle_changed_file(event, fn)

def handle_changed_file(event, fn)
  @opts[:logger]&.call("Detected changed file: #{event} #{fn}")
  @module_loader&.invalidate(fn)
  case event
  when :added
    handle_added_file(fn)
  when :removed
    handle_removed_file(fn)
  when :modified
    handle_modified_file(fn)
  end
end

def handle_modified_file(fn)

def handle_modified_file(fn)
  entry = @files[fn]
  if entry && entry[:kind] == :module
    # invalidate the entry proc, so it will be recalculated
    entry[:proc] = nil
  end
end

def handle_removed_file(fn)

def handle_removed_file(fn)
  entry = @files[fn]
  if entry
    remove_entry_cache_keys(entry)
    @routes.delete(entry[:canonical_path])
    @files.delete(fn)
  end
end

def hook_wrap_if_exists(hook_fn, proc)

def hook_wrap_if_exists(hook_fn, proc)
  return proc if !File.file?(hook_fn)
  ref = path_rel(hook_fn).gsub(/\.rb$/, '')
  hook_proc = @module_loader.load(ref)
  ->(req) { hook_proc.(req, proc) }
end

def initialize(opts, module_loader = nil)

def initialize(opts, module_loader = nil)
  raise 'Invalid location given' if !File.directory?(opts[:location])
  @opts = opts
  @machine = opts[:machine]
  @root = File.expand_path(opts[:location])
  @mount_path = opts[:mount_path] || '/'
  @rel_path_re ||= /^#{@root}/
  @module_loader = module_loader
  @cache        = {} # maps url path to route entry
  @routes       = {} # maps canonical path to route entry (actual routes)
  @files        = {} # maps filename to entry
  @deps         = {} # maps filenames to array of dependent entries
  @x  = {} # maps directories to hook chains
  scan_routes
end

def path_abs(path, base)

def path_abs(path, base)
  File.join(base, path)
end

def path_canonical(rel_path, kind)

def path_canonical(rel_path, kind)
  clean = path_clean(rel_path, kind)
  clean.empty? ? @mount_path : File.join(@mount_path, clean)
end

def path_clean(rel_path, kind)

def path_clean(rel_path, kind)
  if (m = rel_path.match(INDEX_RE))
    return m[1]
  end
  case kind
  when :static
    rel_path
  when :markdown
    rel_path.gsub(MD_EXT_RE, '')
  when :module
    rel_path.gsub(RB_EXT_RE, '')
  end
end

def path_parent(path)

def path_parent(path)
  return nil if path == '/'
  m = path.match(PATH_PARENT_RE)
  m && (m[1] || '/')
end

def path_rel(path)

def path_rel(path)
  path.gsub(@rel_path_re, '')
end

def remove_entry_cache_keys(entry)

def remove_entry_cache_keys(entry)
  entry[:cache_keys]&.each_key { @cache.delete(it) }.clear
end

def route_kind(fn)

def route_kind(fn)
  case File.extname(fn)
  when '.md'
    :markdown
  when '.rb'
    :module
  else
    :static
  end
end

def scan_routes(dir = nil)

def scan_routes(dir = nil)
  dir ||= @root
  Dir[File.join(dir, '*')].each do
    basename = File.basename(it)
    next if (basename =~ HIDDEN_RE)
    File.directory?(it) ? scan_routes(it) : add_route(it)
  end
end

def set_cache(path, entry)

def set_cache(path, entry)
  @cache[path] = entry
  (entry[:cache_keys] ||= {})[path] = true
end

def start_file_watcher

def start_file_watcher
  @opts[:logger]&.call('Watching for file changes...', nil)
  @machine.spin { file_watcher_loop }
end