# frozen-string-literal: true

module Sequel
  module Plugins
    # The column_conflicts plugin overrides Model#get_column_value and #set_column_value
    # to automatically handle column names that conflict with Ruby/Sequel method names.
    #
    # By default, Model#get_column_value and #set_column_value just call send, this
    # plugin overrides the methods and gets/sets the value directly in the values
    # hash if the column name conflicts with an existing Sequel::Model instance
    # method name.
    #
    # Checking for column conflicts causes a performance hit, which is why Sequel
    # does not enable such checks by default.
    #
    # When using this plugin, you can manually update the columns used.  This may be useful if
    # the columns conflict with one of your custom methods, instead of a method defined in
    # Sequel::Model:
    #
    #   Album.plugin :column_conflicts
    #   Album.get_column_conflict!(:column)
    #   Album.set_column_conflict!(:other_column)
    #
    # Usage:
    #
    #   # Make all model's handle column conflicts automatically (called before loading subclasses)
    #   Sequel::Model.plugin :column_conflicts
    #
    #   # Make the Album class handle column conflicts automatically
    #   Album.plugin :column_conflicts
    module ColumnConflicts
      # Check for column conflicts on the current model if the model has a dataset.
      def self.configure(model)
        model.instance_eval do
          @get_column_conflicts = {}
          @set_column_conflicts = {}
          check_column_conflicts if @dataset
        end
      end

      module ClassMethods
        Plugins.after_set_dataset(self, :check_column_conflicts)
        Plugins.inherited_instance_variables(self, :@get_column_conflicts=>:dup, :@set_column_conflicts=>:dup)

        # Hash for columns where the getter method already exists. keys are column symbols/strings that
        # conflict with method names and should be looked up directly instead of calling a method,
        # values are the column symbol to lookup in the values hash.
        attr_reader :get_column_conflicts

        # Hash for columns where the setter method already exists. keys are column symbols/strings suffixed
        # with = that conflict with method names and should be set directly in the values hash,
        # values are the column symbol to set in the values hash.
        attr_reader :set_column_conflicts

        # Compare the column names for the model with the methods defined on Sequel::Model, and automatically
        # setup the column conflicts.
        def check_column_conflicts
          mod = Sequel::Model
          columns.find_all{|c| mod.method_defined?(c)}.each{|c| get_column_conflict!(c)}
          columns.find_all{|c| mod.method_defined?("#{c}=")}.each{|c| set_column_conflict!(c)}
        end

        # Freeze column conflict information when freezing model class.
        def freeze
          @get_column_conflicts.freeze
          @set_column_conflicts.freeze

          super
        end

        # Set the given column as one with a getter method conflict.
        def get_column_conflict!(column)
          @get_column_conflicts[column.to_sym] = @get_column_conflicts[column.to_s] = column.to_sym
        end

        # Set the given column as one with a setter method conflict.
        def set_column_conflict!(column)
          @set_column_conflicts[:"#{column}="] = @set_column_conflicts["#{column}="] = column.to_sym
        end
      end

      module InstanceMethods
        # If the given column has a getter method conflict, lookup the value directly in the values hash.
        def get_column_value(c)
          if col = model.get_column_conflicts[c]
            self[col]
          else
            super
          end
        end

        # If the given column has a setter method conflict, set the value directly in the values hash.
        def set_column_value(c, v)
          if col = model.set_column_conflicts[c]
            self[col] = v
          else
            super
          end
        end
      end
    end
  end
end
