# encoding=utf-8
# ^ to put the regex in utf-8 mode

class Item
  module Search
    class Query
      def initialize(filters, user, text=nil)
        @filters = filters
        @user = user
        @text = text
      end
      
      def results
        @filters.map(&:to_query).inject(Item.all, &:merge).order(:name)
      end

      def to_s
        @text || @filters.map(&:to_s).join(' ')
      end
      
      TEXT_FILTER_EXPR = /([+-]?)(?:(\p{Word}+):)?(?:"([^"]+)"|(\S+))/
      def self.from_text(text, user=nil)
        filters = []
        
        text.scan(TEXT_FILTER_EXPR) do |sign, key, quoted_value, unquoted_value|
          key = 'name' if key.blank?
          value = quoted_value || unquoted_value
          is_positive = (sign != '-')
          
          filter = parse_text_filter(key, value, is_positive, user)
          filters << filter if filter.present?
        end
        
        self.new(filters, user, text)
      end

      def self.from_params(params, user=nil)
        filters = []

        params.values.each do |filter_params|
          key = filter_params[:key]
          value = filter_params[:value]
          is_positive = filter_params[:is_positive] != 'false'

          filter = parse_params_filter(key, value, is_positive, user)
          filters << filter if filter.present?
        end

        self.new(filters, user)
      end

      private

      def self.parse_text_filter(key, value, is_positive, user)
        case key
        when 'name'
          is_positive ?
            Filter.name_includes(value) :
            Filter.name_excludes(value)
        when 'occupies'
          is_positive ? Filter.occupies(value) : Filter.not_occupies(value)
        when 'restricts'
          is_positive ? Filter.restricts(value) : Filter.not_restricts(value)
        when 'fits'
          # First, try the `fits:blue-acara` case.
          # NOTE: This will also work for `fits:"usuki girl-usul"`!
          match = value.match(/\A([^-]+)-([^-]+)\z/)
          if match.present?
            color_name, species_name = match.captures
            pet_type = load_pet_type_by_name(color_name, species_name)
            return is_positive ?
              Filter.fits_pet_type(pet_type, color_name:, species_name:) :
              Filter.not_fits_pet_type(pet_type, color_name:, species_name:)
          end

          # Next, try the `fits:alt-style-87305` case.
          match = value.match(/\Aalt-style-([0-9]+)\z/)
          if match.present?
            alt_style_id, = match.captures
            alt_style = load_alt_style_by_id(alt_style_id)
            return is_positive ?
              Filter.fits_alt_style(alt_style) :
              Filter.not_fits_alt_style(alt_style)
          end

          # Next, try the `fits:nostalgic-faerie-draik` case.
          # NOTE: This will also work for `fits:"nostalgic-usuki girl-usul"`!
          match = value.match(/\A([^-]+)-([^-]+)-([^-]+)\z/)
          if match.present?
            series_name, color_name, species_name = match.captures
            alt_style = load_alt_style_by_name(
              series_name, color_name, species_name)
            return is_positive ?
              Filter.fits_alt_style(alt_style) :
              Filter.not_fits_alt_style(alt_style)
          end

          # TODO: We could make `fits:acara` an alias for `species:acara`, or
          # even the primary syntax?

          # If none of these cases work, raise an error.
          raise_search_error "not_found.fits_target", value: value
        when 'species'
          begin
            species = Species.find_by_name!(value)
            color = Color.find_by_name!('blue')
            pet_type = PetType.where(color_id: color.id, species_id: species.id).first!
          rescue ActiveRecord::RecordNotFound
            raise_search_error "not_found.species",
              species_name: value.capitalize
          end
          is_positive ?
            Filter.fits_species(pet_type.body_id, value) :
            Filter.not_fits_species(pet_type.body_id, value)
        when 'user'
          if user.nil?
            raise_search_error "not_logged_in"
          end
          case value
          when 'owns'
            is_positive ? Filter.owned_by(user) : Filter.not_owned_by(user)
          when 'wants'
            is_positive ? Filter.wanted_by(user) : Filter.not_wanted_by(user)
          else
            raise_search_error "not_found.ownership", keyword: value
          end
        when 'is'
          case value
          when 'nc'
            is_positive ? Filter.is_nc : Filter.is_not_nc
          when 'np'
            is_positive ? Filter.is_np : Filter.is_not_np
          when 'pb'
            is_positive ? Filter.is_pb : Filter.is_not_pb
          when 'modeled'
            is_positive ? Filter.is_modeled : Filter.is_not_modeled
          else
            raise_search_error "not_found.label", label: "is:#{value}"
          end
        else
          raise_search_error "not_found.label", label: key
        end
      end

      def self.parse_params_filter(key, value, is_positive, user)
        case key
        when 'name'
          is_positive ?
            Filter.name_includes(value) :
            Filter.name_excludes(value)
        when 'is_nc'
          is_positive ? Filter.is_nc : Filter.is_not_nc
        when 'is_pb'
          is_positive ? Filter.is_pb : Filter.is_not_pb
        when 'is_np'
          is_positive ? Filter.is_np : Filter.is_not_np
        when 'occupied_zone_set_name'
          is_positive ? Filter.occupies(value) : Filter.not_occupies(value)
        when 'restricted_zone_set_name'
          is_positive ? Filter.restricts(value) : Filter.not_restricts(value)
        when 'fits'
          if value[:alt_style_id].present?
            alt_style = load_alt_style_by_id(value[:alt_style_id])
            is_positive ?
              Filter.fits_alt_style(alt_style) :
              Filter.fits_alt_style(alt_style)
          else
            pet_type = load_pet_type_by_color_and_species(
              value[:color_id], value[:species_id])
            is_positive ?
              Filter.fits_pet_type(pet_type) :
              Filter.not_fits_pet_type(pet_type)
          end
        when 'user_closet_hanger_ownership'
          case value
          when 'true'
            is_positive ?
              Filter.owned_by(user) :
              Filter.not_owned_by(user)
          when 'false'
            is_positive ?
              Filter.wanted_by(user) :
              Filter.not_wanted_by(user)
          end
        else
          Rails.logger.warn "Ignoring unexpected search filter key: #{key}"
        end
      end

      def self.load_pet_type_by_name(color_name, species_name)
        begin
          PetType.matching_name(color_name, species_name).first!
        rescue ActiveRecord::RecordNotFound
          raise_search_error "not_found.pet_type",
            name1: color_name.capitalize, name2: species_name.capitalize
        end
      end

      def self.load_pet_type_by_color_and_species(color_id, species_id)
        begin
          PetType.where(color_id: color_id, species_id: species_id).first!
        rescue ActiveRecord::RecordNotFound
          color_name = Color.find(color_id).name rescue "Color #{color_id}"
          species_name = Species.find(species_id).name rescue "Species #{species_id}"
          raise_search_error "not_found.pet_type",
            name1: color_name.capitalize, name2: species_name.capitalize
        end
      end

      def self.load_alt_style_by_name(series_name, color_name, species_name)
        begin
          AltStyle.matching_name(series_name, color_name, species_name).first!
        rescue ActiveRecord::RecordNotFound
          raise_search_error "not_found.alt_style",
            filter_text: "#{series_name}-#{color_name}-#{species_name}"
        end
      end

      def self.load_alt_style_by_id(alt_style_id)
        begin
          AltStyle.find(alt_style_id)
        rescue ActiveRecord::RecordNotFound
          raise_search_error "not_found.alt_style",
            filter_text: "alt-style-#{alt_style_id}"
        end
      end

      def self.raise_search_error(kind, ...)
        raise Item::Search::Error,
          I18n.translate("items.search.errors.#{kind}", ...)
      end
    end

    class Error < Exception
    end

    private

    # A Filter is basically a wrapper for an Item scope, with extra info about
    # how to convert it into a search query string.
    class Filter
      def initialize(query, text)
        @query = query
        @text = text
      end

      def to_query
        @query
      end

      def to_s
        @text
      end

      def inspect
        "#<#{self.class.name} #{@text.inspect}>"
      end

      def self.name_includes(value)
        self.new Item.name_includes(value), "#{q value}"
      end

      def self.name_excludes(value)
        self.new Item.name_excludes(value), "-#{q value}"
      end

      def self.occupies(value)
        self.new Item.occupies(value), "occupies:#{q value}"
      end

      def self.not_occupies(value)
        self.new Item.not_occupies(value), "-occupies:#{q value}"
      end

      def self.restricts(value)
        self.new Item.restricts(value), "restricts:#{q value}"
      end

      def self.not_restricts(value)
        self.new Item.not_restricts(value), "-restricts:#{q value}"
      end

      def self.fits_pet_type(pet_type, color_name: nil, species_name: nil)
        value = pet_type_to_filter_text(pet_type, color_name:, species_name:)
        self.new Item.fits(pet_type.body_id), "fits:#{q value}"
      end

      def self.not_fits_pet_type(pet_type, color_name: nil, species_name: nil)
        value = pet_type_to_filter_text(pet_type, color_name:, species_name:)
        self.new Item.not_fits(pet_type.body_id), "-fits:#{q value}"
      end

      def self.fits_alt_style(alt_style)
        value = alt_style_to_filter_text(alt_style)
        self.new Item.fits(alt_style.body_id), "fits:#{q value}"
      end

      def self.not_fits_alt_style(alt_style)
        value = alt_style_to_filter_text(alt_style)
        self.new Item.not_fits(alt_style.body_id), "-fits:#{q value}"
      end

      def self.fits_species(body_id, species_name)
        self.new Item.fits(body_id), "species:#{q species_name}"
      end
      
      def self.not_fits_species(body_id, species_name)
        self.new Item.not_fits(body_id), "-species:#{q species_name}"
      end

      def self.owned_by(user)
        self.new user.owned_items, 'user:owns'
      end

      def self.not_owned_by(user)
        self.new user.unowned_items, 'user:owns'
      end

      def self.wanted_by(user)
        self.new user.wanted_items, 'user:wants'
      end

      def self.not_wanted_by(user)
        self.new user.unwanted_items, 'user:wants'
      end

      def self.is_nc
        self.new Item.is_nc, 'is:nc'
      end

      def self.is_not_nc
        self.new Item.is_not_nc, '-is:nc'
      end

      def self.is_np
        self.new Item.is_np, 'is:np'
      end

      def self.is_not_np
        self.new Item.is_not_np, '-is:np'
      end

      def self.is_pb
        self.new Item.is_pb, 'is:pb'
      end

      def self.is_not_pb
        self.new Item.is_not_pb, '-is:pb'
      end

      def self.is_modeled
        self.new Item.is_modeled, 'is:modeled'
      end

      def self.is_not_modeled
        self.new Item.is_not_modeled, '-is:modeled'
      end

      private

      # Add quotes around the value, if needed.
      def self.q(value)
        /\s/.match(value) ? '"' + value + '"' : value
      end

      def self.pet_type_to_filter_text(pet_type, color_name: nil, species_name: nil)
        # Load the color & species name if needed, or use them from the params
        # if already known (e.g. from parsing a "fits:blue-acara" text query).
        color_name ||= pet_type.color.name
        species_name ||= pet_type.species.name

        # NOTE: Some color syntaxes are weird, like `fits:"polka dot-aisha"`!
        "#{color_name}-#{species_name}".downcase
      end

      def self.alt_style_to_filter_text(alt_style)
        # If the real series name has been set in the database by support
        # staff, use that for the canonical filter text for this alt style.
        # Otherwise, represent this alt style by ID.
        if alt_style.real_series_name?
          series_name = alt_style.series_name.downcase
          color_name = alt_style.color.name.downcase
          species_name = alt_style.species.name.downcase
          "#{series_name}-#{color_name}-#{species_name}"
        else
          "alt-style-#{alt_style.id}"
        end
      end
    end
  end
end