class Item < ApplicationRecord
  include PrettyParam
  
  # We use the `type` column to mean something other than what Rails means!
  self.inheritance_column = nil

  SwfAssetType = 'object'

  has_many :closet_hangers
  has_one :contribution, :as => :contributed, :inverse_of => :contributed
  has_many :parent_swf_asset_relationships, :as => :parent
  has_many :swf_assets, :through => :parent_swf_asset_relationships

  attr_writer :current_body_id, :owned, :wanted

  NCRarities = [0, 500]
  PAINTBRUSH_SET_DESCRIPTION = 'This item is part of a deluxe paint brush set!'
  SPECIAL_COLOR_DESCRIPTION_REGEX =
    /This item is only wearable by [a-zA-Z]+ painted ([a-zA-Z]+)\.|WARNING: This [a-zA-Z]+ can be worn by ([a-zA-Z]+) [a-zA-Z]+ ONLY!|If your Neopet is not painted ([a-zA-Z]+), it will not be able to wear this item\./

  cattr_reader :per_page
  @@per_page = 30

  scope :newest, -> {
    order(arel_table[:created_at].desc) if arel_table[:created_at]
  }

  scope :sitemap, -> { order([:id]).limit(49999) }

  scope :with_closet_hangers, -> { joins(:closet_hangers) }

  scope :name_includes, ->(value, locale = I18n.locale) {
    Item.where("name LIKE ?", "%" + sanitize_sql_like(value) + "%")
  }
  scope :name_excludes, ->(value, locale = I18n.locale) {
    Item.where("name NOT LIKE ?", "%" + sanitize_sql_like(value) + "%")
  }
  scope :is_nc, -> {
    i = Item.arel_table
    where(i[:rarity_index].in(Item::NCRarities).or(i[:is_manually_nc].eq(true)))
  }
  scope :is_np, -> {
    i = Item.arel_table
    where(i[:rarity_index].in(Item::NCRarities).or(i[:is_manually_nc].eq(true)).not)
  }
  scope :is_pb, -> {
    where('description LIKE ?',
      '%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
  }
  scope :is_not_pb, -> {
    where('description NOT LIKE ?',
      '%' + sanitize_sql_like(PAINTBRUSH_SET_DESCRIPTION) + '%')
  }
  scope :occupies, ->(zone_label) {
    zone_ids = Zone.matching_label(zone_label).map(&:id)
    sa = SwfAsset.arel_table
    joins(:swf_assets).where(sa[:zone_id].in(zone_ids)).distinct
  }
  scope :not_occupies, ->(zone_label) {
    zone_ids = Zone.matching_label(zone_label).map(&:id)
    i = Item.arel_table
    sa = SwfAsset.arel_table
    # Querying for "has NO swf_assets matching these zone IDs" is trickier than
    # the positive case! To do it, we GROUP_CONCAT the zone_ids together for
    # each item, then use FIND_IN_SET to search the result for each zone ID,
    # and assert that it must not find a match. (This is uhh, not exactly fast,
    # so it helps to have other tighter conditions applied first!)
    # TODO: I feel like this could also be solved with a LEFT JOIN, idk if that
    # performs any better? In Rails 5+ `left_outer_joins` is built in so!
    condition = zone_ids.map { 'FIND_IN_SET(?, GROUP_CONCAT(zone_id)) = 0' }.join(' AND ')
    joins(:swf_assets).group(i[:id]).having(condition, *zone_ids).distinct
  }
  scope :restricts, ->(zone_label) {
    zone_ids = Zone.matching_label(zone_label).map(&:id)
    condition = zone_ids.map { '(SUBSTR(items.zones_restrict, ?, 1) = "1")' }.join(' OR ')
    where(condition, *zone_ids)
  }
  scope :not_restricts, ->(zone_label) {
    zone_ids = Zone.matching_label(zone_label).map(&:id)
    condition = zone_ids.map { '(SUBSTR(items.zones_restrict, ?, 1) = "1")' }.join(' OR ')
    where("NOT (#{condition})", *zone_ids)
  }
  scope :fits, ->(body_id) {
    sa = SwfAsset.arel_table
    joins(:swf_assets).where(sa[:body_id].eq(body_id)).distinct
  }
  scope :not_fits, ->(body_id) {
    i = Item.arel_table
    sa = SwfAsset.arel_table
    # Querying for "has NO swf_assets matching these body IDs" is trickier than
    # the positive case! To do it, we GROUP_CONCAT the body_ids together for
    # each item, then use FIND_IN_SET to search the result for the body ID,
    # and assert that it must not find a match. (This is uhh, not exactly fast,
    # so it helps to have other tighter conditions applied first!)
    #
    # TODO: I feel like this could also be solved with a LEFT JOIN, idk if that
    # performs any better? In Rails 5+ `left_outer_joins` is built in so!
    #
    # NOTE: The `fits` and `not_fits` counts don't perfectly add up to the
    # total number of items, 5 items aren't accounted for? I'm not going to
    # bother looking into this, but one thing I notice is items with no assets
    # somehow would not match either scope in this impl (but LEFT JOIN would!)
    joins(:swf_assets).group(i[:id]).
      having('FIND_IN_SET(?, GROUP_CONCAT(body_id)) = 0', body_id).
      distinct
  }

  def nc_trade_value
    return nil unless nc?
    begin
      OwlsValueGuide.find_by_name(name)
    rescue OwlsValueGuide::NotFound => error
      Rails.logger.debug("No NC trade value listed for #{name} (#{id})")
      return nil
    rescue OwlsValueGuide::NetworkError => error
      Rails.logger.error("Couldn't load nc_trade_value: #{error.full_message}")
      return nil
    end
  end
  
  # Return an OrderedHash mapping users to the number of times they
  # contributed to this item's assets, from most contributions to least.
  def contributors_with_counts
    # Get contributing users' IDs
    swf_asset_ids = swf_assets.select(SwfAsset.arel_table[:id]).map(&:id)
    swf_asset_contributions = Contribution.select('user_id').
      where(:contributed_type => 'SwfAsset', :contributed_id => swf_asset_ids)
    contributor_ids = swf_asset_contributions.map(&:user_id)
    
    # Get the users, mapped by ID
    contributors_by_id = {}
    User.find(contributor_ids).each { |u| contributors_by_id[u.id] = u }
    
    # Count each user's contributions
    contributor_counts_by_id = Hash.new(0)
    contributor_ids.each { |id| contributor_counts_by_id[id] += 1 }
    
    # Build an OrderedHash mapping users to counts in descending order
    contributors_with_counts = ActiveSupport::OrderedHash.new
    contributor_counts_by_id.sort_by { |k, v| v }.reverse.each do |id, count|
      contributor = contributors_by_id[id]
      contributors_with_counts[contributor] = count
    end
    contributors_with_counts
  end

  def nc?
    is_manually_nc? || NCRarities.include?(rarity_index)
  end
  
  def pb?
    I18n.with_locale(:en) { self.description == PAINTBRUSH_SET_DESCRIPTION }
  end

  def owned?
    @owned
  end

  def wanted?
    @wanted
  end

  def restricted_zones(options={})
    options[:scope] ||= Zone.all
    options[:scope].find(restricted_zone_ids)
  end
  
  def restricted_zone_ids
    unless @restricted_zone_ids
      @restricted_zone_ids = []
      zones_restrict.split(//).each_with_index do |switch, id|
        @restricted_zone_ids << (id.to_i + 1) if switch == '1'
      end
    end
    @restricted_zone_ids
  end
  
  def occupied_zone_ids
    occupied_zones.map(&:id)
  end

  def occupied_zones(options={})
    options[:scope] ||= Zone.all
    all_body_ids = []
    zone_body_ids = {}
    selected_assets = swf_assets.select('body_id, zone_id').each do |swf_asset|
      zone_body_ids[swf_asset.zone_id] ||= []
      body_ids = zone_body_ids[swf_asset.zone_id]
      body_ids << swf_asset.body_id unless body_ids.include?(swf_asset.body_id)
      all_body_ids << swf_asset.body_id unless all_body_ids.include?(swf_asset.body_id)
    end
    zones = options[:scope].find(zone_body_ids.keys)
    zones_by_id = zones.inject({}) { |h, z| h[z.id] = z; h }
    total_body_ids = all_body_ids.size
    zone_body_ids.each do |zone_id, body_ids|
      zones_by_id[zone_id].sometimes = true if body_ids.size < total_body_ids
    end
    zones
  end

  def affected_zones
    restricted_zones + occupied_zones
  end

  def special_color
    @special_color ||= determine_special_color
  end

  def special_color_id
    special_color.try(:id)
  end

  protected
  def determine_special_color
    I18n.with_locale(I18n.default_locale) do
      # Rather than go find the special description in all locales, let's just
      # run this logic in English.
      if description.include?(PAINTBRUSH_SET_DESCRIPTION)
        name_words = name.downcase.split
        Color.nonstandard.each do |color|
          return color if name_words.include?(color.name)
        end
      end

      match = description.match(SPECIAL_COLOR_DESCRIPTION_REGEX)
      if match
        # Since there are multiple formats in the one regex, there are multiple
        # possible color name captures. So, take the first non-nil capture.
        color = match.captures.detect(&:present?)
        return Color.find_by_name(color.downcase)
      end

      # HACK: this should probably be a flag on the record instead of
      #     being hardcoded :P
      if [71893, 76192, 76202, 77367, 77368, 77369, 77370].include?(id)
        return Color.find_by_name('baby')
      end

      if [76198].include?(id)
        return Color.find_by_name('mutant')
      end

      if [75372].include?(id)
        return Color.find_by_name('maraquan')
      end

      if manual_special_color_id?
        return Color.find(manual_special_color_id)
      end
    end
  end
  public

  def species_support_ids
    @species_support_ids_array ||= read_attribute('species_support_ids').split(',').map(&:to_i) rescue nil
  end

  def species_support_ids=(replacement)
    @species_support_ids_array = nil
    replacement = replacement.join(',') if replacement.is_a?(Array)
    write_attribute('species_support_ids', replacement)
  end
  
  def support_species?(species)
    species_support_ids.blank? || species_support_ids.include?(species.id)
  end

  def modeled_body_ids
    @modeled_body_ids ||= swf_assets.select('DISTINCT body_id').map(&:body_id)
  end

  def modeled_color_ids
    # Might be empty if modeled_body_ids is 0. But it's currently not called
    # in that scenario, so, whatever.
    @modeled_color_ids ||= PetType.select('DISTINCT color_id').
                                   where(body_id: modeled_body_ids).
                                   map(&:color_id)
  end

  def basic_body_ids
    @basic_body_ids ||= begin
      basic_color_ids ||= Color.select([:id]).basic.map(&:id)
      PetType.select('DISTINCT body_id').
        where(color_id: basic_color_ids).map(&:body_id)
    end
  end

  def predicted_body_ids
    @predicted_body_ids ||= if modeled_body_ids.include?(0)
      # Oh, look, it's already known to fit everybody! Sweet. We're done. (This
      # isn't folded into the case below, in case this item somehow got a
      # body-specific and non-body-specific asset. In all the cases I've seen
      # it, that indicates a glitched item, but this method chooses to reflect
      # behavior elsewhere in the app by saying that we can put this item on
      # anybody. (Heh. Any body.))
      modeled_body_ids
    elsif modeled_body_ids.size == 1
      # This might just be a species-specific item. Let's be conservative in
      # our prediction, though we'll revise it if we see another body ID.
      modeled_body_ids
    else
      # If an item is worn by more than one body, then it must be wearable by
      # all bodies of the same color. (To my knowledge, anyway. I'm not aware
      # of any exceptions.) So, let's find those bodies by first finding those
      # colors.
      basic_modeled_body_ids, nonbasic_modeled_body_ids = modeled_body_ids.
        partition { |bi| basic_body_ids.include?(bi) }

      output = []
      if basic_modeled_body_ids.present?
        output += basic_body_ids
      end
      if nonbasic_modeled_body_ids.present?
        nonbasic_modeled_color_ids = PetType.select('DISTINCT color_id').
          where(body_id: nonbasic_modeled_body_ids).
          map(&:color_id)
        output += PetType.select('DISTINCT body_id').
          where(color_id: nonbasic_modeled_color_ids).
          map(&:body_id)
      end
      output
    end
  end

  def predicted_missing_body_ids
    @predicted_missing_body_ids ||= predicted_body_ids - modeled_body_ids
  end

  def predicted_missing_standard_body_ids_by_species_id
    @predicted_missing_standard_body_ids_by_species_id ||=
      PetType.select('DISTINCT body_id, species_id').
              joins(:color).
              where(body_id: predicted_missing_body_ids,
                    colors: {standard: true}).
              inject({}) { |h, pt| h[pt.species_id] = pt.body_id; h }
  end

  def predicted_missing_standard_body_ids_by_species
    species = Species.where(id: predicted_missing_standard_body_ids_by_species_id.keys)
    species_by_id = species.inject({}) { |h, s| h[s.id] = s; h }
    predicted_missing_standard_body_ids_by_species_id.inject({}) { |h, (sid, bid)|
      h[species_by_id[sid]] = bid; h }
  end

  def predicted_missing_nonstandard_body_pet_types
    PetType.joins(:color).
            where(body_id: predicted_missing_body_ids - basic_body_ids,
                  colors: {standard: false})
  end

  def predicted_missing_nonstandard_body_ids_by_species_by_color
    pet_types = predicted_missing_nonstandard_body_pet_types

    species_by_id = {}
    Species.find(pet_types.map(&:species_id)).each do |species|
      species_by_id[species.id] = species
    end

    colors_by_id = {}
    Color.find(pet_types.map(&:color_id)).each do |color|
      colors_by_id[color.id] = color
    end

    body_ids_by_species_by_color = {}
    pet_types.each do |pt|
      color = colors_by_id[pt.color_id]
      body_ids_by_species_by_color[color] ||= {}
      body_ids_by_species_by_color[color][species_by_id[pt.species_id]] = pt.body_id
    end
    body_ids_by_species_by_color
  end

  def predicted_fully_modeled?
    predicted_missing_body_ids.empty?
  end

  def predicted_modeled_ratio
    modeled_body_ids.size.to_f / predicted_body_ids.size
  end

  def as_json(options={})
    super({
      only: [:id, :name, :description, :thumbnail_url, :rarity_index],
      methods: [:zones_restrict],
    }.merge(options))
  end

  def handle_assets!
    if @parent_swf_asset_relationships_to_update && @current_body_id
      new_swf_asset_ids = @parent_swf_asset_relationships_to_update.map(&:swf_asset_id)
      rels = ParentSwfAssetRelationship.arel_table
      swf_assets = SwfAsset.arel_table
      
      # If a relationship used to bind an item and asset for this body type,
      # but doesn't in this sample, the two have been unbound. Delete the
      # relationship.
      ids_to_delete = self.parent_swf_asset_relationships.
        select(rels[:id]).
        joins(:swf_asset).
        where(rels[:swf_asset_id].not_in(new_swf_asset_ids)).
        where(swf_assets[:body_id].in([@current_body_id, 0])).
        map(&:id)
      
      unless ids_to_delete.empty?
        ParentSwfAssetRelationship.where(:id => ids_to_delete).delete_all
      end
      
      @parent_swf_asset_relationships_to_update.each do |rel|
        rel.save!
        rel.swf_asset.save!
      end
    end
  end
  
  def body_specific?
    # If there are species support IDs (it's not empty), the item is
    # body-specific. If it's empty, it fits everyone the same.
    explicitly_body_specific? || !species_support_ids.empty?
  end

  def add_origin_registry_info(info, locale)
    # bear in mind that numbers from registries are floats
    species_support_strs = info['species_support'] || []
    self.species_support_ids = species_support_strs.map(&:to_i)

    self.name = info['name']
    self.description = info['description']
    self.thumbnail_url = info['thumbnail_url']
    self.category = info['category']
    self.type = info['type']
    self.rarity = info['rarity']
    self.rarity_index = info['rarity_index'].to_i
    self.price = info['price'].to_i
    self.weight_lbs = info['weight_lbs'].to_i
    self.zones_restrict = info['zones_restrict']
  end

  def pending_swf_assets
    @parent_swf_asset_relationships_to_update.inject([]) do |all_swf_assets, relationship|
      all_swf_assets << relationship.swf_asset
    end
  end

  def parent_swf_asset_relationships_to_update=(rels)
    @parent_swf_asset_relationships_to_update = rels
  end

  Body = Struct.new(:id, :species)
  Appearance = Struct.new(:body, :swf_assets)
  def appearances
    all_swf_assets = swf_assets.to_a

    # If there are no assets yet, there are no appearances.
    return [] if all_swf_assets.empty?

    # Get all SWF assets, and separate the ones that fit everyone (body_id=0).
    swf_assets_by_body_id = all_swf_assets.group_by(&:body_id)
    swf_assets_for_all_bodies = swf_assets_by_body_id.delete(0) || []

    # If there are no body-specific assets, return one appearance for them all.
    if swf_assets_by_body_id.empty?
      body = Body.new(0, nil)
      return [Appearance.new(body, swf_assets_for_all_bodies)]
    end

    # Otherwise, create an appearance for each real (nonzero) body ID. We don't
    # generally expect body_id = 0 and body_id != 0 to mix, but if they do,
    # uhh, let's merge the body_id = 0 ones in?
    swf_assets_by_body_id.map do |body_id, body_specific_assets|
      swf_assets_for_body = body_specific_assets + swf_assets_for_all_bodies
      species = Species.with_body_id(body_id).first!
      body = Body.new(body_id, species)
      Appearance.new(body, swf_assets_for_body)
    end
  end

  def self.all_by_ids_or_children(ids, swf_assets)
    swf_asset_ids = []
    swf_assets_by_id = {}
    swf_assets_by_parent_id = {}
    swf_assets.each do |swf_asset|
      id = swf_asset.id
      swf_assets_by_id[id] = swf_asset
      swf_asset_ids << id
    end
    SwfAsset.object_assets.joins(:parent_swf_asset_relationships).
      where(SwfAsset.arel_table[:id].in(swf_asset_ids)).select([
        SwfAsset.arel_table[:id],
        ParentSwfAssetRelationship.arel_table[:parent_id]
      ]).each do |row|
        item_id = row.parent_id.to_i
        swf_assets_by_parent_id[item_id] ||= []
        swf_assets_by_parent_id[item_id] << swf_assets_by_id[row.id.to_i]
        ids << item_id
      end
    find(ids).tap do |items|
      items.each do |item|
        swf_assets = swf_assets_by_parent_id[item.id]
        if swf_assets
          swf_assets.each do |swf_asset|
            swf_asset.item = item
          end
        end
      end
    end
  end

  def self.collection_from_pet_type_and_registries(pet_type, info_registry, asset_registry, scope=Item.all)
    # bear in mind that registries are arrays with many nil elements,
    # due to how the parser works

    # Collect existing items
    items = {}
    item_ids = []
    info_registry.each do |item_id, info|
      if info && info[:is_compatible]
        item_ids << item_id.to_i
      end
    end

    # Collect existing relationships
    existing_relationships_by_item_id_and_swf_asset_id = {}
    existing_items = scope.where(id: item_ids).
      includes(:parent_swf_asset_relationships)
    existing_items.each do |item|
      items[item.id] = item
      relationships_by_swf_asset_id = {}
      item.parent_swf_asset_relationships.each do |relationship|
        relationships_by_swf_asset_id[relationship.swf_asset_id] = relationship
      end
      existing_relationships_by_item_id_and_swf_asset_id[item.id] =
        relationships_by_swf_asset_id
    end

    # Collect existing assets
    swf_asset_ids = []
    asset_registry.each do |asset_id, asset_data|
      swf_asset_ids << asset_id.to_i if asset_data
    end
    existing_swf_assets = SwfAsset.object_assets.includes(:zone).
      where(remote_id: swf_asset_ids)
    existing_swf_assets_by_remote_id = {}
    existing_swf_assets.each do |swf_asset|
      existing_swf_assets_by_remote_id[swf_asset.remote_id] = swf_asset
    end

    # With each asset in the registry,
    relationships_by_item_id = {}
    asset_registry.each do |asset_id, asset_data|
      if asset_data
        # Build and update the item
        item_id = asset_data[:obj_info_id].to_i
        next unless item_ids.include?(item_id) # skip incompatible (Uni Bug)
        item = items[item_id]
        unless item
          item = Item.new
          item.id = item_id
          items[item_id] = item
        end
        item.add_origin_registry_info info_registry[item.id.to_s], I18n.default_locale
        item.current_body_id = pet_type.body_id

        # Build and update the SWF
        swf_asset_remote_id = asset_data[:asset_id].to_i
        swf_asset = existing_swf_assets_by_remote_id[swf_asset_remote_id]
        unless swf_asset
          swf_asset = SwfAsset.new
          swf_asset.remote_id = swf_asset_remote_id
        end
        swf_asset.origin_object_data = asset_data
        swf_asset.origin_pet_type = pet_type
        swf_asset.item = item

        # Build and update the relationship
        relationship = existing_relationships_by_item_id_and_swf_asset_id[item.id][swf_asset.id] rescue nil
        unless relationship
          relationship = ParentSwfAssetRelationship.new
          relationship.parent = item
        end
        relationship.swf_asset = swf_asset
        relationships_by_item_id[item_id] ||= []
        relationships_by_item_id[item_id] << relationship
      end
    end

    # Set up the relationships to be updated on item save
    relationships_by_item_id.each do |item_id, relationships|
      items[item_id].parent_swf_asset_relationships_to_update = relationships
    end

    items.values
  end
end