module Montage
  # Represents a pseudo-file system path, where a directory can be replaced
  # with the :sprite named segment. :name will match any directory -- all
  # files with the same :name value will be placed into the same sprite file.
  #
  # Where a sprite source defines a :name segment, no sprite name needs to be
  # explicitly set in the .montage file.
  #
  # For example, given the following directory structure:
  #
  #   /path/to/something/
  #     big/
  #       one.png
  #       two.png
  #     small/
  #       three.png
  #       four.png
  #
  # ... then segmented path "/path/to/something/:name/*" will match all four
  # PNG files, with "one" and "two" being placed in the "big" sprite, "three"
  # and "four" in the "small" sprite.
  #
  class SpriteDefinition
    # Creates a new SpriteDefinition instance.
    #
    # @param [Montage::Project]       project
    # @param [String, Pathname]       path
    # @param [Hash{String => Object}] options
    #
    def initialize(project, path, options = {})
      @project = project
      @path    = project.paths.root + path

      # Symbolize option keys.
      @options = options.inject({}) do |opts, (key, value)|
        opts[key.to_sym] = value ; opts
      end

      @options[:to] = (@project.paths.root +
                      (@options[:to] || Project::DEFAULTS[:to])).to_s

      @options[:url]     ||= project.paths.url
      @options[:padding] ||= project.padding

      if has_name_segment? and @options.has_key?(:name)
        raise Montage::DuplicateName, <<-ERROR.compress_lines
          Sprite `#{path}' has both a :name path segment and a "name"
          option; please use only one.
        ERROR
      elsif not has_name_segment? and not @options.has_key?(:name)
        # Infer the sprite name from the filename.
        @options[:name] =
          File.basename(@options[:to], File.extname(@options[:to]))
      elsif has_name_segment? and not @options[:to] =~ /:name/
        raise Montage::MissingName, <<-ERROR.compress_lines
          Sprite `#{path}' requires :name in the "to" option.
        ERROR
      end
    end

    # Returns an array of Sprites defined.
    #
    # @return [Array(Montage::Sprite)]
    #
    def to_sprites
      matching_sources.map do |sprite_name, sources|
        save_path = Pathname.new(@options[:to].gsub(/:name/, sprite_name))

        url = @options[:url].dup # Since it may be the DEFAULT string
        url.gsub!(/:name/, sprite_name)
        url.gsub!(/:filename/, save_path.basename.to_s)

        Montage::Sprite.new(sprite_name, sources, save_path, @project,
          :url     => url,
          :padding => @options[:padding]
        )
      end
    end

    private # ================================================================

    # Returns whether the path has a :name segment.
    #
    # @return [Boolean]
    #
    def has_name_segment?
      @path.to_s =~ /:name/
    end

    # Returns a Hash containing source files which can be used when creating
    # Sprites. Each key is sprite name with each value the path to a matching
    # source file. When a SpriteDefinition doesn't have a :name segment, the
    # Hash will contain a single key: nil.
    #
    # @return[Hash{String => Array(Pathname)}]
    #
    def matching_sources
      @matching_sources ||= begin
        if not has_name_segment?
          { @options[:name] => Pathname.glob(@path.to_s) }
        else
          regexp = Regexp.escape(@path.to_s)
          regexp.gsub!(/\\\*\\\*/, '.+')
          regexp.gsub!(/\\\*/,     '[^\\/]+')
          regexp.gsub!(/:name/,    '([^\\/]+)')

          # Replace OR globs ({png,jpg,etc}).
          regexp.gsub!(/\\\{([^\\\}]+)\\\}/) do |found|
            "(?:#{found[2..-4].split(',').join('|')})"
          end

          all_files = Pathname.glob(@path.to_s.gsub(/:name/, '*'))
          all_files.inject({}) do |sources, file|
            if file.file? && match = file.to_s.match(regexp)
              sources[match[1]] ||= []
              sources[match[1]] << file
            end

            sources
          end
        end
      end # begin
    end # matching_sources

  end # SpriteDefinition
end # Montage
