# 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:
  module Eclipse #:nodoc:
    include Extension

    class Eclipse

      attr_reader :options
      attr_writer :name

      def initialize(project)
        @project = project
        @options = Options.new(project)
      end

      def name
        return @name if @name
        return @project.id.split('-').last if @options.short_names
        @project.id
      end

      # :call-seq:
      #   classpath_variables :VAR => '/path/to/location'
      # Sets classpath variables to be used for library path substitution
      # on the project.
      #
      def classpath_variables(*values)
        fail "eclipse.classpath_variables expects a single hash argument" if values.size > 1
        if values.size == 1
          fail "eclipse.classpath_variables expects a Hash argument" unless values[0].is_a? Hash
          # convert keys to strings
          values = values[0].inject({}) { |h, (k,v)| h[k.to_s] = @project.path_to(v); h }
          @variables = values.merge(@variables || {})
        end
        @variables || (@project.parent ? @project.parent.eclipse.classpath_variables : default_classpath_variables)
      end

      def default_classpath_variables
        vars = {}
        vars[:SCALA_HOME] = ENV['SCALA_HOME'] if ENV['SCALA_HOME']
        vars[:JAVA_HOME]  = ENV['JAVA_HOME']  if ENV['JAVA_HOME']
        vars
      end

      # :call-seq:
      #   natures=(natures)
      # Sets the Eclipse project natures on the project.
      #
      def natures=(var)
        @natures = arrayfy(var)
      end

      # :call-seq:
      #   natures() => [n1, n2]
      # Returns the Eclipse project natures on the project.
      # They may be derived from the parent project if no specific natures have been set
      # on the project.
      #
      # An Eclipse project nature is used internally by Eclipse to determine the aspects of a project.
      def natures(*values)
        if values.size > 0
          @natures ||= []
          @natures += values.flatten
        else
          @natures || (@project.parent ? @project.parent.eclipse.natures : [])
        end
      end

      # :call-seq:
      #   classpath_containers=(cc)
      # Sets the Eclipse project classpath containers on the project.
      #
      def classpath_containers=(var)
        @classpath_containers = arrayfy(var)
      end

      # :call-seq:
      #   classpath_containers() => [con1, con2]
      # Returns the Eclipse project classpath containers on the project.
      # They may be derived from the parent project if no specific classpath containers have been set
      # on the project.
      #
      # A classpath container is an Eclipse pre-determined ensemble of dependencies made available to
      # the project classpath.
      def classpath_containers(*values)
        if values.size > 0
          @classpath_containers ||= []
          @classpath_containers += values.flatten
        else
          @classpath_containers || (@project.parent ? @project.parent.eclipse.classpath_containers : [])
        end
      end

      # :call-seq:
      #   exclude_libs() => [lib1, lib2]
      # Returns the an array of libraries to be excluded from the generated Eclipse classpath
      def exclude_libs(*values)
        if values.size > 0
          @exclude_libs ||= []
          @exclude_libs += values.flatten
        else
          @exclude_libs || (@project.parent ? @project.parent.eclipse.exclude_libs : [])
        end
      end

      # :call-seq:
      #   exclude_libs=(lib1, lib2)
      # Sets libraries to be excluded from the generated Eclipse classpath
      #
      def exclude_libs=(libs)
        @exclude_libs = arrayfy(libs)
      end

      # :call-seq:
      #   builders=(builders)
      # Sets the Eclipse project builders on the project.
      #
      def builders=(var)
        @builders = arrayfy(var)
      end

      # :call-seq:
      #   builders() => [b1, b2]
      # Returns the Eclipse project builders on the project.
      # They may be derived from the parent project if no specific builders have been set
      # on the project.
      #
      # A builder is an Eclipse background job that parses the source code to produce built artifacts.
      def builders(*values)
        if values.size > 0
          @builders ||= []
          @builders += values.flatten
        else
          @builders || (@project.parent ? @project.parent.eclipse.builders : [])
        end
      end

      private

      def arrayfy(obj)
        obj.is_a?(Array) ? obj : [obj]
      end
    end

    class Options

      attr_writer :m2_repo_var, :short_names

      def initialize(project)
        @project = project
      end

      # The classpath variable used to point at the local maven2 repository.
      # Example:
      #   eclipse.options.m2_repo_var = 'M2_REPO'
      def m2_repo_var(*values)
        fail "m2_repo_var can only accept one value: #{values}" if values.size > 1
        if values.size > 0
          @m2_repo_var = values[0]
        else
          @m2_repo_var || (@project.parent ? @project.parent.eclipse.options.m2_repo_var : 'M2_REPO')
        end
      end

      def short_names
        @short_names || (@project.parent ? @project.parent.eclipse.options.short_names : false)
      end
    end

    def eclipse
      @eclipse ||= Eclipse.new(self)
      @eclipse
    end

    first_time do
      # Global task "eclipse" generates artifacts for all projects.
      desc 'Generate Eclipse artifacts for all projects'
      Project.local_task('eclipse'=>'artifacts') { |name|  "Generating Eclipse project for #{name}" }
    end

    before_define do |project|
      project.recursive_task('eclipse')
    end

    after_define(:eclipse => :package) do |project|
      # Need to enhance because using project.projects during load phase of the
      # buildfile has harmful side-effects on project definition order
      project.enhance do
        eclipse = project.task('eclipse')
        # We don't create the .project and .classpath files if the project contains projects.
        if project.projects.empty?

          eclipse.enhance [ file(project.path_to('.classpath')), file(project.path_to('.project')) ]

          # The only thing we need to look for is a change in the Buildfile.
          file(project.path_to('.classpath')=>Buildr.application.buildfile) do |task|
            if (project.eclipse.natures.reject { |x| x.is_a?(Symbol) }.size > 0)
              info "Writing #{task.name}"

              m2repo = Buildr::Repositories.instance.local

              File.open(task.name, 'w') do |file|
                classpathentry = ClasspathEntryWriter.new project, file
                classpathentry.write do
                  # Note: Use the test classpath since Eclipse compiles both "main" and "test" classes using the same classpath
                  cp = project.test.compile.dependencies.map(&:to_s) - [ project.compile.target.to_s, project.resources.target.to_s ]
                  cp = cp.uniq

                  # Convert classpath elements into applicable Project objects
                  cp.collect! { |path| Buildr.projects.detect { |prj| prj.packages.detect { |pkg| pkg.to_s == path } } || path }

                  # Remove excluded libs
                  cp -= project.eclipse.exclude_libs.map(&:to_s)

                  # project_libs: artifacts created by other projects
                  project_libs, others = cp.partition { |path| path.is_a?(Project) }

                  # Separate artifacts under known classpath variable paths
                  # including artifacts located in local Maven2 repository
                  vars = []
                  project.eclipse.classpath_variables.merge(project.eclipse.options.m2_repo_var => m2repo).each do |name, path|
                    matching, others = others.partition { |f| File.expand_path(f.to_s).index(path) == 0 }
                    matching.each do |m|
                      vars << [m, name, path]
                    end
                  end

                  # Generated: Any non-file classpath elements in the project are assumed to be generated
                  libs, generated = others.partition { |path| File.file?(path.to_s) }

                  classpathentry.src project.compile.sources + generated
                  classpathentry.src project.resources

                  if project.test.compile.target
                    classpathentry.src project.test.compile
                    classpathentry.src project.test.resources
                  end

                  project.eclipse.classpath_containers.each { |container|
                    classpathentry.con container
                  }

                  # Classpath elements from other projects
                  classpathentry.src_projects project_libs

                  classpathentry.output project.compile.target if project.compile.target
                  classpathentry.lib libs
                  classpathentry.var vars
                end
              end
            end
          end

          # The only thing we need to look for is a change in the Buildfile.
          file(project.path_to('.project')=>Buildr.application.buildfile) do |task|
            info "Writing #{task.name}"
            File.open(task.name, 'w') do |file|
              xml = Builder::XmlMarkup.new(:target=>file, :indent=>2)
              xml.projectDescription do
                xml.name project.eclipse.name
                xml.projects
                unless project.eclipse.builders.empty?
                  xml.buildSpec do
                    project.eclipse.builders.each { |builder|
                      xml.buildCommand do
                        xml.name builder
                      end
                    }
                  end
                end
                unless project.eclipse.natures.empty?
                  xml.natures do
                    project.eclipse.natures.each { |nature|
                      xml.nature nature unless nature.is_a? Symbol
                    }
                  end
                end
              end
            end
          end
        end
      end
    end


    # Writes 'classpathentry' tags in an xml file.
    # It converts tasks to paths.
    # It converts absolute paths to relative paths.
    # It ignores duplicate directories.
    class ClasspathEntryWriter #:nodoc:
      def initialize project, target
        @project = project
        @xml = Builder::XmlMarkup.new(:target=>target, :indent=>2)
        @excludes = [ '**/.svn/', '**/CVS/' ].join('|')
        @paths_written = []
      end

      def write &block
        @xml.classpath &block
      end

      def con path
        @xml.classpathentry :kind=>'con', :path=>path
      end

      def lib libs
        libs.map(&:to_s).sort.uniq.each do |path|
          @xml.classpathentry :kind=>'lib', :path=>relative(path)
        end
      end

      # Write a classpathentry of kind 'src'.
      # Accept an array of absolute paths or a task.
      def src arg
        if [:sources, :target].all? { |message| arg.respond_to?(message) }
          src_from_task arg
        else
          src_from_absolute_paths arg
        end
      end

      # Write a classpathentry of kind 'src' for dependent projects.
      # Accept an array of projects.
      def src_projects project_libs
        project_libs.map { |project| project.eclipse.name }.sort.uniq.each do |eclipse_name|
          @xml.classpathentry :kind=>'src', :combineaccessrules=>'false', :path=>"/#{eclipse_name}"
        end
      end

      def output target
        @xml.classpathentry :kind=>'output', :path=>relative(target)
      end

      # Write a classpathentry of kind 'var' (variable) for a library in a local repo.
      # * +libs+ is an array of library paths.
      # * +var_name+ is a variable name as defined in Eclipse (e.g., 'M2_REPO').
      # * +var_value+ is the value of this variable (e.g., '/home/me/.m2').
      # E.g., <tt>var([lib1, lib2], 'M2_REPO', '/home/me/.m2/repo')</tt>
      def var(libs)
        libs.each do |lib_path, var_name, var_value|
          lib_artifact = file(lib_path)

          attribs = { :kind => 'var', :path => lib_path }

          if lib_artifact.respond_to? :sources_artifact
            attribs[:sourcepath] = lib_artifact.sources_artifact
          end

          if lib_artifact.respond_to? :javadoc_artifact
            attribs[:javadocpath] = lib_artifact.javadoc_artifact
          end

          # make all paths relative
          attribs.each_key do |k|
            attribs[k] = attribs[k].to_s.sub(var_value, var_name.to_s) if k.to_s =~ /path/
          end

          @xml.classpathentry attribs
        end
      end

    private

      # Find a path relative to the project's root directory if possible. If the
      # two paths do not share the same root the absolute path is returned. This
      # can happen on Windows, for instance, when the two paths are not on the
      # same drive.
      def relative path
        path or raise "Invalid path '#{path.inspect}'"
        msg = [:to_path, :to_str, :to_s].find { |msg| path.respond_to? msg }
        path = path.__send__(msg)
        begin
          relative = Util.relative_path(File.expand_path(path), @project.path_to)
          if relative['..']
            # paths don't share same root
            Util.normalize_path(path)
          else
            relative
          end
        rescue ArgumentError
          Util.normalize_path(path)
        end
      end

      def src_from_task task
        src_from_absolute_paths task.sources, task.target
      end

      def src_from_absolute_paths absolute_paths, output=nil
        relative_paths = absolute_paths.map { |src| relative(src) }
        relative_paths.sort.uniq.each do |path|
          unless @paths_written.include?(path)
            attributes = { :kind=>'src', :path=>path, :excluding=>@excludes }
            attributes[:output] = relative(output) if output
            @xml.classpathentry attributes
            @paths_written << path
          end
        end
      end
    end

    module Plugin
      include Extension

      NATURE    = 'org.eclipse.pde.PluginNature'
      CONTAINER = 'org.eclipse.pde.core.requiredPlugins'
      BUILDERS   = ['org.eclipse.pde.ManifestBuilder', 'org.eclipse.pde.SchemaBuilder']

      after_define do |project|
        eclipse = project.eclipse

        # smart defaults
        if eclipse.natures.empty? && (
            (File.exists? project.path_to("plugin.xml")) ||
            (File.exists? project.path_to("OSGI-INF")) ||
            (File.exists?(project.path_to("META-INF/MANIFEST.MF")) && File.read(project.path_to("META-INF/MANIFEST.MF")).match(/^Bundle-SymbolicName:/)))
          eclipse.natures = [NATURE, Buildr::Eclipse::Java::NATURE]
          eclipse.classpath_containers = [CONTAINER, Buildr::Eclipse::Java::CONTAINER] if eclipse.classpath_containers.empty?
          eclipse.builders = BUILDERS + [Buildr::Eclipse::Java::BUILDER] if eclipse.builders.empty?
        end

        # :plugin nature explicitly set
        if eclipse.natures.include? :plugin
          unless eclipse.natures.include? NATURE
            # plugin nature must be before java nature
            eclipse.natures += [Buildr::Eclipse::Java::NATURE] unless eclipse.natures.include? Buildr::Eclipse::Java::NATURE
            index = eclipse.natures.index(Buildr::Eclipse::Java::NATURE) || -1
            eclipse.natures = eclipse.natures.insert(index, NATURE)
          end
          unless eclipse.classpath_containers.include? CONTAINER
            # plugin container must be before java container
            index = eclipse.classpath_containers.index(Buildr::Eclipse::Java::CONTAINER) || -1
            eclipse.classpath_containers = eclipse.classpath_containers.insert(index, CONTAINER)
          end
          unless (eclipse.builders.include?(BUILDERS[0]) && eclipse.builders.include?(BUILDERS[1]))
            # plugin builder must be before java builder
            index = eclipse.classpath_containers.index(Buildr::Eclipse::Java::BUILDER) || -1
            eclipse.builders = eclipse.builders.insert(index, BUILDERS[1]) unless eclipse.builders.include? BUILDERS[1]
            index = eclipse.classpath_containers.index(BUILDERS[1]) || -1
            eclipse.builders = eclipse.builders.insert(index, BUILDERS[0]) unless eclipse.builders.include? BUILDERS[0]
          end
        end
      end
    end

    module Scala
      include Extension

      NATURE    = 'org.scala-ide.sdt.core.scalanature'
      CONTAINER = 'org.scala-ide.sdt.launching.SCALA_CONTAINER'
      BUILDER   = 'org.scala-ide.sdt.core.scalabuilder'

      after_define :eclipse => :eclipse_scala
      after_define :eclipse_scala do |project|
        eclipse = project.eclipse
        # smart defaults
        if eclipse.natures.empty? && (project.compile.language == :scala || project.test.compile.language == :scala)
          eclipse.natures = [NATURE, Buildr::Eclipse::Java::NATURE]
          eclipse.classpath_containers = [CONTAINER, Buildr::Eclipse::Java::CONTAINER] if eclipse.classpath_containers.empty?
          eclipse.builders = BUILDER if eclipse.builders.empty?
          eclipse.exclude_libs += Buildr::Scala::Scalac.dependencies
        end

        # :scala nature explicitly set
        if eclipse.natures.include? :scala
          unless eclipse.natures.include? NATURE
            # scala nature must be before java nature
            eclipse.natures += [Buildr::Eclipse::Java::NATURE] unless eclipse.natures.include? Buildr::Eclipse::Java::NATURE
            index = eclipse.natures.index(Buildr::Eclipse::Java::NATURE) || -1
            eclipse.natures = eclipse.natures.insert(index, NATURE)
          end
          unless eclipse.classpath_containers.include? CONTAINER
            # scala container must be before java container
            index = eclipse.classpath_containers.index(Buildr::Eclipse::Java::CONTAINER) || -1
            eclipse.classpath_containers = eclipse.classpath_containers.insert(index, CONTAINER)
          end
          unless eclipse.builders.include? BUILDER
            # scala builder overrides java builder
            eclipse.builders -= [Buildr::Eclipse::Java::BUILDER]
            eclipse.builders += [BUILDER]
          end
          eclipse.exclude_libs += Buildr::Scala::Scalac.dependencies
        end
      end
    end

    module Java
      include Extension

      NATURE    = 'org.eclipse.jdt.core.javanature'
      CONTAINER = 'org.eclipse.jdt.launching.JRE_CONTAINER'
      BUILDER    = 'org.eclipse.jdt.core.javabuilder'

      after_define do |project|
        eclipse = project.eclipse

        # smart defaults
        if project.compile.language == :java || project.test.compile.language == :java
          eclipse.natures = NATURE if eclipse.natures.empty?
          eclipse.classpath_containers = CONTAINER if eclipse.classpath_containers.empty?
          eclipse.builders = BUILDER if eclipse.builders.empty?
        end

        # :java nature explicitly set
        if eclipse.natures.include? :java
          eclipse.natures += [NATURE] unless eclipse.natures.include? NATURE
          eclipse.classpath_containers += [CONTAINER] unless eclipse.classpath_containers.include? CONTAINER
          eclipse.builders += [BUILDER] unless eclipse.builders.include? BUILDER
        end
      end
    end

  end

end # module Buildr

class Buildr::Project
  include Buildr::Eclipse
  include Buildr::Eclipse::Plugin
  include Buildr::Eclipse::Scala
  include Buildr::Eclipse::Java
end
