# encoding: utf-8
module RailsBestPractices
  module Prepares
    # Remember models and model associations.
    class ModelPrepare < Core::Check
      include Core::Check::Classable
      include Core::Check::Accessable

      interesting_nodes :class, :def, :defs, :command, :alias
      interesting_files MODEL_FILES

      ASSOCIATION_METHODS = %w(belongs_to has_one has_many has_and_belongs_to_many embeds_many embeds_one embedded_in many one)

      def initialize
        @models = Prepares.models
        @model_associations = Prepares.model_associations
        @model_attributes = Prepares.model_attributes
        @methods = Prepares.model_methods
      end

      # remember the class name.
      add_callback :start_class do |node|
        if "ActionMailer::Base" != current_extend_class_name
          @models << @klass
        end
      end

      # check def node to remember all methods.
      #
      # the remembered methods (@methods) are like
      #     {
      #       "Post" => {
      #         "save" => {"file" => "app/models/post.rb", "line_number" => 10, "unused" => false, "unused" => false},
      #         "find" => {"file" => "app/models/post.rb", "line_number" => 10, "unused" => false, "unused" => false}
      #       },
      #       "Comment" => {
      #         "create" => {"file" => "app/models/comment.rb", "line_number" => 10, "unused" => false, "unused" => false},
      #       }
      #     }
      add_callback :start_def do |node|
        if @klass &&
            "ActionMailer::Base" != current_extend_class_name &&
            (classable_modules.empty? || klasses.any?)
          method_name = node.method_name.to_s
          @methods.add_method(current_class_name, method_name, {"file" => node.file, "line_number" => node.line_number}, current_access_control)
        end
      end

      # check defs node to remember all static methods.
      #
      # the remembered methods (@methods) are like
      #     {
      #       "Post" => {
      #         "save" => {"file" => "app/models/post.rb", "line_number" => 10, "unused" => false, "unused" => false},
      #         "find" => {"file" => "app/models/post.rb", "line_number" => 10, "unused" => false, "unused" => false}
      #       },
      #       "Comment" => {
      #         "create" => {"file" => "app/models/comment.rb", "line_number" => 10, "unused" => false, "unused" => false},
      #       }
      #     }
      add_callback :start_defs do |node|
        if @klass && "ActionMailer::Base" != current_extend_class_name
          method_name = node.method_name.to_s
          @methods.add_method(current_class_name, method_name, {"file" => node.file, "line_number" => node.line_number}, current_access_control)
        end
      end

      # check command node to remember all assoications or named_scope/scope methods.
      #
      # the remembered association names (@associations) are like
      #     {
      #       "Project" => {
      #         "categories" => {"has_and_belongs_to_many" => "Category"},
      #         "project_manager" => {"has_one" => "ProjectManager"},
      #         "portfolio" => {"belongs_to" => "Portfolio"},
      #         "milestones => {"has_many" => "Milestone"}
      #       }
      #     }
      add_callback :start_command do |node|
        case node.message.to_s
        when *%w(named_scope scope alias_method)
          method_name = node.arguments.all.first.to_s
          @methods.add_method(current_class_name, method_name, {"file" => node.file, "line_number" => node.line_number}, current_access_control)
        when "alias_method_chain"
          method, feature = *node.arguments.all.map(&:to_s)
          @methods.add_method(current_class_name, "#{method}_with_#{feature}", {"file" => node.file, "line_number" => node.line_number}, current_access_control)
          @methods.add_method(current_class_name, "#{method}", {"file" => node.file, "line_number" => node.line_number}, current_access_control)
        when "field"
          arguments = node.arguments.all
          attribute_name = arguments.first.to_s
          attribute_type = arguments.last.hash_value("type").present? ? arguments.last.hash_value("type").to_s : "String"
          @model_attributes.add_attribute(current_class_name, attribute_name, attribute_type)
        when "key"
          attribute_name, attribute_type = node.arguments.all.map(&:to_s)
          @model_attributes.add_attribute(current_class_name, attribute_name, attribute_type)
        when *ASSOCIATION_METHODS
          remember_association(node)
        else
        end
      end

      # check alias node to remembr the alias methods.
      add_callback :start_alias do |node|
        method_name = node.new_method.to_s
        @methods.add_method(current_class_name, method_name, {"file" => node.file, "line_number" => node.line_number}, current_access_control)
      end

      # after prepare process, fix incorrect associations' class_name.
      add_callback :after_check do
        @model_associations.each do |model, model_associations|
          model_associations.each do |association_name, association_meta|
            unless @models.include?(association_meta["class_name"])
              if @models.include?("#{model}::#{association_meta['class_name']}")
                association_meta["class_name"] = "#{model}::#{association_meta['class_name']}"
              elsif @models.include?(model.gsub(/::\w+$/, ""))
                association_meta["class_name"] = model.gsub(/::\w+$/, "")
              end
            end
          end
        end
      end

      private

        # remember associations, with class to association names.
        def remember_association(node)
          association_meta = node.message.to_s
          association_name = node.arguments.all.first.to_s
          arguments_node = node.arguments.all.last
          if arguments_node.hash_value("class_name").present?
            association_class = arguments_node.hash_value("class_name").to_s
          end
          association_class ||= association_name.classify
          @model_associations.add_association(current_class_name, association_name, association_meta, association_class)
        end
    end
  end
end