# frozen_string_literal: true

module ApipieDSL
  class Application
    if defined? Rails
      require 'apipie_dsl/static_dispatcher'

      class Engine < Rails::Engine
        initializer 'static assets', before: :build_middleware_stack do |app|
          app.middleware.use ::ApipieDSL::StaticDispatcher, "#{root}/app/public"
        end
      end
    end
    attr_reader :class_descriptions, :refs

    def initialize
      initialize_environment
    end

    def available_versions
      @class_descriptions.keys.sort
    end

    def set_class_versions(klass, versions)
      @class_versions[klass.to_s] = versions
    end

    def class_versions(klass)
      ret = @class_versions[klass.to_s]
      return ret unless ret.empty?
      return [ApipieDSL.configuration.default_version] if [Class, Module].include?(klass) || klass.nil?

      class_versions(ApipieDSL.superclass_for(klass))
    end

    def get_class_name(klass)
      class_name = klass.respond_to?(:name) ? klass.name : klass
      raise ArgumentError, "ApipieDSL: Can not resolve class #{klass} name." unless class_name.is_a?(String)
      return class_name if ApipieDSL.configuration.class_full_names?

      class_name.split('::').last
    end

    def define_param_group(klass, name, &block)
      key = "#{klass.name}##{name}"
      @param_groups[key] = block
    end

    def get_param_group(klass, name)
      key = "#{klass.name}##{name}"
      raise StandardError, "Param group #{key} is not defined" unless @param_groups.key?(key)

      @param_groups[key]
    end

    def define_prop_group(klass, name, &block)
      key = "#{klass.name}##{name}"
      @prop_groups[key] = block
    end

    def get_prop_group(klass, name)
      key = "#{klass.name}##{name}"
      raise StandardError, "Prop group #{key} is not defined" unless @prop_groups.key?(key)

      @prop_groups[key]
    end

    def define_method_description(klass, method_name, dsl_data)
      return if ignored?(klass, method_name)

      ret_method_description = nil
      versions = dsl_data[:dsl_versions] || []
      versions = class_versions(klass) if versions.empty?

      versions.each do |version|
        class_name_with_version = "#{version}##{get_class_name(klass)}"
        class_description = get_class_description(class_name_with_version)

        if class_description.nil?
          class_dsl_data = { sections: [ApipieDSL.configuration.default_section] }
          class_description = define_class_description(klass, version, class_dsl_data)
        end

        method_description = ApipieDSL::MethodDescription.new(method_name, class_description, dsl_data)

        # Create separate method description for each version in
        # case the method belongs to more versions. Return just one
        # because the version doesn't matter for the purpose it's used
        # (to wrap the original version with validators)
        ret_method_description ||= method_description
        class_description.add_method_description(method_description)
      end

      ret_method_description
    end

    def define_class_description(klass, version, dsl_data = {})
      return if ignored?(klass)

      class_name = dsl_data[:class_name] || get_class_name(klass)
      class_description = @class_descriptions[version][class_name]
      if class_description
        # Already defined the class somewhere (probably in
        # some method. Updating just meta data from dsl
        class_description.update_from_dsl_data(dsl_data) unless dsl_data.empty?
      else
        class_description = ApipieDSL::ClassDescription.new(klass, class_name, dsl_data, version)
        ApipieDSL.debug("@class_descriptions[#{version}][#{class_name}] = #{class_description}")
        @class_descriptions[version][class_description.id] ||= class_description
      end
      class_description.refs.each do |ref|
        if @refs[version][ref]
          ApipieDSL.debug("Overwriting refs for #{class_name}")
        end

        @refs[version][ref] = class_description
      end
      class_description
    end

    # get method
    #
    # There are two ways how this method can be used:
    # 1) Specify both parameters
    #   class_name:
    #       class - IO
    #       string with resource name and version - "v1#io"
    #   method_name: name of the method (string or symbol)
    #
    # 2) Specify only first parameter:
    #   class_name: string containing both class and method name joined
    #   with '#' symbol.
    #   - "io#puts" get default version
    #   - "v2#io#puts" get specific version
    def get_method_description(class_name, method_name = nil)
      ApipieDSL.debug("Getting #{class_name}##{method_name} documentation")
      crumbs = class_name.split('#')
      method_name = crumbs.pop if method_name.nil?
      class_name = crumbs.join('#')
      class_description = get_class_description(class_name)
      raise ArgumentError, "Class #{class_name} does not exists." if class_description.nil?

      class_description.method_description(method_name.to_sym)
    end

    # options:
    # => "io"
    # => "v2#io"
    # =>  V2::IO
    def get_class_description(klass, version = nil)
      return nil if [NilClass, nil].include?(klass)

      if klass.is_a?(String)
        crumbs = klass.split('#')
        version = crumbs.first if crumbs.size == 2
        version ||= ApipieDSL.configuration.default_version
        return @class_descriptions[version][crumbs.last] if @class_descriptions.key?(version)
      else
        class_name = get_class_name(klass)
        class_name = "#{version}##{class_name}" if version
        return nil if class_name.nil?

        class_description = get_class_description(class_name)
        return class_description if class_description && class_description.klass.to_s == klass.to_s
      end
    end

    # get all versions of class description
    def get_class_descriptions(klass)
      available_versions.map do |version|
        get_class_description(klass, version)
      end.compact
    end

    # get all versions of method description
    def get_method_descriptions(klass, method)
      get_class_descriptions(klass).map do |class_description|
        class_description.method_description(method.to_sym)
      end.compact
    end

    def remove_method_description(klass, versions, method_name)
      versions.each do |version|
        klass = get_class_name(klass)
        if (class_description = class_description("#{version}##{klass}"))
          class_description.remove_method_description(method_name)
        end
      end
    end

    def docs(version, class_name, method_name, lang, section = nil)
      empty_doc = empty_docs(version, lang)
      return empty_doc unless valid_search_args?(version, class_name, method_name)

      classes =
        if class_name.nil?
          class_descriptions[version].each_with_object({}) do |(name, description), result|
            result[name] = description.docs(section, nil, lang)
            result
          end
        else
          [@class_descriptions[version][class_name].docs(section, method_name, lang)]
        end
      empty_doc[:docs][:classes] = classes
      empty_doc
    end

    def dsl_classes_paths
      ApipieDSL.configuration.dsl_classes_matchers.map { |m| Dir.glob(m) }
               .concat(Dir.glob(ApipieDSL.configuration.dsl_classes_matcher))
               .flatten.uniq
    end

    def translate(str, locale)
      if ApipieDSL.configuration.translate
        ApipieDSL.configuration.translate.call(str, locale)
      else
        str
      end
    end

    def ignored?(klass, method = nil)
      ignored = ApipieDSL.configuration.ignored
      class_name = get_class_name(klass.name)
      return true if ignored.include?(class_name)
      return true if ignored.include?("#{class_name}##{method}")
    end

    def reload_documentation
      # don't load translated strings, we'll translate them later
      old_locale = locale
      reset_locale(ApipieDSL.configuration.default_locale)

      rails_mark_classes_for_reload if ApipieDSL.configuration.rails?

      dsl_classes_paths.each do |file|
        begin
          ApipieDSL.debug("Loading #{file}")
          if ApipieDSL.configuration.rails?
            load_class_from_file(file)
          else
            load(file)
          end
        rescue StandardError
          # Some constant the file uses may not be defined
          # before it's loaded
          # TODO: better loading
        end
      end

      reset_locale(old_locale)
    end

    def load_documentation
      return if @documentation_loaded && !ApipieDSL.configuration.reload_dsl?

      ApipieDSL.reload_documentation
      @documentation_loaded = true
    end

    def locale
      ApipieDSL.configuration.locale.call(nil) if ApipieDSL.configuration.locale
    end

    def reset_locale(loc)
      ApipieDSL.configuration.locale.call(loc) if ApipieDSL.configuration.locale
    end

    private

    def initialize_environment
      @class_descriptions ||= Hash.new { |h, version| h[version] = {} }
      @class_versions ||= Hash.new { |h, klass| h[klass.to_s] = [] }
      @refs ||= Hash.new { |h, version| h[version] = {} }
      @param_groups ||= {}
      @prop_groups ||= {}
    end

    def empty_docs(version, lang)
      url_args = ApipieDSL.configuration.version_in_url ? version : ''
      {
        docs: {
          name: ApipieDSL.configuration.app_name,
          info: ApipieDSL.app_info(version, lang),
          copyright: ApipieDSL.configuration.copyright,
          doc_url: ApipieDSL.full_url(url_args),
          classes: {}
        }
      }
    end

    def load_class_from_file(class_file)
      require_dependency class_file
    end

    # Since Rails 3.2, the classes are reloaded only on file change.
    # We need to reload all the classes to rebuild the
    # docs, therefore we just force to reload all the code.
    def rails_mark_classes_for_reload
      unless Rails.application.config.cache_classes
        Rails::VERSION::MAJOR == 4 ? ActionDispatch::Reloader.cleanup! : Rails.application.reloader.reload!
        initialize_environment
        Rails::VERSION::MAJOR == 4 ? ActionDispatch::Reloader.prepare! : Rails.application.reloader.prepare!
      end
    end

    def valid_search_args?(version, class_name, method_name)
      return false unless class_descriptions.key?(version)

      if class_name
        return false unless class_descriptions[version].key?(class_name)

        if method_name
          class_description = class_descriptions[version][class_name]
          return false unless class_description.valid_method_name?(method_name)
        end
      end
      true
    end
  end
end
