require 'journey/core-ext/hash'
require 'journey/router/utils'
require 'journey/router/strexp'
require 'journey/routes'
require 'journey/formatter'

before = $-w
$-w = false
require 'journey/parser'
$-w = before

require 'journey/route'
require 'journey/path/pattern'

module Journey
  class Router
    class RoutingError < ::StandardError
    end

    VERSION = '1.0.4'

    class NullReq # :nodoc:
      attr_reader :env
      def initialize env
        @env = env
      end

      def request_method
        env['REQUEST_METHOD']
      end

      def path_info
        env['PATH_INFO']
      end

      def ip
        env['REMOTE_ADDR']
      end

      def [](k); env[k]; end
    end

    attr_reader :request_class, :formatter
    attr_accessor :routes

    def initialize routes, options
      @options       = options
      @params_key    = options[:parameters_key]
      @request_class = options[:request_class] || NullReq
      @routes        = routes
    end

    def call env
      env['PATH_INFO'] = Utils.normalize_path env['PATH_INFO']

      find_routes(env).each do |match, parameters, route|
        script_name, path_info, set_params = env.values_at('SCRIPT_NAME',
                                                           'PATH_INFO',
                                                           @params_key)

        unless route.path.anchored
          env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/')
          env['PATH_INFO']   = Utils.normalize_path(match.post_match)
        end

        env[@params_key] = (set_params || {}).merge parameters

        status, headers, body = route.app.call(env)

        if 'pass' == headers['X-Cascade']
          env['SCRIPT_NAME'] = script_name
          env['PATH_INFO']   = path_info
          env[@params_key]   = set_params
          next
        end

        return [status, headers, body]
      end

      return [404, {'X-Cascade' => 'pass'}, ['Not Found']]
    end

    def recognize req
      find_routes(req.env).each do |match, parameters, route|
        unless route.path.anchored
          req.env['SCRIPT_NAME'] = match.to_s
          req.env['PATH_INFO']   = match.post_match.sub(/^([^\/])/, '/\1')
        end

        yield(route, nil, parameters)
      end
    end

    def visualizer
      tt     = GTG::Builder.new(ast).transition_table
      groups = partitioned_routes.first.map(&:ast).group_by { |a| a.to_s }
      asts   = groups.values.map { |v| v.first }
      tt.visualizer asts
    end

    private

    def partitioned_routes
      routes.partitioned_routes
    end

    def ast
      routes.ast
    end

    def simulator
      routes.simulator
    end

    def custom_routes
      partitioned_routes.last
    end

    def filter_routes path
      return [] unless ast
      data = simulator.match(path)
      data ? data.memos : []
    end

    def find_routes env
      req = request_class.new env

      routes = filter_routes(req.path_info) + custom_routes.find_all { |r|
        r.path.match(req.path_info)
      }

      routes.sort_by(&:precedence).find_all { |r|
        r.constraints.all? { |k,v| v === req.send(k) } &&
          r.verb === req.request_method
      }.reject { |r| req.ip && !(r.ip === req.ip) }.map { |r|
        match_data  = r.path.match(req.path_info)
        match_names = match_data.names.map { |n| n.to_sym }
        match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) }
        info = Hash[match_names.zip(match_values).find_all { |_,y| y }]

        [match_data, r.defaults.merge(info), r]
      }
    end
  end
end
