# frozen_string_literal: true

require 'sinatra/base'

module Sinatra
  # = Sinatra::Reloader
  #
  # <b>DEPRECATED:<b> Please consider using an alternative like
  # <tt>rerun</tt> or <tt>rack-unreloader</tt> instead.
  #
  # Extension to reload modified files.  Useful during development,
  # since it will automatically require files defining routes, filters,
  # error handlers and inline templates, with every incoming request,
  # but only if they have been updated.
  #
  # == Usage
  #
  # === Classic Application
  #
  # To enable the reloader in a classic application all you need to do is
  # require it:
  #
  #     require "sinatra"
  #     require "sinatra/reloader" if development?
  #
  #     # Your classic application code goes here...
  #
  # === Modular Application
  #
  # To enable the reloader in a modular application all you need to do is
  # require it, and then, register it:
  #
  #     require "sinatra/base"
  #     require "sinatra/reloader"
  #
  #     class MyApp < Sinatra::Base
  #       configure :development do
  #         register Sinatra::Reloader
  #       end
  #
  #       # Your modular application code goes here...
  #     end
  #
  # == Using the Reloader in Other Environments
  #
  # By default, the reloader is only enabled for the development
  # environment. Similar to registering the reloader in a modular
  # application, a classic application requires manually enabling the
  # extension for it to be available in a non-development environment.
  #
  #      require "sinatra"
  #      require "sinatra/reloader"
  #
  #      configure :production do
  #        enable :reloader
  #      end
  #
  # == Changing the Reloading Policy
  #
  # You can refine the reloading policy with +also_reload+ and
  # +dont_reload+, to customize which files should, and should not, be
  # reloaded, respectively. You can also use +after_reload+ to execute a
  # block after any file being reloaded.
  #
  # === Classic Application
  #
  # Simply call the methods:
  #
  #     require "sinatra"
  #     require "sinatra/reloader" if development?
  #
  #     also_reload '/path/to/some/file'
  #     dont_reload '/path/to/other/file'
  #     after_reload do
  #       puts 'reloaded'
  #     end
  #
  #     # Your classic application code goes here...
  #
  # === Modular Application
  #
  # Call the methods inside the +configure+ block:
  #
  #     require "sinatra/base"
  #     require "sinatra/reloader"
  #
  #     class MyApp < Sinatra::Base
  #       configure :development do
  #         register Sinatra::Reloader
  #         also_reload '/path/to/some/file'
  #         dont_reload '/path/to/other/file'
  #         after_reload do
  #           puts 'reloaded'
  #         end
  #       end
  #
  #       # Your modular application code goes here...
  #     end
  #
  module Reloader
    # Watches a file so it can tell when it has been updated, and what
    # elements does it contain.
    class Watcher
      # Represents an element of a Sinatra application that may need to
      # be reloaded.  An element could be:
      # * a route
      # * a filter
      # * an error handler
      # * a middleware
      # * inline templates
      #
      # Its +representation+ attribute is there to allow to identify the
      # element within an application, that is, to match it with its
      # Sinatra's internal representation.
      class Element < Struct.new(:type, :representation)
      end

      # Collection of file +Watcher+ that can be associated with a
      # Sinatra application.  That way, we can know which files belong
      # to a given application and which files have been modified.  It
      # also provides a mechanism to inform a Watcher of the elements
      # defined in the file being watched and if its changes should be
      # ignored.
      class List
        @app_list_map = Hash.new { |hash, key| hash[key] = new }

        # Returns the +List+ for the application +app+.
        def self.for(app)
          @app_list_map[app]
        end

        # Creates a new +List+ instance.
        def initialize
          @path_watcher_map = Hash.new do |hash, key|
            hash[key] = Watcher.new(key)
          end
        end

        # Lets the +Watcher+ for the file located at +path+ know that the
        # +element+ is defined there, and adds the +Watcher+ to the +List+,
        # if it isn't already there.
        def watch(path, element)
          watcher_for(path).elements << element
        end

        # Tells the +Watcher+ for the file located at +path+ to ignore
        # the file changes, and adds the +Watcher+ to the +List+, if
        # it isn't already there.
        def ignore(path)
          watcher_for(path).ignore
        end

        # Adds a +Watcher+ for the file located at +path+ to the
        # +List+, if it isn't already there.
        def watcher_for(path)
          @path_watcher_map[File.expand_path(path)]
        end
        alias watch_file watcher_for

        # Returns an array with all the watchers in the +List+.
        def watchers
          @path_watcher_map.values
        end

        # Returns an array with all the watchers in the +List+ that
        # have been updated.
        def updated
          watchers.find_all(&:updated?)
        end
      end

      attr_reader :path, :elements, :mtime

      # Creates a new +Watcher+ instance for the file located at +path+.
      def initialize(path)
        @ignore = nil
        @path = path
        @elements = []
        update
      end

      # Indicates whether or not the file being watched has been modified.
      def updated?
        !ignore? && !removed? && mtime != File.mtime(path)
      end

      # Updates the mtime of the file being watched.
      def update
        @mtime = File.mtime(path)
      end

      # Indicates whether or not the file being watched has inline
      # templates.
      def inline_templates?
        elements.any? { |element| element.type == :inline_templates }
      end

      # Informs that the modifications to the file being watched
      # should be ignored.
      def ignore
        @ignore = true
      end

      # Indicates whether or not the modifications to the file being
      # watched should be ignored.
      def ignore?
        !!@ignore
      end

      # Indicates whether or not the file being watched has been removed.
      def removed?
        !File.exist?(path)
      end
    end

    MUTEX_FOR_PERFORM = Mutex.new

    # Allow a block to be executed after any file being reloaded
    @@after_reload = []
    def after_reload(&block)
      @@after_reload << block
    end

    # When the extension is registered it extends the Sinatra application
    # +klass+ with the modules +BaseMethods+ and +ExtensionMethods+ and
    # defines a before filter to +perform+ the reload of the modified files.
    def self.registered(klass)
      @reloader_loaded_in ||= {}
      return if @reloader_loaded_in[klass]

      @reloader_loaded_in[klass] = true

      klass.extend BaseMethods
      klass.extend ExtensionMethods
      klass.set(:reloader) { klass.development? }
      klass.set(:reload_templates) { klass.reloader? }
      klass.before do
        if klass.reloader?
          MUTEX_FOR_PERFORM.synchronize { Reloader.perform(klass) }
        end
      end
      klass.set(:inline_templates, klass.app_file) if klass == Sinatra::Application
    end

    # Reloads the modified files, adding, updating and removing the
    # needed elements.
    def self.perform(klass)
      reloaded_paths = []
      Watcher::List.for(klass).updated.each do |watcher|
        klass.set(:inline_templates, watcher.path) if watcher.inline_templates?
        watcher.elements.each { |element| klass.deactivate(element) }
        # Deletes all old elements.
        watcher.elements.delete_if { true }
        $LOADED_FEATURES.delete(watcher.path)
        require watcher.path
        watcher.update
        reloaded_paths << watcher.path
      end
      return if reloaded_paths.empty?

      @@after_reload.each do |block|
        block.arity.zero? ? block.call : block.call(reloaded_paths)
      end
      # Prevents after_reload from increasing each time it's reloaded.
      @@after_reload.delete_if do |blk|
        path, = blk.source_location
        path && reloaded_paths.include?(path)
      end
    end

    # Contains the methods defined in Sinatra::Base that are overridden.
    module BaseMethods
      # Protects Sinatra::Base.run! from being called more than once.
      def run!(*args)
        if settings.reloader?
          super unless running?
        else
          super
        end
      end

      # Does everything Sinatra::Base#route does, but it also tells the
      # +Watcher::List+ for the Sinatra application to watch the defined
      # route.
      #
      # Note: We are using #compile! so we don't interfere with extensions
      # changing #route.
      def compile!(verb, path, block, **options)
        source_location = block.respond_to?(:source_location) ?
          block.source_location.first : caller_files[1]
        signature = super
        watch_element(
          source_location, :route, { verb: verb, signature: signature }
        )
        signature
      end

      # Does everything Sinatra::Base#inline_templates= does, but it also
      # tells the +Watcher::List+ for the Sinatra application to watch the
      # inline templates in +file+ or the file who made the call to this
      # method.
      def inline_templates=(file = nil)
        file = (caller_files[1] || File.expand_path($0)) if file.nil? || file == true
        watch_element(file, :inline_templates)
        super
      end

      # Does everything Sinatra::Base#use does, but it also tells the
      # +Watcher::List+ for the Sinatra application to watch the middleware
      # being used.
      def use(middleware, *args, &block)
        path = caller_files[1] || File.expand_path($0)
        watch_element(path, :middleware, [middleware, args, block])
        super
      end

      # Does everything Sinatra::Base#add_filter does, but it also tells
      # the +Watcher::List+ for the Sinatra application to watch the defined
      # filter.
      def add_filter(type, path = nil, **options, &block)
        source_location = block.respond_to?(:source_location) ?
          block.source_location.first : caller_files[1]
        result = super
        watch_element(source_location, :"#{type}_filter", filters[type].last)
        result
      end

      # Does everything Sinatra::Base#error does, but it also tells the
      # +Watcher::List+ for the Sinatra application to watch the defined
      # error handler.
      def error(*codes, &block)
        path = caller_files[1] || File.expand_path($0)
        result = super
        codes.each do |c|
          watch_element(path, :error, code: c, handler: @errors[c])
        end
        result
      end

      # Does everything Sinatra::Base#register does, but it also lets the
      # reloader know that an extension is being registered, because the
      # elements defined in its +registered+ method need a special treatment.
      def register(*extensions, &block)
        start_registering_extension
        result = super
        stop_registering_extension
        result
      end

      # Does everything Sinatra::Base#register does and then registers the
      # reloader in the +subclass+.
      def inherited(subclass)
        result = super
        subclass.register Sinatra::Reloader
        result
      end
    end

    # Contains the methods that the extension adds to the Sinatra application.
    module ExtensionMethods
      # Removes the +element+ from the Sinatra application.
      def deactivate(element)
        case element.type
        when :route
          verb      = element.representation[:verb]
          signature = element.representation[:signature]
          (routes[verb] ||= []).delete(signature)
        when :middleware
          @middleware.delete(element.representation)
        when :before_filter
          filters[:before].delete(element.representation)
        when :after_filter
          filters[:after].delete(element.representation)
        when :error
          code    = element.representation[:code]
          handler = element.representation[:handler]
          @errors.delete(code) if @errors[code] == handler
        end
      end

      # Indicates with a +glob+ which files should be reloaded if they
      # have been modified.  It can be called several times.
      def also_reload(*glob)
        Dir[*glob].each { |path| Watcher::List.for(self).watch_file(path) }
      end

      # Indicates with a +glob+ which files should not be reloaded even if
      # they have been modified.  It can be called several times.
      def dont_reload(*glob)
        Dir[*glob].each { |path| Watcher::List.for(self).ignore(path) }
      end

      private

      # attr_reader :register_path warn on -w (private attribute)
      def register_path; @register_path ||= nil; end

      # Indicates an extesion is being registered.
      def start_registering_extension
        @register_path = caller_files[2]
      end

      # Indicates the extesion has already been registered.
      def stop_registering_extension
        @register_path = nil
      end

      # Indicates whether or not an extension is being registered.
      def registering_extension?
        !register_path.nil?
      end

      # Builds a Watcher::Element from +type+ and +representation+ and
      # tells the Watcher::List for the current application to watch it
      # in the file located at +path+.
      #
      # If an extension is being registered, it also tells the list to
      # watch it in the file where the extension has been registered.
      # This prevents the duplication of the elements added by the
      # extension in its +registered+ method with every reload.
      def watch_element(path, type, representation = nil)
        list = Watcher::List.for(self)
        element = Watcher::Element.new(type, representation)
        list.watch(path, element)
        list.watch(register_path, element) if registering_extension?
      end
    end
  end

  register Reloader
  Delegator.delegate :also_reload, :dont_reload
end
