# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with this
# work for additional information regarding copyright ownership.  The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations under
# the License.

module Buildr #:nodoc:

  desc 'Download all artifacts'
  task 'artifacts'

  desc "Download all artifacts' sources"
  task 'artifacts:sources'

  desc "Download all artifacts' external annotations"
  task 'artifacts:annotations'

  desc "Download all artifacts' javadoc"
  task 'artifacts:javadoc'

  # Mixin with a task to make it behave like an artifact. Implemented by the packaging tasks.
  #
  # An artifact has an identifier, group identifier, type, version number and
  # optional classifier. All can be used to locate it in the local repository,
  # download from or upload to a remote repository.
  #
  # The #to_spec and #to_hash methods allow it to be used everywhere an artifact is
  # accepted.
  module ActsAsArtifact

    ARTIFACT_ATTRIBUTES = [:group, :id, :type, :classifier, :version]
    MAVEN_METADATA = 'maven-metadata.xml'

    class << self
    private

      # :stopdoc:
      def included(mod)
        mod.extend self
      end

      def extend_object(base)
        base.instance_eval { alias :install_old :install     } if base.respond_to? :install
        base.instance_eval { alias :uninstall_old :uninstall } if base.respond_to? :uninstall
        base.instance_eval { alias :upload_old :upload       } if base.respond_to? :upload
        super
      end

      def extended(base)
        #We try to keep the previous instance methods defined on the base instance if there were ones.
        base.instance_eval { alias :install :install_old     } if base.respond_to? :install_old
        base.instance_eval { alias :uninstall :uninstall_old } if base.respond_to? :uninstall_old
        base.instance_eval { alias :upload :upload_old       } if base.respond_to? :upload_old
      end

      # :startdoc:
    end

    # The artifact identifier.
    attr_reader :id
    # The group identifier.
    attr_reader :group
    # The file type. (Symbol)
    attr_reader :type
    # The version number.
    attr_reader :version
    # Optional artifact classifier.
    attr_reader :classifier

    attr_accessor :buildr_project

    def snapshot?
      version =~ /-SNAPSHOT$/
    end

    def final_version
      return version unless snapshot?
      Time.now.strftime("%Y%m%d.%H%M%S")
    end

    # :call-seq:
    #   to_spec_hash => Hash
    #
    # Returns the artifact specification as a hash. For example:
    #   com.example:app:jar:1.2
    # becomes:
    #   { :group=>'com.example',
    #     :id=>'app',
    #     :type=>:jar,
    #     :version=>'1.2' }
    def to_spec_hash
      base = { :group=>group, :id=>id, :type=>type, :version=>version }
      classifier ? base.merge(:classifier=>classifier) : base
    end
    alias_method :to_hash, :to_spec_hash

    # :call-seq:
    #   to_spec => String
    #
    # Returns the artifact specification, in the structure:
    #   <group>:<artifact>:<type>:<version>
    # or
    #   <group>:<artifact>:<type>:<classifier>:<version>
    def to_spec
      classifier ? "#{group}:#{id}:#{type}:#{classifier}:#{version}" : "#{group}:#{id}:#{type}:#{version}"
    end

    # :call-seq:
    #   pom => Artifact
    #
    # Convenience method that returns a POM artifact.
    def pom
      return self if type == :pom
      Buildr.artifact(:group=>group, :id=>id, :version=>version, :type=>:pom)
    end

    # :call-seq:
    #   sources_artifact => Artifact
    #
    # Convenience method that returns a sources artifact.
    def sources_artifact
      sources_spec = to_spec_hash.merge(:classifier=>'sources')
      sources_task = OptionalArtifact.define_task(Buildr.repositories.locate(sources_spec))
      sources_task.send :apply_spec, sources_spec
      sources_task
    end

    # :call-seq:
    #   javadoc_artifact => Artifact
    #
    # Convenience method that returns the associated javadoc artifact
    def javadoc_artifact
      javadoc_spec = to_spec_hash.merge(:classifier=>'javadoc')
      javadoc_task = OptionalArtifact.define_task(Buildr.repositories.locate(javadoc_spec))
      javadoc_task.send :apply_spec, javadoc_spec
      javadoc_task
    end

    # :call-seq:
    #   annotations_artifact => Artifact
    #
    # Convenience method that returns an annotations artifact. The annotations artifact is used by
    # Intellij IDEA as a source of external annotations.
    def annotations_artifact
      annotations_spec = to_spec_hash.merge(:classifier=>'annotations')
      annotations_task = OptionalArtifact.define_task(Buildr.repositories.locate(annotations_spec))
      annotations_task.send :apply_spec, annotations_spec
      annotations_task
    end

    # :call-seq:
    #   pom_xml => string
    #
    # Creates POM XML for this artifact.
    def pom_xml
      if self.buildr_project
        Buildr::CustomPom.pom_xml(self.buildr_project, self)
      else
        Proc.new do
          xml = Builder::XmlMarkup.new(:indent => 2)
          xml.instruct!
          xml.project do
            xml.modelVersion '4.0.0'
            xml.groupId group
            xml.artifactId id
            xml.version version
            xml.classifier classifier if classifier
          end
        end
      end
    end

    # :call-seq:
    #   maven_metadata_xml => string
    #
    # Creates Maven Metadata XML content for this artifact.
    def maven_metadata_xml
      xml = Builder::XmlMarkup.new(:indent=>2)
      xml.instruct!
      xml.metadata do
        xml.groupId       group
        xml.artifactId    id
        xml.version       version
        xml.versioning do
          xml.snapshot do
            xml.timestamp final_version
            xml.buildNumber 1
          end
          xml.lastupdated Time.now.strftime("%Y%m%d%H%M%S")
        end
      end
    end

    def install
      invoke
      in_local_repository = Buildr.repositories.locate(self)
      if pom && pom != self && classifier.nil?
        pom.invoke
        pom.install
      end
      if name != in_local_repository
        mkpath File.dirname(in_local_repository)
        cp name, in_local_repository, :preserve => false
        info "Installed #{name} to #{in_local_repository}"
      end
    end

    def uninstall
      installed = Buildr.repositories.locate(self)
      rm installed if File.exist?(installed)
      pom.uninstall if pom && pom != self && classifier.nil?
    end

    # :call-seq:
    #   upload
    #   upload(url)
    #   upload(options)
    #
    # Uploads the artifact, its POM and digital signatures to remote server.
    #
    # In the first form, uses the upload options specified by repositories.release_to
    # or repositories.snapshot_to if the subject is a snapshot artifact.
    # In the second form, uses a URL that includes all the relevant information.
    # In the third form, uses a hash with the options :url, :username, :password,
    # and :permissions. All but :url are optional.
    def upload(upload_to = nil)
      upload_task(upload_to).invoke
    end

    def upload_task(upload_to = nil)
      upload_to ||= Buildr.repositories.snapshot_to if snapshot? && Buildr.repositories.snapshot_to != nil && Buildr.repositories.snapshot_to[:url] != nil
      upload_to ||= Buildr.repositories.release_to
      upload_to = { :url=>upload_to } unless Hash === upload_to
      raise ArgumentError, 'Don\'t know where to upload, perhaps you forgot to set repositories.release_to' unless upload_to[:url]

      # Set the upload URI, including mandatory slash (we expect it to be the base directory).
      # Username/password may be part of URI, or separate entities.
      uri = URI.parse(upload_to[:url].clone)
      uri.path = uri.path + '/' unless uri.path[-1] == '/'
      to_escape = "!\"\#$%&'()*+,-./:;<=>?@{}|~`'"
      uri.user = URI.encode(upload_to[:username], to_escape) if upload_to[:username]
      uri.password = URI.encode(upload_to[:password], to_escape) if upload_to[:password]

      path = group.gsub('.', '/') + "/#{id}/#{version}/#{upload_name}"

      unless task = Buildr.application.lookup(uri+path)
        deps = [self]
        deps << pom.upload_task( upload_to ) if pom && pom != self && classifier.nil?

        task = Rake::Task.define_task uri + path => deps do
          # Upload artifact relative to base URL, need to create path before uploading.
          options = upload_to[:options] || {:permissions => upload_to[:permissions]}
          info "Deploying #{to_spec}"
          URI.upload uri + path, name, options
          if snapshot? && pom != self
             maven_metadata = group.gsub('.', '/') + "/#{id}/#{version}/#{MAVEN_METADATA}"
             URI.write uri + maven_metadata, maven_metadata_xml, :permissions => upload_to[:permissions]
          end
        end
      end
      task
    end

  protected

    # Apply specification to this artifact.
    def apply_spec(spec)
      spec = Artifact.to_hash(spec)
      ARTIFACT_ATTRIBUTES.each { |key| instance_variable_set("@#{key}", spec[key]) }
      self
    end

    def group_path
      group.gsub('.', '/')
    end

    def upload_name
      return File.basename(name) unless snapshot?
      return File.basename(name).gsub(/SNAPSHOT/, "#{final_version}-1")
    end

    def extract_type(type)
      return :jar if type == :bundle
      type
    end

  end


  # A file task referencing an artifact in the local repository.
  #
  # This task includes all the artifact attributes (group, id, version, etc). It points
  # to the artifact's path in the local repository. When invoked, it will download the
  # artifact into the local repository if the artifact does not already exist.
  #
  # Note: You can enhance this task to create the artifact yourself, e.g. download it from
  # a site that doesn't have a remote repository structure, copy it from a different disk, etc.
  class Artifact < Rake::FileTask

    # The default artifact type.
    DEFAULT_TYPE = :jar

    include ActsAsArtifact, Buildr

    class << self

      # :call-seq:
      #   lookup(spec) => Artifact
      #
      # Lookup a previously registered artifact task based on its specification (String or Hash).
      def lookup(spec)
        @artifacts ||= {}
        @artifacts[to_spec(spec)]
      end

      # :call-seq:
      #   list => specs
      #
      # Returns an array of specs for all the registered artifacts. (Anything created from artifact, or package).
      def list
        @artifacts ||= {}
        @artifacts.keys
      end

      # :call-seq:
      #   register(artifacts) => artifacts
      #
      # Register an artifact task(s) for later lookup (see #lookup).
      def register(*tasks)
        @artifacts ||= {}
        fail 'You can only register an artifact task, one of the arguments is not a Task that responds to to_spec' unless
          tasks.all? { |task| task.respond_to?(:to_spec) && task.respond_to?(:invoke) }
        tasks.each { |task| @artifacts[task.to_spec] = task }
        tasks
      end

      # :call-seq:
      #   to_hash(spec_hash) => spec_hash
      #   to_hash(spec_string) => spec_hash
      #   to_hash(artifact) => spec_hash
      #
      # Turn a spec into a hash. This method accepts a String, Hash or any object that responds to
      # the method to_spec. There are several reasons to use this method:
      # * You can pass anything that could possibly be a spec, and get a hash.
      # * It will check that the spec includes the group identifier, artifact
      #   identifier and version number and set the file type, if missing.
      # * It will always return a new specs hash.
      def to_hash(spec)
        if spec.respond_to?(:to_spec)
          to_hash spec.to_spec
        elsif Hash === spec
          rake_check_options spec, :id, :group, :type, :classifier, :version, :scope
          # Sanitize the hash and check it's valid.
          spec = ARTIFACT_ATTRIBUTES.inject({}) { |h, k| h[k] = spec[k].to_s if spec[k] ; h }
          fail "Missing group identifier for #{spec.inspect}" unless spec[:group]
          fail "Missing artifact identifier for #{spec.inspect}" unless spec[:id]
          fail "Missing version for #{spec.inspect}" unless spec[:version]
          spec[:type] = (spec[:type] || DEFAULT_TYPE).to_sym
          spec
        elsif String === spec
          group, id, type, version, *rest = spec.split(':').map { |part| part.empty? ? nil : part }
          unless rest.empty?
            # Optional classifier comes before version.
            classifier, version = version, rest.shift
            fail "Expecting <group:id:type:version> or <group:id:type:classifier:version>, found <#{spec}>" unless rest.empty?
          end
          to_hash :group=>group, :id=>id, :type=>type, :version=>version, :classifier=>classifier
        else
          fail 'Expecting a String, Hash or object that responds to to_spec'
        end
      end

      # :call-seq:
      #   to_spec(spec_hash) => spec_string
      #
      # Convert a hash back to a spec string. This method accepts
      # a string, hash or any object that responds to to_spec.
      def to_spec(hash)
        hash = to_hash(hash) unless Hash === hash
        version = ":#{hash[:version]}" if hash[:version]
        classifier = ":#{hash[:classifier]}" if hash[:classifier]
        "#{hash[:group]}:#{hash[:id]}:#{hash[:type] || DEFAULT_TYPE}#{classifier}#{version}"
      end

      # :call-seq:
      #   hash_to_file_name(spec_hash) => file_name
      #
      # Convert a hash spec to a file name.
      def hash_to_file_name(hash)
        version = "-#{hash[:version]}" if hash[:version]
        classifier = "-#{hash[:classifier]}" if hash[:classifier]
        "#{hash[:id]}#{version}#{classifier}.#{extract_type(hash[:type]) || DEFAULT_TYPE}"
      end

    end

    def initialize(*args) #:nodoc:
      super
      enhance do |task|
        # Default behavior: download the artifact from one of the remote repositories
        # if the file does not exist. But this default behavior is counter productive
        # if the artifact knows how to build itself (e.g. download from a different location),
        # so don't perform it if the task found a different way to create the artifact.
        task.enhance do
          if download_needed? task
            info "Downloading #{to_spec}"
            download
            pom.invoke rescue nil if pom && pom != self && classifier.nil?
          end
        end
      end
    end

    # :call-seq:
    #   from(path) => self
    #
    # Use this when you want to install or upload an artifact from a given file, for example:
    #   test = artifact('group:id:jar:1.0').from('test.jar')
    #   install test
    # See also Buildr#install and Buildr#upload.
    def from(path)
      @from = path.is_a?(Rake::Task) ? path : File.expand_path(path.to_s)
      enhance [@from] do
        mkpath File.dirname(name)
        cp @from.to_s, name
      end
      pom.content pom_xml unless pom == self || pom.has_content?
      self
    end

    # :call-seq:
    #   content(string) => self
    #   content(Proc) => self
    #
    # Use this when you want to install or upload an artifact from a given content, for example:
    #   readme = artifact('com.example:readme:txt:1.0').content(<<-EOF
    #     Please visit our website at http://example.com/readme
    #   <<EOF
    #   install readme
    #
    # If the argument is a Proc the it will be called when the artifact is written out. If the result is not a proc
    # and not a string, it will be converted to a string using to_s
    def content(string = nil)
      unless string
        @content = @content.call if @content.is_a?(Proc)
        return @content
      end

      unless @content
        enhance do
          write name, self.content
        end

        class << self
          # Force overwriting target since we don't have source file
          # to check for timestamp modification
          def needed?
            true
          end
        end
      end
      @content = string
      pom.content pom_xml unless pom == self || pom.has_content?
      self
    end

  protected

    def has_content?
      @from || @content
    end

    # :call-seq:
    #   download
    #
    # Downloads an artifact from one of the remote repositories, and stores it in the local
    # repository. Raises an exception if the artifact is not found.
    #
    # This method attempts to download the artifact from each repository in the order in
    # which they are returned from #remote, until successful.
    def download
      trace "Downloading #{to_spec}"
      remote = Buildr.repositories.remote_uri
      remote = remote.each { |repo_url| repo_url.path += '/' unless repo_url.path[-1] == '/' }
      fail "Unable to download #{to_spec}. No remote repositories defined." if remote.empty?
      exact_success = remote.find do |repo_url|
        begin
          path = "#{group_path}/#{id}/#{version}/#{File.basename(name)}"
          download_artifact(repo_url + path)
          true
        rescue URI::NotFoundError
          false
        rescue Exception=>error
          info error
          trace error.backtrace.join("\n")
          false
        end
      end

      if exact_success
        return
      elsif snapshot?
        download_m2_snapshot(remote)
      else
        fail_download(remote)
      end
    end

    def download_m2_snapshot(remote_uris)
      remote_uris.find do |repo_url|
        snapshot_url = current_snapshot_repo_url(repo_url)
        if snapshot_url
          begin
            download_artifact snapshot_url
            true
          rescue URI::NotFoundError
            false
          end
        else
          false
        end
      end or fail_download(remote_uris)
    end

    def current_snapshot_repo_url(repo_url)
      begin
        metadata_path = "#{group_path}/#{id}/#{version}/maven-metadata.xml"
        metadata_xml = StringIO.new
        URI.download repo_url + metadata_path, metadata_xml
        metadata = REXML::Document.new(metadata_xml.string).root
        timestamp = REXML::XPath.first(metadata, '//timestamp')
        build_number = REXML::XPath.first(metadata, '//buildNumber')
        error "No timestamp provided for the snapshot #{to_spec}" if timestamp.nil?
        error "No build number provided for the snapshot #{to_spec}" if build_number.nil?
        return nil if timestamp.nil? || build_number.nil?
        snapshot_of = version[0, version.size - 9]
        classifier_snippet = (classifier != nil) ? "-#{classifier}" : ""
        repo_url + "#{group_path}/#{id}/#{version}/#{id}-#{snapshot_of}-#{timestamp.text}-#{build_number.text}#{classifier_snippet}.#{type}"
      rescue URI::NotFoundError
        nil
      end
    end

    def fail_download(remote_uris)
      fail "Failed to download #{to_spec}, tried the following repositories:\n#{remote_uris.join("\n")}"
    end

   protected

    # :call-seq:
    #   needed?
    #
    # Validates whether artifact is required to be downloaded from repository
    def needed?
      return true if snapshot? && File.exist?(name) && (update_snapshot? || old?)
      super
    end

  private

    # :call-seq:
    #   download_artifact
    #
    # Downloads artifact from given repository,
    # supports downloading snapshot artifact with relocation on succeed to local repository
    def download_artifact(path)
      download_file = "#{name}.#{Time.new.to_i}"
      begin
        URI.download path, download_file
        if File.exist?(download_file)
          FileUtils.mkdir_p(File.dirname(name))
          FileUtils.mv(download_file, name)
        end
      ensure
        File.delete(download_file) if File.exist?(download_file)
      end
    end

    # :call-seq:
    #   :download_needed?
    #
    # Validates whether artifact is required to be downloaded from repository
    def download_needed?(task)
      return true if !File.exist?(name)

      if snapshot?
        return false if offline? && File.exist?(name)
        return true if update_snapshot? || old?
      end

      return false
    end

    def update_snapshot?
      Buildr.application.options.update_snapshots
    end

    def offline?
      Buildr.application.options.work_offline
    end

    # :call-seq:
    #   old?
    #
    # Checks whether existing artifact is older than period from build settings or one day
    def old?
      settings = Buildr.application.settings
      time_to_be_old = settings.user[:expire_time] || settings.build[:expire_time] || 60 * 60 * 24
      File.mtime(name).to_i < (Time.new.to_i - time_to_be_old)
    end

  end


  # An artifact that is optional.
  # If downloading fails, the user will be informed but it will not raise an exception.
  class OptionalArtifact < Artifact

    protected

    # If downloading fails, the user will be informed but it will not raise an exception.
    def download
      super
    rescue
      info "Failed to download #{to_spec}. Skipping it."
    end

  end


  # Holds the path to the local repository, URLs for remote repositories, and settings for release server.
  #
  # You can access this object from the #repositories method. For example:
  #   puts repositories.local
  #   repositories.remote << 'http://example.com/repo'
  #   repositories.release_to = 'sftp://example.com/var/www/public/repo'
  class Repositories
    include Singleton

    # :call-seq:
    #   local => path
    #
    # Returns the path to the local repository.
    #
    # The default path is .m2/repository relative to the home directory.
    # You can set this using the M2_REPO environment variable or the repositories/local
    # value in your settings.yaml file.
    def local
      @local ||= File.expand_path(ENV['M2_REPO'] || ENV['local_repo'] ||
        (Buildr.settings.user['repositories'] && Buildr.settings.user['repositories']['local']) ||
        File.join(ENV['HOME'], '.m2/repository'))
    end

    # :call-seq:
    #   local = path
    #
    # Sets the path to the local repository.
    #
    # The best place to set the local repository path is from a buildr.rb file
    # located in the .buildr directory under your home directory. That way all
    # your projects will share the same path, without affecting other developers
    # collaborating on these projects.
    def local=(dir)
      @local = dir ? File.expand_path(dir) : nil
    end

    # :call-seq:
    #   locate(spec) => path
    #
    # Locates an artifact in the local repository based on its specification, and returns
    # a file path.
    #
    # For example:
    #   locate :group=>'log4j', :id=>'log4j', :version=>'1.1'
    #     => ~/.m2/repository/log4j/log4j/1.1/log4j-1.1.jar
    def locate(spec)
      spec = Artifact.to_hash(spec)
      File.join(local, spec[:group].split('.'), spec[:id], spec[:version], Artifact.hash_to_file_name(spec))
    end

    # :call-seq:
    #   mirrors => Array
    #
    # Returns an array of all the mirror repository URLs.
    #
    # Mirrors override remote repositories defined in the project.
    # The best way is to add repositories to the user settings file under '$HOME/.buildr/settings.yaml'.
    # For example:
    #   repositories:
    #     mirrors:
    #     - http://example.com/repository
    def mirrors
      unless @mirrors
        @mirrors = [Buildr.settings.user, Buildr.settings.build].inject([]) { |repos, hash|
          repos | Array(hash['repositories'] && hash['repositories']['mirrors'])
        }
      end
      @mirrors
    end

    # :call-seq:
    #   remote = Array
    #   remote = url
    #   remote = nil
    #
    # With a String argument, clears the array and set it to that single URL.
    #
    # With an Array argument, clears the array and set it to these specific URLs.
    #
    # With nil, clears the array.
    def mirrors=(urls)
      case urls
      when nil then @mirrors = nil
      when Array then @mirrors = urls.dup
      else @mirrors = [urls.to_s]
      end
    end

    # :call-seq:
    #   remote => Array
    #
    # Returns an array of all the remote repository URLs.
    #
    # When downloading artifacts, repositories are accessed in the order in which they appear here.
    # The best way is to add repositories individually, for example:
    #   repositories.remote << 'http://example.com/repo'
    #
    # You can also specify remote repositories in the settings.yaml (per user) and build.yaml (per build)
    # files.  Both sets of URLs are loaded by default into this array, URLs from the personal setting
    # showing first.
    #
    # For example:
    #   repositories:
    #     remote:
    #     - http://example.com/repo
    #     - http://elsewhere.com/repo
    def remote
      unless mirrors.empty?
        info "Remote repositories overridden by mirrors #{mirrors.map(&:to_s).join(", ")}"
        mirrors
      end
      unless @remote
        @remote = [Buildr.settings.user, Buildr.settings.build].inject([]) { |repos, hash|
          repos | Array(hash['repositories'] && hash['repositories']['remote'])
        }
      end
      @remote
    end

    # :call-seq:
    #   remote_uri => Array
    #
    # Returns an array of all the remote repositories as instances of URI
    #
    # Supports
    #  * String urls: "http://example.com/repo"
    #  * URI: URI.parse( "http://example.com/repo" )
    #  * Hash: { :url => "http://example.com/repo", :user => "user", :pass => "pass" }
    #
    def remote_uri
      remote

      uris = []
      @remote.each do |repo|
        case repo
        when nil then
          # ignore nil
        when URI then
          uris << repo
        when Hash then
          url = (repo[:url] || repo['url'] )
          if url
            uri = URI.parse(url)
            if ( username = (repo[:username] || repo['username'] || repo[:user] || repo['user']) )
              uri.user = username
            end

            if ( password = (repo[:password] || repo['password'] || repo[:pass] || repo['pass']) )
              uri.password = password
            end
            uris << uri
          else
            fail( "Repository Hash format missing url: #{repo}" )
          end

        when String then
          uris << URI.parse(repo)
        else
          fail( "Unsupported Repository format: #{repo}" )
        end
      end

      uris
    end

    # :call-seq:
    #   remote = Array
    #   remote = url
    #   remote = nil
    #
    # With a String argument, clears the array and set it to that single URL.
    #
    # With an Array argument, clears the array and set it to these specific URLs.
    #
    # With nil, clears the array.
    def remote=(urls)
      case urls
      when nil then @remote = nil
      when Array then @remote = urls.dup
      else @remote = [urls.to_s]
      end
    end

    # :call-seq:
    #   release_to = url
    #   release_to = hash
    #
    # Specifies the release server. Accepts a Hash with different repository settings
    # (e.g. url, username, password), or a String to only set the repository URL.
    #
    # Besides the URL, all other settings depend on the transport protocol in use.
    #
    # For example:
    #   repositories.release_to = 'sftp://john:secret@example.com/var/www/repo/'
    #
    #   repositories.release_to = { :url=>'sftp://example.com/var/www/repo/',
    #                                :username='john', :password=>'secret' }
    # Or in the settings.yaml file:
    #   repositories:
    #     release_to: sftp://john:secret@example.com/var/www/repo/
    #
    #   repositories:
    #     release_to:
    #       url: sftp://example.com/var/www/repo/
    #       username: john
    #       password: secret
    def release_to=(options)
      options = { :url=>options } unless Hash === options
      @release_to = options
    end

    # :call-seq:
    #   release_to => hash
    #
    # Returns the current release server setting as a Hash. This is a more convenient way to
    # configure the settings, as it allows you to specify the settings progressively.
    #
    # For example, the Buildfile will contain the repository URL used by all developers:
    #   repositories.release_to[:url] ||= 'sftp://example.com/var/www/repo'
    # Your private buildr.rb will contain your credentials:
    #   repositories.release_to[:username] = 'john'
    #   repositories.release_to[:password] = 'secret'
    def release_to
      unless @release_to
        value = (Buildr.settings.user['repositories'] && Buildr.settings.user['repositories']['release_to']) \
          || (Buildr.settings.build['repositories'] && Buildr.settings.build['repositories']['release_to'])
        @release_to = Hash === value ? value.inject({}) { |hash, (key, value)| hash.update(key.to_sym=>value) } : { :url=>Array(value).first }
      end
      @release_to
    end

    # :call-seq:
    #   snapshot_to = url
    #   snapshot_to = hash
    #
    # Specifies the release server. Accepts a Hash with different repository settings
    # (e.g. url, username, password), or a String to only set the repository URL.
    #
    # Besides the URL, all other settings depend on the transport protocol in use.
    #
    # For example:
    #   repositories.snapshot_to = 'sftp://john:secret@example.com/var/www/repo/'
    #
    #   repositories.snapshot_to = { :url=>'sftp://example.com/var/www/repo/',
    #                                :username='john', :password=>'secret' }
    # Or in the settings.yaml file:
    #   repositories:
    #     snapshot_to: sftp://john:secret@example.com/var/www/repo/
    #
    #   repositories:
    #     snapshot_to:
    #       url: sftp://example.com/var/www/repo/
    #       username: john
    #       password: secret
    def snapshot_to=(options)
      options = { :url=>options } unless Hash === options
      @snapshot_to = options
    end

    # :call-seq:
    #   snapshot_to => hash
    #
    # Returns the current snapshot release server setting as a Hash. This is a more convenient way to
    # configure the settings, as it allows you to specify the settings progressively.
    #
    # For example, the Buildfile will contain the repository URL used by all developers:
    #   repositories.snapshot_to[:url] ||= 'sftp://example.com/var/www/repo'
    # Your private buildr.rb will contain your credentials:
    #   repositories.snapshot_to[:username] = 'john'
    #   repositories.snapshot_to[:password] = 'secret'
    def snapshot_to
      unless @snapshot_to
        value = (Buildr.settings.user['repositories'] && Buildr.settings.user['repositories']['snapshot_to']) \
          || (Buildr.settings.build['repositories'] && Buildr.settings.build['repositories']['snapshot_to'])
        @snapshot_to = Hash === value ? value.inject({}) { |hash, (key, value)| hash.update(key.to_sym=>value) } : { :url=>Array(value).first }
      end
      @snapshot_to
    end

  end

  # :call-seq:
  #    repositories => Repositories
  #
  # Returns an object you can use for setting the local repository path, remote repositories
  # URL and release server settings.
  #
  # See Repositories.
  def repositories
    Repositories.instance
  end

  # :call-seq:
  #   artifact(spec) => Artifact
  #   artifact(spec) { |task| ... } => Artifact
  #
  # Creates a file task to download and install the specified artifact in the local repository.
  #
  # You can use a String or a Hash for the artifact specification. The file task will point at
  # the artifact's path inside the local repository. You can then use this tasks as a prerequisite
  # for other tasks.
  #
  # This task will download and install the artifact only once. In fact, it will download and
  # install the artifact if the artifact does not already exist. You can enhance it if you have
  # a different way of creating the artifact in the local repository. See Artifact for more details.
  #
  # For example, to specify an artifact:
  #   artifact('log4j:log4j:jar:1.1')
  #
  # To use the artifact in a task:
  #   compile.with artifact('log4j:log4j:jar:1.1')
  #
  # To specify an artifact and the means for creating it:
  #   download(artifact('dojo:dojo-widget:zip:2.0')=>
  #     'http://download.dojotoolkit.org/release-2.0/dojo-2.0-widget.zip')
  def artifact(spec, path = nil, &block) #:yields:task
    spec = artifact_ns.fetch(spec) if spec.kind_of?(Symbol)
    spec = Artifact.to_hash(spec)
    unless task = Artifact.lookup(spec)
      task = Artifact.define_task(path || repositories.locate(spec))
      task.send :apply_spec, spec
      Rake::Task['rake:artifacts'].enhance [task]
      Artifact.register(task)
      unless spec[:type] == :pom
        Rake::Task['artifacts:sources'].enhance [task.sources_artifact]
        Rake::Task['artifacts:javadoc'].enhance [task.javadoc_artifact]
        Rake::Task['artifacts:annotations'].enhance [task.annotations_artifact]
      end
    end
    task.enhance &block
  end

  # :call-seq:
  #   artifacts(*spec) => artifacts
  #
  # Handles multiple artifacts at a time. This method is the plural equivalent of
  # #artifact, but can do more things.
  #
  # Returns an array of artifacts built using the supplied
  # specifications, each of which can be:
  # * An artifact specification (String or Hash). Returns the appropriate Artifact task.
  # * An artifact of any other task. Returns the task as is.
  # * A project. Returns all artifacts created (packaged) by that project.
  # * A string. Returns that string, assumed to be a file name.
  # * An array of artifacts or a Struct.
  # * A symbol. Returns the named artifact from the current ArtifactNamespace
  #
  # For example, handling a collection of artifacts:
  #   xml = [ xerces, xalan, jaxp ]
  #   ws = [ axis, jax-ws, jaxb ]
  #   db = [ jpa, mysql, sqltools ]
  #   artifacts(xml, ws, db)
  #
  # Using artifacts created by a project:
  #   artifacts project('my-app')               # All packages
  #   artifacts project('my-app').package(:war) # Only the WAR
  def artifacts(*specs, &block)
    specs.flatten.inject([]) do |set, spec|
      case spec
      when ArtifactNamespace
        set |= spec.artifacts
      when Symbol, Hash
        set |= [artifact(spec)]
      when /([^:]+:){2,4}/ # A spec as opposed to a file name.
        set |= [artifact(spec)]
      when String # Must always expand path.
        set |= [File.expand_path(spec)]
      when Project
        set |= artifacts(spec.packages)
      when Rake::Task
        set |= [spec]
      when Struct
        set |= artifacts(spec.values)
      else
        if spec.respond_to? :to_spec
          set |= artifacts(spec.to_spec)
        else
          fail "Invalid artifact specification in #{specs.inspect}"
        end
      end
    end
  end

  def transitive(*args)
    options = Hash === args.last ? args.pop : {}
    dep_opts = {
      :scopes   => options[:scopes] || [nil, "compile", "runtime"],
      :optional => options[:optional]
    }
    specs = args.flatten
    specs.inject([]) do |set, spec|
      case spec
      when /([^:]+:){2,4}/ # A spec as opposed to a file name.
        artifact = artifact(spec)
        set |= [artifact] unless artifact.type == :pom
        set |= POM.load(artifact.pom).dependencies(dep_opts).map { |spec| artifact(spec) }
      when Hash
        set |= [transitive(spec, options)]
      when String # Must always expand path.
        set |= transitive(file(File.expand_path(spec)), options)
      when Project
        set |= transitive(spec.packages, options)
      when Rake::Task
        set |= spec.respond_to?(:to_spec) ? transitive(spec.to_spec, options) : [spec]
      when Struct
        set |= transitive(spec.values, options)
      else
        fail "Invalid artifact specification in: #{specs.to_s}"
      end
    end
  end

  # :call-seq:
  #   group(ids, :under=>group_name, :version=>number) => artifacts
  #
  # Convenience method for defining multiple artifacts that belong to the same group, type and version.
  # Accepts multiple artifact identifiers followed by two or three hash values:
  # * :under -- The group identifier
  # * :version -- The version number
  # * :type -- The artifact type (optional)
  # * :classifier -- The artifact classifier (optional)
  #
  # For example:
  #   group 'xbean', 'xbean_xpath', 'xmlpublic', :under=>'xmlbeans', :version=>'2.1.0'
  # Or:
  #   group %w{xbean xbean_xpath xmlpublic}, :under=>'xmlbeans', :version=>'2.1.0'
  def group(*args)
    hash = args.pop
    args.flatten.map do |id|
      artifact :group   => hash[:under],
               :type    => hash[:type],
               :version => hash[:version],
               :classifier => hash[:classifier],
               :id => id
    end
  end

  # :call-seq:
  #   install(artifacts) => install_task
  #
  # Installs the specified artifacts in the local repository as part of the install task.
  #
  # You can use this to install various files in the local repository, for example:
  #   install artifact('group:id:jar:1.0').from('some_jar.jar')
  #   $ buildr install
  def install(*args, &block)
    artifacts = artifacts(args).uniq
    raise ArgumentError, 'This method can only install artifacts' unless artifacts.all? { |f| f.respond_to?(:to_spec) }
    task('install').tap do |install|
      install.enhance(artifacts) do
        artifacts.each(&:install)
      end
      install.enhance &block if block
      task('uninstall') do
        artifacts.map(&:to_s ).each { |file| rm file if File.exist?(file) }
      end
    end
  end

  # :call-seq:
  #   upload(artifacts)
  #
  # Uploads the specified artifacts to the release server as part of the upload task.
  #
  # You can use this to upload various files to the release server, for example:
  #   upload artifact('group:id:jar:1.0').from('some_jar.jar')
  #   $ buildr upload
  def upload(*args, &block)
    artifacts = artifacts(args)
    raise ArgumentError, 'This method can only upload artifacts' unless artifacts.all? { |f| f.respond_to?(:to_spec) }
    upload_artifacts_tasks = artifacts.map { |artifact| artifact.upload_task }
    task('upload').tap do |task|
      task.enhance &block if block
      task.enhance upload_artifacts_tasks
    end
  end

end
