class Rack::App::Router
  require 'rack/app/router/tree'
  require 'rack/app/router/error'

  attr_reader :tree

  NOT_FOUND_APP = lambda do |_env|
    rack_response = Rack::Response.new
    rack_response.status = 404
    rack_response.write('404 Not Found')
    rack_response.finish
  end

  def call(env)
    env[Rack::App::Constants::ENV::ROUTER] = self
    @tree.call(env) || NOT_FOUND_APP.call(env)
  end

  def endpoints
    @endpoints ||= []
  end

  def register_endpoint!(endpoint)
    endpoints.push(endpoint)
    compile_endpoint!(endpoint)
    endpoint
  end

  # add ! to method name
  def reset
    @lookup_paths = {} # (Hash.new)
    @tree = Rack::App::Router::Tree.new
    compile_registered_endpoints!
  end

  # rename to merge!
  def merge_router!(router, prop = {})
    raise(ArgumentError, 'invalid router object, must implement :endpoints interface') unless router.respond_to?(:endpoints)
    router.endpoints.each do |endpoint|
      new_request_path = ::Rack::App::Utils.join(prop[:namespaces], endpoint.request_path)
      new_ancestors = endpoint.config.ancestor_apps + [prop[:new_ancestor]]
      new_endpoint = endpoint.fork(:request_path => new_request_path, :ancestors => new_ancestors)
      register_endpoint!(new_endpoint)
    end
    nil
  end

  def show_endpoints
    endpoints = self.endpoints

    wd0 = endpoints.map { |endpoint| endpoint.request_method.to_s.length }.max
    wd1 = endpoints.map { |endpoint| endpoint.request_path.to_s.length }.max
    wd2 = endpoints.map { |endpoint| endpoint.description.to_s.length }.max

    endpoints.sort_by { |endpoint| [endpoint.request_method, endpoint.request_path] }.map do |endpoint|
      [
        endpoint.request_method.to_s.ljust(wd0),
        endpoint.request_path.to_s.ljust(wd1),
        endpoint.description.to_s.ljust(wd2)
      ].join('   ')
    end
  end

  def path_to(klass, defined_path)
    @lookup_paths[klass] || raise(self.class::Error::AppIsNotMountedInTheRouter, "#{klass} is not registered in the router")
    found_path = @lookup_paths[klass][defined_path] || raise(self.class::Error::MountedAppDoesNotHaveThisPath, 'missing path reference')
    found_path.dup
  end

  protected

  def initialize
    reset
  end

  def compile_registered_endpoints!
    endpoints.each do |endpoint|
      compile_endpoint!(endpoint)
    end
  end

  def compile_endpoint!(endpoint)
    @tree.add(endpoint)
    add_to_lookup_paths(endpoint)
  end

  def add_to_lookup_paths(endpoint)
    return unless endpoint.rack_app?
    def_path = endpoint.config.defined_request_path
    final_path = endpoint.request_path
    dictionary = @lookup_paths[endpoint.config.app_class] ||= {}
    dictionary[def_path] = final_path
  end
end
