class ClosetList < ApplicationRecord
  belongs_to :user
  has_many :hangers, class_name: 'ClosetHanger', foreign_key: 'list_id', dependent: :destroy

  validates :name, :presence => true, :uniqueness => {:scope => :user_id}
  validates :user, :presence => true
  validates :hangers_owned, :inclusion => {:in => [true, false], :message => "can't be blank"}

  delegate :log_trade_activity, to: :user

  scope :alphabetical, -> { order(:name) }
  scope :publicly_visible, -> {
    where(arel_table[:visibility].gteq(ClosetVisibility[:public].id))
  }
  scope :trading, -> {
    where(arel_table[:visibility].gteq(ClosetVisibility[:trading].id))
  }
  scope :visible_to, ->(user) {
    condition = arel_table[:visibility].gteq(ClosetVisibility[:public].id)
    condition = condition.or(arel_table[:user_id].eq(user.id)) if user
    where(condition)
  }

  after_save :sync_hangers_owned!
  after_save :log_trade_activity, if: :trading?
  after_destroy :log_trade_activity, if: :trading?

  def trading?
    visibility >= ClosetVisibility[:trading].id
  end

  def sync_hangers_owned!
    if hangers_owned_changed?
      hangers.each do |hanger|
        hanger.owned = hangers_owned
        hanger.save!
      end
    end
  end

  def try_non_null(method_name)
    send(method_name)
  end

  def self.preload_items(
    lists,
    hangers_scope: ClosetHanger.all,
    items_scope: Item.all
  )
    # Preload the records we need. (This is like `includes`, but `includes`
    # always selects all fields for all records, and we give the caller the
    # opportunity to specify which fields it actually wants via scope!)
    hangers = hangers_scope.where(list_id: lists.map(&:id))

    # Group the records by relevant IDs.
    hangers_by_list_id = hangers.group_by(&:list_id)

    # Assign the preloaded records to the records they belong to. (This is like
    # doing e.g. h.item = ..., but that's a database write - we actually just
    # want to set the `item` field itself directly! Hacky, ripped from how
    # `ActiveRecord::Associations::Preloader` does it!)
    lists.each do |list|
      list.association(:hangers).target = hangers_by_list_id[list.id]
    end

    # Then, do similar preloading for the hangers and their items.
    ClosetHanger.preload_items(
      hangers,
      items_scope: items_scope,
    )
  end

  module VisibilityMethods
    delegate :trading?, to: :visibility_level

    def visibility_level
      ClosetVisibility.levels[visibility]
    end

    def trading_changed?
      return false unless visibility_changed?
      level_change = visibility_change.map { |v| ClosetVisibility.levels[v] }
      old_trading, new_trading = level_change.map(&:trading?)
      old_trading != new_trading
    end
  end

  def self.group_by_owned
    {true => [], false => []}.tap do |h|
      all.each { |list| h[list.hangers_owned?] << list}
    end
  end

  include VisibilityMethods

  class Null
    include VisibilityMethods
    attr_reader :user

    def initialize(user)
      @user = user
    end

    def hangers
      user.closet_hangers.unlisted.where(owned: hangers_owned)
    end

    def hangers_owned?
      hangers_owned
    end

    def try_non_null(method_name)
      nil
    end
  end

  class NullOwned < Null
    def hangers_owned
      true
    end

    def visibility
      user.owned_closet_hangers_visibility
    end

    def visibility_changed?
      user.owned_closet_hangers_visibility_changed?
    end

    def visibility_change
      user.owned_closet_hangers_visibility_change
    end
  end

  class NullWanted < Null
    def hangers_owned
      false
    end

    def visibility
      user.wanted_closet_hangers_visibility
    end

    def visibility_changed?
      user.wanted_closet_hangers_visibility_changed?
    end

    def visibility_change
      user.wanted_closet_hangers_visibility_change
    end
  end
end