# 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
  class POM

    POM_TO_SPEC_MAP = { :group=> 'groupId', :id=> 'artifactId', :type=> 'type',
      :version=> 'version', :classifier=> 'classifier', :scope=> 'scope'}
    SCOPES_TRANSITIVE = [nil, 'compile', 'runtime']
    SCOPES_WE_USE = SCOPES_TRANSITIVE + ['provided']

    # POM project as Hash (using XmlSimple).
    attr_reader :project
    # Parent POM if referenced by this POM.
    attr_reader :parent

    class << self

      # :call-seq:
      #   POM.load(arg)
      #
      # Load new POM object form various kind of sources such as artifact, hash representing spec, filename, XML.
      def load(source)
        case source
        when Hash
          load(Buildr.artifact(source).pom)
        when Artifact
          pom = source.pom
          pom.invoke
          load(pom.to_s)
        when Rake::FileTask
          source.invoke
          load(source.to_s)
        when String
          filename = File.expand_path(source)
          unless pom = cache[filename]
            trace "Loading m2 pom file from #{filename}"
            begin
              pom = POM.new(IO.read(filename))
            rescue REXML::ParseException => e
              fail "Could not parse #{filename}, #{e.continued_exception}"
            end
            cache[filename] = pom
          end
          pom
        else
          raise ArgumentError, 'Expecting Hash spec, Artifact, file name or file task'
        end
      end

    private

      def cache()
        @cache ||= {}
      end

    end

    def initialize(xml) #:nodoc:
      @project = XmlSimple.xml_in(xml)
      @parent = POM.load(pom_to_hash(project["parent"].first).merge(:type=>'pom')) if project['parent']
    end

    # :call-seq:
    #   dependencies(scopes?) => artifacts
    #   dependencies(:scopes = [:runtime, :test, ...], :optional = true) => artifacts
    #
    # Returns list of required dependencies as specified by the POM. You can specify which scopes
    # to use (e.g. "compile", "runtime"); use +nil+ for dependencies with unspecified scope.
    # The default scopes are +nil+, "compile" and "runtime" (aka SCOPES_WE_USE) and no optional dependencies.
    # Specifying optional = true will return all optional dependencies matching the given scopes.
    def dependencies(options = {})
      # backward compatibility
      options = { :scopes => options } if Array === options

      # support symbols, but don't fidget with nil
      options[:scopes] = (options[:scopes] || SCOPES_WE_USE).map { |s| s.to_s if s }

      # try to cache dependencies also
      @depends_for_scopes ||= {}
      unless depends = @depends_for_scopes[options]
        declared = project['dependencies'].first['dependency'] rescue nil
        depends = (declared || [])
        depends = depends.reject { |dep| value_of(dep['optional']) =~ /true/ } unless options[:optional]
        depends = depends.map { |dep|
            spec = pom_to_hash(dep, properties)
            apply = managed(spec)
            spec = apply.merge(spec) if apply

            next if options[:exclusions] && options[:exclusions].any? { |ex| dep['groupId'] == ex['groupId'] && dep['artifactId'] == ex['artifactId'] }

            # calculate transitive dependencies
            if options[:scopes].include?(spec[:scope])
              spec.delete(:scope)

              exclusions = dep['exclusions'].first['exclusion'] rescue nil
              transitive_deps = POM.load(spec).dependencies(:exclusions => exclusions, :scopes => (options[:scopes_transitive] || SCOPES_TRANSITIVE) ) rescue []

              [Artifact.to_spec(spec)] + transitive_deps
            end
          }.flatten.compact #.uniq_by{|spec| art = spec.split(':'); "#{art[0]}:#{art[1]}"}
        @depends_for_scopes[options] = depends
      end
      depends
    end

    # :call-seq:
    #   properties() => hash
    #
    # Returns properties available to this POM as hash. Includes explicit properties and pom.xxx/project.xxx
    # properties for groupId, artifactId, version and packaging.
    def properties()
      @properties ||= begin
        pom = %w(groupId artifactId version packaging).inject({}) { |hash, key|
          value = project[key] || (parent ? parent.project[key] : nil)
          hash[key] = hash["pom.#{key}"] = hash["project.#{key}"] = value_of(value) if value
          hash
        }
        pom = %w(groupId artifactId version).inject(pom) { |hash, key|
          value = parent.project[key]
          hash[key] = hash["pom.parent.#{key}"] = hash["project.parent.#{key}"] = value_of(value) if value
          hash
        } if parent
        props = project['properties'].first rescue {}
        props = props.inject({}) { |mapped, pair| mapped[pair.first] = value_of(pair.last, props) ; mapped }
        (parent ? parent.properties.merge(props) : props).merge(pom)
      end
    end

    # :call-seq:
    #    managed() => hash
    #    managed(hash) => hash
    #
    # The first form returns all the managed dependencies specified by this POM in dependencyManagement.
    # The second form uses a single spec hash and expands it from the current/parent POM. Used to determine
    # the version number if specified in dependencyManagement instead of dependencies.
    def managed(spec = nil)
      if spec
        managed.detect { |dep| [:group, :id, :type, :classifier].all? { |key| spec[key] == dep[key] } } ||
          (parent ? parent.managed(spec) : nil)
      else
        @managed ||= begin
          managed = project['dependencyManagement'].first['dependencies'].first['dependency'] rescue nil
          managed ? managed.map { |dep| pom_to_hash(dep, properties) } : []
        end
      end
    end

  private

    # :call-seq:
    #    value_of(element) => string
    #    value_of(element, true) => string
    #
    # Returns the normalized text value of an element from its XmlSimple value. The second form performs
    # property substitution.
    def value_of(element, substitute = nil)
      value = element.to_a.join.strip
      value = value.gsub(/\$\{([^}]+)\}/) { |key| Array(substitute[$1]).join.strip } if substitute
      value
    end

    # :call-seq:
    #    pom_to_hash(element) => hash
    #    pom_to_hash(element, true) => hash
    #
    # Return the spec hash from an XmlSimple POM referencing element (e.g. project, parent, dependency).
    # The second form performs property substitution.
    def pom_to_hash(element, substitute = nil)
      hash = POM_TO_SPEC_MAP.inject({}) { |spec, pair|
        spec[pair.first] = value_of(element[pair.last], substitute) if element[pair.last]
        spec
      }
      {:scope => 'compile', :type => 'jar'}.merge(hash)
    end

  end
end
