# Include this mix-in to make a class droppable to Liquid, and adjust
# its behavior in Liquid by implementing its dedicated Drop class
# named with a "Drop" suffix.
module LiquidDroppable
  extend ActiveSupport::Concern

  class Drop < Liquid::Drop
    def initialize(object)
      @object = object
    end

    def to_s
      @object.to_s
    end

    def each
      (public_instance_methods - Drop.public_instance_methods).each { |name|
        yield [name, __send__(name)]
      }
    end

    def as_json
      return {} unless defined?(self.class::METHODS)
      Hash[self.class::METHODS.map { |m| [m, send(m).as_json] }]
    end
  end

  included do
    const_set :Drop,
              if Kernel.const_defined?(drop_name = "#{name}Drop")
                Kernel.const_get(drop_name)
              else
                Kernel.const_set(drop_name, Class.new(Drop))
              end
  end

  def to_liquid
    self.class::Drop.new(self)
  end

  class MatchDataDrop < Drop
    METHODS = %w[pre_match post_match names size]

    METHODS.each { |attr|
      define_method(attr) {
        @object.__send__(attr)
      }
    }

    def to_s
      @object[0]
    end

    def liquid_method_missing(method)
      @object[method]
    rescue IndexError
      nil
    end
  end

  class ::MatchData
    def to_liquid
      MatchDataDrop.new(self)
    end
  end

  require 'uri'

  class URIDrop < Drop
    METHODS = URI::Generic::COMPONENT

    METHODS.each { |attr|
      define_method(attr) {
        @object.__send__(attr)
      }
    }
  end

  class ::URI::Generic
    def to_liquid
      URIDrop.new(self)
    end
  end

  class ActiveRecordCollectionDrop < Drop
    include Enumerable

    def each(&block)
      @object.each(&block)
    end

    # required for variable indexing as array
    def [](i)
      case i
      when Integer
        @object[i]
      when 'size', 'first', 'last'
        __send__(i)
      end
    end

    # required for variable indexing as array
    def fetch(i, &block)
      @object.fetch(i, &block)
    end

    # compatibility with array; also required by the `size` filter
    def size
      @object.count
    end

    # compatibility with array
    def first
      @object.first
    end

    # compatibility with array
    def last
      @object.last
    end

    # This drop currently does not support the `slice` filter.
  end

  class ::ActiveRecord::Associations::CollectionProxy
    def to_liquid
      ActiveRecordCollectionDrop.new(self)
    end
  end
end
