class PetState < ApplicationRecord
  SwfAssetType = 'biology'

  MAIN_POSES = %w(HAPPY_FEM HAPPY_MASC SAD_FEM SAD_MASC SICK_FEM SICK_MASC)
  
  has_many :contributions, :as => :contributed,
    :inverse_of => :contributed # in case of duplicates being merged
  has_many :outfits
  has_many :parent_swf_asset_relationships, :as => :parent
  has_many :swf_assets, :through => :parent_swf_asset_relationships

  serialize :swf_asset_ids, coder: Serializers::IntegerSet, type: Array

  belongs_to :pet_type

  delegate :species_id, :species, :color_id, :color, to: :pet_type

  alias_method :swf_asset_ids_from_association, :swf_asset_ids

  scope :glitched, -> { where(glitched: true) }
  scope :needs_labeling, -> { unlabeled.where(glitched: false) }
  scope :unlabeled, -> { with_pose("UNKNOWN") }
  scope :usable, -> { where(labeled: true, glitched: false) }

  scope :newest, -> { order(created_at: :desc) }
  scope :newest_pet_type, -> { joins(:pet_type).merge(PetType.newest) }
  scope :created_before, ->(time) { where(arel_table[:created_at].lt(time)) }

  # A simple ordering that tries to bring reliable pet states to the front.
  scope :emotion_order, -> {
    order(Arel.sql(
      "(mood_id IS NULL) ASC, mood_id ASC, female DESC, unconverted DESC, " +
      "glitched ASC, id DESC"
    ))
  }

  # Filter pet states using the "pose" concept we use in the editor.
  scope :with_pose, -> pose {
    case pose
    when "UNCONVERTED"
      where(unconverted: true)
    when "HAPPY_MASC"
      where(mood_id: 1, female: false)
    when "HAPPY_FEM"
      where(mood_id: 1, female: true)
    when "SAD_MASC"
      where(mood_id: 2, female: false)
    when "SAD_FEM"
      where(mood_id: 2, female: true)
    when "SICK_MASC"
      where(mood_id: 4, female: false)
    when "SICK_FEM"
      where(mood_id: 4, female: true)
    when "UNKNOWN"
      where(mood_id: nil).or(where(female: nil))
    else
      raise ArgumentError, "unexpected pose value #{pose}"
    end
  }

  def pose
    if unconverted?
      "UNCONVERTED"
    elsif mood_id.nil? || female.nil?
      "UNKNOWN"
    elsif mood_id == 1 && !female?
      "HAPPY_MASC"
    elsif mood_id == 1 && female?
      "HAPPY_FEM"
    elsif mood_id == 2 && !female?
      "SAD_MASC"
    elsif mood_id == 2 && female?
      "SAD_FEM"
    elsif mood_id == 4 && !female?
      "SICK_MASC"
    elsif mood_id == 4 && female?
      "SICK_FEM"
    else
      raise "could not identify pose: moodId=#{mood_id}, female=#{female}, " +
        "unconverted=#{unconverted}"
    end
  end

  # TODO: More and more, wanting to refactor poses…
  def pose=(pose)
    case pose
    when "UNKNOWN"
      label_pose nil, nil, unconverted: nil, labeled: false
    when "HAPPY_MASC"
      label_pose 1, false
    when "HAPPY_FEM"
      label_pose 1, true
    when "SAD_MASC"
      label_pose 2, false
    when "SAD_FEM"
      label_pose 2, true
    when "SICK_MASC"
      label_pose 4, false
    when "SICK_FEM"
      label_pose 4, true
    when "UNCONVERTED"
      label_pose nil, nil, unconverted: true
    end
  end

  def to_param
    "#{id}-#{pose.split('_').map(&:capitalize).join('-')}"
  end

  # Because our column is named `swf_asset_ids`, we need to ensure writes to
  # it go to the attribute, and not the thing ActiveRecord does of finding the
  # relevant `swf_assets`.
  # TODO: Consider renaming the column to `cached_swf_asset_ids`?
  def swf_asset_ids=(new_swf_asset_ids)
    write_attribute(:swf_asset_ids, new_swf_asset_ids)
  end

  private

  # A helper for the `pose=` method.
  def label_pose(mood_id, female, unconverted: false, labeled: true)
    self.labeled = labeled
    self.mood_id = mood_id
    self.female = female
    self.unconverted = unconverted
  end

  def self.last_updated_key
    PetState.maximum(:updated_at)
  end

  def self.all_supported_poses
    Rails.cache.fetch("PetState.all_supported_poses #{last_updated_key}") do
      {}.tap do |h|
        includes(:pet_type).find_each do |pet_state|
          h[pet_state.species_id] ||= {}
          h[pet_state.species_id][pet_state.color_id] ||= []
          h[pet_state.species_id][pet_state.color_id] << pet_state.pose
        end

        h.values.map(&:values).flatten(1).each(&:uniq!).each(&:sort!)
      end
    end
  end

  def self.next_unlabeled_appearance(after_id: nil)
    # Rather than just getting the newest unlabeled pet state, prioritize the
    # newest *pet type*. This better matches the user's perception of what the
    # newest state is, because the Rainbow Pool UI is grouped by pet type!
    pet_states = needs_labeling.newest_pet_type.newest

    # If `after_id` is given, convert it from a PetState ID to creation
    # timestamps, and find the next record prior to those timestamps. This
    # enables skipping past records the user doesn't want to label.
    if after_id
      begin
        after_pet_state = PetState.find(after_id)
        before_pt_created_at = after_pet_state.pet_type.created_at
        before_ps_created_at = after_pet_state.created_at
      rescue ActiveRecord::RecordNotFound
        Rails.logger.warn "PetState.next_unlabeled_appearance: Could not " +
          "find pet state ##{after_id}"
        return nil
      end

      # Because we sort by `newest_pet_type` first, then breaks ties by
      # `newest`, our filter needs to operate the same way. Kudos to:
      # https://brunoscheufler.com/blog/2022-01-01-paginating-large-ordered-datasets-with-cursor-based-pagination
      pet_states.merge!(
        PetType.created_before(before_pt_created_at).or(
          PetType.created_at(before_pt_created_at).and(
            PetState.created_before(before_ps_created_at)
          )
        )
      )
    end

    pet_states.first
  end
end