# 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.

# Base module for all things Java.
module Java

  # JDK commands: java, javac, javadoc, etc.
  module Commands

    class << self

      # :call-seq:
      #   java(class, *args, options?)
      #
      # Runs Java with the specified arguments.
      #
      # Each argument should be provided as separate array value, e.g.
      #
      #  java("-jar", "yuicompressor-2.4.2.jar", "--type","css",
      #       "src/main/webapp/styles/styles-all.css",
      #       "-o", "src/main/webapp/styles/styles-all-min.css")
      #
      # The last argument may be a Hash with additional options:
      # * :dir -- The working directory from which to execute task..
      # * :classpath -- One or more file names, tasks or artifact specifications.
      #   These are all expanded into artifacts, and all tasks are invoked.
      # * :java_args -- Any additional arguments to pass (e.g. -hotspot, -xms)
      # * :properties -- Hash of system properties (e.g. 'path'=>base_dir).
      # * :name -- Shows this name, otherwise shows the first argument (the class name).
      # * :verbose -- If true, prints the command and all its argument.
      # * :pathing_jar -- If true, forces the use of a "pathing" jar, false disables. Nil
      #                   will default to using a "pathing" jar under windows with long classpaths.
      #                   See http://stackoverflow.com/questions/201816/how-to-set-a-long-java-classpath-in-msdos-windows
      def java(*args, &block)
        options = Hash === args.last ? args.pop : {}
        options[:verbose] ||= trace?(:java)
        rake_check_options options, :classpath, :java_args, :properties, :name, :verbose, :dir, :pathing_jar

        name = options[:name]
        if name.nil?
          name = "java #{args.first}"
        end

        cmd_args = []
        if options[:dir]
          pwd = options[:dir]
          if Buildr::Util.win_os?
            # Ruby uses forward slashes regardless of platform,
            # unfortunately cd c:/some/path fails on Windows
            cmd_args << "cd /d \"#{pwd.gsub(%r{/}, '\\')}\" && "
          else
            cmd_args << "cd '#{pwd}' && "
          end
        end
        cmd_args << path_to_bin('java')
        cp = classpath_from(options)

        unless cp.empty?
          if options[:pathing_jar] == true || (options[:pathing_jar].nil? && Util.win_os? && cp.join(':').size > 2048)
            paths = cp.map do |c|
              path = File.directory?(c) && !c.end_with?('/') ? "#{c}/" : c.to_s
              Buildr::Util.win_os? ? "/#{path}" : path
            end
            manifest = Buildr::Packaging::Java::Manifest.new([{'Class-Path' => paths.map{|p| URI.encode(p)}.join(" ")}])
            tjar = Tempfile.new(['javacmd', '.jar'])
            Zip::OutputStream.open(tjar.path) do |zos|
              zos.put_next_entry('META-INF/MANIFEST.MF')
              zos.write manifest.to_s
              zos.write "\n"
            end
            tjar.close

            cmd_args << '-classpath' << tjar.path
          else
            cmd_args << '-classpath' << cp.join(File::PATH_SEPARATOR)
          end
        end
        options[:properties].each { |k, v| cmd_args << "-D#{k}=#{v}" } if options[:properties]
        cmd_args += (options[:java_args] || (ENV['JAVA_OPTS'] || ENV['JAVA_OPTIONS']).to_s.split).flatten
        cmd_args += args.flatten.compact

        tmp = nil
        begin
            # Windows can't handle cmd lines greater than 2048/8192 chars.
            # If our cmd line is longer, we create a batch file and execute it instead.
          if Util.win_os? &&  cmd_args.map(&:inspect).join(' ').size > 2048
            # remove '-classpath' and the classpath itself from the cmd line.
            cp_i = cmd_args.index{|x| x.to_s =~ /^-classpath/}
            2.times do
              cmd_args.delete_at cp_i unless cp_i.nil?
            end
            # create tmp batch file.
            tmp = Tempfile.new(['starter', '.bat'])
            tmp.write "@echo off\n"
            tmp.write "SET CLASSPATH=#{cp.join(File::PATH_SEPARATOR).gsub(%r{/}, '\\')}\n"
            tmp.write cmd_args.map(&:inspect).join(' ')
            tmp.close
            # set new cmd line.
            cmd_args = [tmp.path]
          end

          unless Buildr.application.options.dryrun
            info "Running #{name}" if name && options[:verbose]
            block = lambda { |ok, res| fail "Failed to execute #{name}, see errors above" unless ok } unless block
            cmd_args = cmd_args.map(&:inspect).join(' ') if Util.win_os?
            sh(*cmd_args) do |ok, ps|
              block.call ok, ps
            end
          end
        ensure
          unless tmp.nil?
            tmp.close
            tmp.unlink
          end
        end
      end

      # :call-seq:
      #   apt(*files, options)
      #
      # Runs Apt with the specified arguments.
      #
      # The last argument may be a Hash with additional options:
      # * :compile -- If true, compile source files to class files.
      # * :source -- Specifies source compatibility with a given JVM release.
      # * :output -- Directory where to place the generated source files, or the
      #   generated class files when compiling.
      # * :classpath -- One or more file names, tasks or artifact specifications.
      #   These are all expanded into artifacts, and all tasks are invoked.
      def apt(*args)
        options = Hash === args.last ? args.pop : {}
        rake_check_options options, :compile, :source, :output, :classpath

        files = args.flatten.map(&:to_s).
          collect { |arg| File.directory?(arg) ? FileList["#{arg}/**/*.java"] : arg }.flatten
        cmd_args = [ trace?(:apt) ? '-verbose' : '-nowarn' ]
        if options[:compile]
          cmd_args << '-d' << options[:output].to_s
        else
          cmd_args << '-nocompile' << '-s' << options[:output].to_s
        end
        cmd_args << '-source' << options[:source] if options[:source]
        cp = classpath_from(options)
        cmd_args << '-classpath' << cp.join(File::PATH_SEPARATOR) unless cp.empty?
        cmd_args += files
        unless Buildr.application.options.dryrun
          info 'Running apt'
          trace (['apt'] + cmd_args).join(' ')
          Java.load
          ::Java::com.sun.tools.apt.Main.process(cmd_args.to_java(::Java::java.lang.String)) == 0 or
            fail 'Failed to process annotations, see errors above'
        end
      end

      # :call-seq:
      #   javac(*files, options)
      #
      # Runs Javac with the specified arguments.
      #
      # The last argument may be a Hash with additional options:
      # * :output -- Target directory for all compiled class files.
      # * :classpath -- One or more file names, tasks or artifact specifications.
      #   These are all expanded into artifacts, and all tasks are invoked.
      # * :sourcepath -- Additional source paths to use.
      # * :javac_args -- Any additional arguments to pass (e.g. -extdirs, -encoding)
      # * :name -- Shows this name, otherwise shows the working directory.
      def javac(*args)
        options = Hash === args.last ? args.pop : {}
        rake_check_options options, :classpath, :sourcepath, :output, :javac_args, :name

        files = args.flatten.each { |f| f.invoke if f.respond_to?(:invoke) }.map(&:to_s).
          collect { |arg| File.directory?(arg) ? FileList["#{File.expand_path(arg)}/**/*.java"] : File.expand_path(arg) }.flatten
        name = options[:name] || Dir.pwd

        cmd_args = []
        cp = classpath_from(options)
        cmd_args << '-classpath' << cp.join(File::PATH_SEPARATOR) unless cp.empty?
        cmd_args << '-sourcepath' << [options[:sourcepath]].flatten.join(File::PATH_SEPARATOR) if options[:sourcepath]
        cmd_args << '-d' << File.expand_path(options[:output].to_s) if options[:output]
        cmd_args += options[:javac_args].flatten if options[:javac_args]
        cmd_args += files
        unless Buildr.application.options.dryrun
          mkdir_p options[:output] if options[:output]
          info "Compiling #{files.size} source files in #{name}"
          trace (['javac'] + cmd_args).join(' ')
          Java.load
          ::Java::com.sun.tools.javac.Main.compile(cmd_args.to_java(::Java::java.lang.String)) == 0 or
            fail 'Failed to compile, see errors above'
        end
      end

      # :call-seq:
      #   javadoc(*files, options)
      #
      # Runs Javadocs with the specified files and options.
      #
      # This method accepts the following special options:
      # * :output -- The output directory
      # * :classpath -- Array of classpath dependencies.
      # * :sourcepath -- Array of sourcepaths (paths or tasks).
      # * :name -- Shows this name, otherwise shows the working directory.
      #
      # All other options are passed to Javadoc as following:
      # * true -- As is, for example, :author=>true becomes -author
      # * false -- Prefixed, for example, :index=>false becomes -noindex
      # * string -- Option with value, for example, :windowtitle=>'My project' becomes -windowtitle "My project"
      # * array -- Option with set of values separated by spaces.
      def javadoc(*args)
        options = Hash === args.last ? args.pop : {}
        fail "No output defined for javadoc" if options[:output].nil?
        options[:output] = File.expand_path(options[:output].to_s)
        cmd_args = [ '-d', options[:output], trace?(:javadoc) ? '-verbose' : '-quiet' ]
        options.reject { |key, value| [:output, :name, :sourcepath, :classpath].include?(key) }.
          each { |key, value| value.invoke if value.respond_to?(:invoke) }.
          each do |key, value|
            case value
            when true, nil
              cmd_args << "-#{key}"
            when false
              cmd_args << "-no#{key}"
            when Hash
              value.each { |k,v| cmd_args << "-#{key}" << k.to_s << v.to_s }
            else
              cmd_args += Array(value).map { |item| ["-#{key}", item.to_s] }.flatten
            end
          end
        [:sourcepath, :classpath].each do |option|
          options[option].to_a.flatten.tap do |paths|
            cmd_args << "-#{option}" << paths.flatten.map(&:to_s).join(File::PATH_SEPARATOR) unless paths.empty?
          end
        end
        files = args.each {|arg| arg.invoke if arg.respond_to?(:invoke)}.collect {|arg| arg.is_a?(Project) ? arg.compile.sources.collect{|dir| Dir["#{File.expand_path(dir.to_s)}/**/*.java"]} : File.expand_path(arg.to_s) }
        cmd_args += files.flatten.uniq.map(&:to_s)
        name = options[:name] || Dir.pwd
        unless Buildr.application.options.dryrun
          info "Generating Javadoc for #{name}"
          trace (['javadoc'] + cmd_args).join(' ')
          Java.load
          ::Java::com.sun.tools.javadoc.Main.execute(cmd_args.to_java(::Java::java.lang.String)) == 0 or
            fail 'Failed to generate Javadocs, see errors above'
        end
      end

    protected

      # :call-seq:
      #   path_to_bin(cmd?) => path
      #
      # Returns the path to the specified Java command (with no argument to java itself).
      def path_to_bin(name = nil)
        home = ENV['JAVA_HOME'] or fail 'Are we forgetting something? JAVA_HOME not set.'
        bin = Util.normalize_path(File.join(home, 'bin'))
        fail 'JAVA_HOME environment variable does not point to a valid JRE/JDK installation.' unless File.exist? bin
        Util.normalize_path(File.join(bin, name.to_s))
      end

      # :call-seq:
      #    classpath_from(options) => files
      #
      # Extracts the classpath from the options, expands it by calling artifacts, invokes
      # each of the artifacts and returns an array of paths.
      def classpath_from(options)
        Buildr.artifacts(options[:classpath] || []).map(&:to_s).
          map { |t| task(t).invoke; File.expand_path(t) }
      end

    end

  end

end
