# frozen_string_literal: true

require 'sinatra/base'
require 'mustermann'

module Sinatra
  # = Sinatra::Namespace
  #
  # <tt>Sinatra::Namespace</tt> is an extension that adds namespaces to an
  # application.  This namespaces will allow you to share a path prefix for the
  # routes within the namespace, and define filters, conditions and error
  # handlers exclusively for them.  Besides that, you can also register helpers
  # and extensions that will be used only within the namespace.
  #
  # == Usage
  #
  # Once you have loaded the extension (see below), you can use the +namespace+
  # method to define namespaces in your application.
  #
  # You can define a namespace by a path prefix:
  #
  #     namespace '/blog' do
  #       get { haml :blog }
  #       get '/:entry_permalink' do
  #         @entry = Entry.find_by_permalink!(params[:entry_permalink])
  #         haml :entry
  #       end
  #
  #       # More blog routes...
  #     end
  #
  # by a condition:
  #
  #     namespace :host_name => 'localhost' do
  #       get('/admin/dashboard') { haml :dashboard }
  #       get('/admin/login')     { haml :login }
  #
  #       # More admin routes...
  #     end
  #
  # or both:
  #
  #     namespace '/admin', :host_name => 'localhost' do
  #       get('/dashboard')  { haml :dashboard }
  #       get('/login')      { haml :login }
  #       post('/login')     { login_user }
  #
  #       # More admin routes...
  #     end
  #
  # Regex is also accepted:
  #
  #     namespace /\/posts\/([^\/&?]+)\// do
  #       get { haml :blog }
  #
  #       # More blog routes...
  #     end
  #
  # When you define a filter or an error handler, or register an extension or a
  # set of helpers within a namespace, they only affect the routes defined in
  # it.  For instance, lets define a before filter to prevent the access of
  # unauthorized users to the admin section of the application:
  #
  #     namespace '/admin' do
  #       helpers AdminHelpers
  #       before  { authenticate unless request.path_info == '/admin/login' }
  #
  #       get '/dashboard' do
  #         # Only authenticated users can access here...
  #         haml :dashboard
  #       end
  #
  #       # More admin routes...
  #     end
  #
  #     get '/' do
  #       # Any user can access here...
  #       haml :index
  #     end
  #
  # Well, they actually also affect the nested namespaces:
  #
  #     namespace '/admin' do
  #       helpers AdminHelpers
  #       before  { authenticate unless request.path_info == '/admin/login' }
  #
  #       namespace '/users' do
  #         get do
  #           # Only authenticated users can access here...
  #           @users = User.all
  #           haml :users
  #         end
  #
  #         # More user admin routes...
  #       end
  #
  #       # More admin routes...
  #     end
  #
  # Redirecting within the namespace can be done using redirect_to:
  #
  #     namespace '/admin' do
  #       get '/foo'  do
  #         redirect_to '/bar' # Redirects to /admin/bar
  #       end
  #
  #       get '/foo' do
  #         redirect '/bar' # Redirects to /bar
  #       end
  #     end
  #
  # === Classic Application Setup
  #
  # To be able to use namespaces in a classic application all you need to do is
  # require the extension:
  #
  #     require "sinatra"
  #     require "sinatra/namespace"
  #
  #     namespace '/users' do
  #     end
  #
  # === Modular Application Setup
  #
  # To be able to use namespaces in a modular application all you need to do is
  # require the extension, and then, register it:
  #
  #     require "sinatra/base"
  #     require "sinatra/namespace"
  #
  #     class MyApp < Sinatra::Base
  #       register Sinatra::Namespace
  #
  #       namespace '/users' do
  #       end
  #     end
  #
  # === Within an extension
  #
  # To be able to use namespaces within an extension, you need to first create
  # an extension. This includes defining the `registered(app)` method in the
  # module.
  #
  #     require 'sinatra/base' # For creating Sinatra extensions
  #     require 'sinatra/namespace' # To create namespaces
  #
  #     module Zomg # Keep everything under "Zomg" namespace for sanity
  #       module Routes # Define a new "Routes" module
  #
  #         def self.registered(app)
  #           # First, register the Namespace extension
  #           app.register Sinatra::Namespace
  #
  #           # This defines an `/api` namespace on the application
  #           app.namespace '/api' do
  #             get '/users' do
  #               # do something with `GET "/api/users"`
  #             end
  #           end
  #
  #         end
  #       end
  #
  #       # Lastly, register the extension to use in our app
  #       Sinatra.register Routes
  #     end
  #
  # In order to use this extension, is the same as any other Sinatra extension:
  #
  #     module Zomg
  #       # Define our app class, we use modular app for this example
  #       class App < Sinatra::Base
  #         # this gives us all the namespaces we defined earlier
  #         register Routes
  #
  #         get '/' do
  #           "Welcome to my application!"
  #         end
  #       end
  #     end
  #
  #     Zomg::App.run! # Don't forget to start your app ;)
  #
  # Phew! That was a mouthful.
  #
  # I hope that helps you use `Sinatra::Namespace` in every way imaginable!
  #
  module Namespace
    def self.new(base, pattern, conditions = {}, &block)
      Module.new do
        # quelch uninitialized variable warnings, since these get used by compile method.
        @pattern = nil
        @conditions = nil
        extend NamespacedMethods
        include InstanceMethods
        @base = base
        @extensions = []
        @errors = {}
        @pattern, @conditions = compile(pattern, conditions)
        @templates            = Hash.new { |_h, k| @base.templates[k] }
        namespace = self
        before { extend(@namespace = namespace) }
        class_eval(&block)
      end
    end

    module InstanceMethods
      def settings
        @namespace
      end

      def template_cache
        super.fetch(:nested, @namespace) { TemplateCache.new }
      end

      def redirect_to(uri, *args)
        redirect("#{@namespace.pattern}#{uri}", *args)
      end
    end

    module SharedMethods
      def namespace(pattern, conditions = {}, &block)
        Sinatra::Namespace.new(self, pattern, conditions, &block)
      end
    end

    module NamespacedMethods
      include SharedMethods
      attr_reader :base, :templates

      ALLOWED_ENGINES = %i[
        erb erubi haml hamlit builder nokogiri sass scss
        liquid markdown rdoc asciidoc markaby
        rabl slim yajl
      ]

      def self.prefixed(*names)
        names.each { |n| define_method(n) { |*a, &b| prefixed(n, *a, &b) } }
      end

      prefixed :before, :after, :delete, :get, :head, :options, :patch, :post, :put

      def helpers(*extensions, &block)
        class_eval(&block) if block_given?
        include(*extensions) if extensions.any?
      end

      def register(*extensions, &block)
        extensions << Module.new(&block) if block_given?
        @extensions += extensions
        extensions.each do |extension|
          extend extension
          extension.registered(self) if extension.respond_to?(:registered)
        end
      end

      def invoke_hook(name, *args)
        @extensions.each { |e| e.send(name, *args) if e.respond_to?(name) }
      end

      def not_found(&block)
        error(Sinatra::NotFound, &block)
      end

      def errors
        base.errors.merge(namespace_errors)
      end

      def namespace_errors
        @errors
      end

      def error(*codes, &block)
        args  = Sinatra::Base.send(:compile!, 'ERROR', /.*/, block)
        codes = codes.map { |c| Array(c) }.flatten
        codes << Exception if codes.empty?
        codes << Sinatra::NotFound if codes.include?(404)

        codes.each do |c|
          errors = @errors[c] ||= []
          errors << args
        end
      end

      def respond_to(*args)
        return @conditions[:provides] || base.respond_to if args.empty?

        @conditions[:provides] = args
      end

      def set(key, value = self, &block)
        return key.each { |k, v| set(k, v) } if key.respond_to?(:each) && block.nil? && (value == self)
        raise ArgumentError, "may not set #{key}" unless ([:views] + ALLOWED_ENGINES).include?(key)

        block ||= proc { value }
        singleton_class.send(:define_method, key, &block)
      end

      def enable(*opts)
        opts.each { |key| set(key, true) }
      end

      def disable(*opts)
        opts.each { |key| set(key, false) }
      end

      def template(name, &block)
        first_location = caller_locations.first
        filename = first_location.path
        line = first_location.lineno
        templates[name] = [block, filename, line]
      end

      def layout(name = :layout, &block)
        template name, &block
      end

      def pattern
        @pattern
      end

      private

      def app
        base.respond_to?(:base) ? base.base : base
      end

      def compile(pattern, conditions, default_pattern = nil)
        if pattern.respond_to? :to_hash
          conditions = conditions.merge pattern.to_hash
          pattern = nil
        end
        base_pattern = @pattern
        base_conditions = @conditions
        pattern ||= default_pattern
        [prefixed_path(base_pattern, pattern),
         (base_conditions || {}).merge(conditions)]
      end

      def prefixed_path(a, b)
        return a || b || /.*/ unless a && b
        return Mustermann.new(b) if a == /.*/

        Mustermann.new(a) + Mustermann.new(b)
      end

      def prefixed(method, pattern = nil, conditions = {}, &block)
        default = %r{(?:/.*)?} if (method == :before) || (method == :after)
        pattern, conditions = compile pattern, conditions, default
        result = base.send(method, pattern, **conditions, &block)
        invoke_hook :route_added, method.to_s.upcase, pattern, block
        result
      end

      def method_missing(method, *args, &block)
        base.send(method, *args, &block)
      end

      def respond_to?(method, include_private = false)
        super || base.respond_to?(method, include_private)
      end
    end

    module BaseMethods
      include SharedMethods
    end

    def self.extended(base)
      base.extend BaseMethods
    end
  end

  register Sinatra::Namespace
  Delegator.delegate :namespace
end
