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

# The Scala Module
module Buildr::Scala
  DEFAULT_VERSION = '2.11.8'

  class << self

    def version_str
      warn 'Use of Scala.version_str is deprecated. Use Scala.version instead'
      version
    end

    def installed_version
      unless @installed_version
        @installed_version = if Scalac.installed?
          begin
            # try to read the value from the properties file
            props = Zip::File.open(File.expand_path('lib/scala-library.jar', Scalac.scala_home)) do |zipfile|
              zipfile.read 'library.properties'
            end

            version_str = props.match(/version\.number\s*=\s*([^\s]+)/).to_a[1]

            if version_str
              md = version_str.match(/\d+\.\d[\d\.]*/) or
                fail "Unable to parse Scala version: #{version_str}"

              md[0].sub(/.$/, "") # remove trailing dot, if any
            end
          rescue => e
            warn "Unable to parse library.properties in $SCALA_HOME/lib/scala-library.jar: #{e}"
            nil
          end
        end
      end

      @installed_version
    end

    def version
      Buildr.settings.build['scala.version'] || installed_version || DEFAULT_VERSION
    end

    # check if version matches any of the given prefixes
    def version?(*v)
      v.any? { |v| version.index(v.to_s) == 0 }
    end

    # returns Scala version without build number.
    # e.g.  "2.9.0-1" => "2.9.0"
    def version_without_build
      version.split('-')[0]
    end

    # returns Scala version without tiny number.
    # e.g.  "2.11.8" => "2.11"
    def version_major_minor
      version.split('.')[0..1].join('.')
    end
  end

  # Scalac compiler:
  #   compile.using(:scalac)
  # Used by default if .scala files are found in the src/main/scala directory (or src/test/scala)
  # and sets the target directory to target/classes (or target/test/classes).
  #
  # Accepts the following options:
  # * :warnings    -- Generate warnings if true (opposite of -nowarn).
  # * :deprecation -- Output source locations where deprecated APIs are used.
  # * :optimise    -- Generates faster bytecode by applying optimisations to the program.
  # * :target      -- Class file compatibility with specified release.
  # * :debug       -- Generate debugging info.
  # * :other       -- Array of options to pass to the Scalac compiler as is, e.g. -Xprint-types
  class Scalac < Buildr::Compiler::Base

    DEFAULT_ZINC_VERSION  = '0.3.12'
    DEFAULT_SBT_VERSION   = '0.13.12'
    DEFAULT_JLINE_VERSION = '2.14.2'
    DEFAULT_SCALAMAIN_VERSION = '1.0.3'

    class << self
      def scala_home
        env_home = ENV['SCALA_HOME']

        @home ||= (if !env_home.nil? && File.exists?(env_home + '/lib/scala-library.jar') && File.exists?(env_home + '/lib/scala-compiler.jar')
          env_home
        else
          nil
        end)
      end

      def installed?
        !scala_home.nil?
      end

      def use_installed?
        if installed? && Buildr.settings.build['scala.version']
          Buildr.settings.build['scala.version'] == Scala.installed_version
        else
          Buildr.settings.build['scala.version'].nil? && installed?
        end
      end

      def dependencies
        scala_dependencies = if use_installed?
          %w(scala-library scala-compiler).map { |s| File.expand_path("lib/#{s}.jar", scala_home) }
        else
          REQUIRES.artifacts.map(&:to_s)
        end

        zinc_dependencies = ZINC_REQUIRES.artifacts.map(&:to_s)

        (scala_dependencies + zinc_dependencies).compact
      end

      def use_fsc
        use_installed? && ENV['USE_FSC'] =~ /^(yes|on|true)$/i
      end

      def applies_to?(project, task) #:nodoc:
        paths = task.sources + [sources].flatten.map { |src| Array(project.path_to(:source, task.usage, src.to_sym)) }
        paths.flatten!

        # Just select if we find .scala files
        paths.any? { |path| !Dir["#{path}/**/*.scala"].empty? }
      end
    end

    # The scalac compiler jars are added to classpath at load time,
    # if you want to customize artifact versions, you must set them on the
    #
    #      artifact_ns['Buildr::Compiler::Scalac'].library = '2.7.5'
    #
    # namespace before this file is required.  This is of course, only
    # if SCALA_HOME is not set or invalid.
    REQUIRES = ArtifactNamespace.for(self) do |ns|
      version = Buildr.settings.build['scala.version'] || DEFAULT_VERSION
      ns.library!      'org.scala-lang:scala-library:jar:>=' + version
      ns.compiler!     'org.scala-lang:scala-compiler:jar:>=' + version
      unless Buildr::Scala.version?(2.7, 2.8, 2.9)
        # added in Scala 2.10
        ns.reflect!      'org.scala-lang:scala-reflect:jar:>=' + version
        ns.actors!       'org.scala-lang:scala-actors:jar:>=' + version
        ns.xml!          'org.scala-lang.modules:scala-xml_2.11:jar:1.0.5'
        ns.parser_combinators! 'org.scala-lang.modules:scala-parser-combinators_2.11:jar:1.0.4'

      end
    end

    ZINC_REQUIRES = ArtifactNamespace.for(self) do |ns|
      zinc_version  = Buildr.settings.build['zinc.version']  || DEFAULT_ZINC_VERSION
      sbt_version   = Buildr.settings.build['sbt.version']   || DEFAULT_SBT_VERSION
      jline_version = Buildr.settings.build['jline.version'] || DEFAULT_JLINE_VERSION
      scalamain_version = Buildr.settings.build['scalamain.version'] || DEFAULT_SCALAMAIN_VERSION
      ns.zinc!          "com.typesafe.zinc:zinc:jar:>=#{zinc_version}"
      ns.sbt_interface! "com.typesafe.sbt:sbt-interface:jar:>=#{sbt_version}"
      ns.incremental!   "com.typesafe.sbt:incremental-compiler:jar:>=#{sbt_version}"
      ns.compiler_interface_sources! "com.typesafe.sbt:compiler-interface:jar:sources:>=#{sbt_version}"
      ns.jline!        "jline:jline:jar:>=#{jline_version}"
      ns.scalamain!    "io.tmio:scalamain:jar:>=#{scalamain_version}"
    end

    Javac = Buildr::Compiler::Javac

    OPTIONS = [:warnings, :deprecation, :optimise, :target, :debug, :other, :javac]

    # Lazy evaluation to allow change in buildfile
    Java.classpath << lambda { dependencies }

    specify :language=>:scala, :sources => [:scala, :java], :source_ext => [:scala, :java],
            :target=>'classes', :target_ext=>'class', :packaging=>:jar

    def initialize(project, options) #:nodoc:
      super
      # use common options also for javac

      options[:javac] ||= Buildr::Compiler::Javac::OPTIONS.inject({}) do |hash, option|
        hash[option] = options[option]
        hash
      end
      options[:debug] = Buildr.options.debug if options[:debug].nil?
      options[:warnings] = verbose if options[:warnings].nil?
      options[:deprecation] ||= false
      options[:optimise] ||= false
      options[:make] ||= :transitivenocp if Scala.version? 2.8
      @java = Javac.new(project, options[:javac])
    end

    def compile(sources, target, dependencies) #:nodoc:
      if zinc?
        compile_with_zinc(sources, target, dependencies)
      else
        compile_with_scalac(sources, target, dependencies)
      end
    end

    def compile_with_scalac(sources, target, dependencies) #:nodoc:
      check_options(options, OPTIONS + (Scala.version?(2.8) ? [:make] : []))

      java_sources = java_sources(sources)
      enable_dep_tracing = Scala.version?(2.8) && java_sources.empty?

      dependencies.unshift target if enable_dep_tracing

      cmd_args = []
      cmd_args << '-classpath' << dependencies.join(File::PATH_SEPARATOR)
      source_paths = sources.select { |source| File.directory?(source) }
      cmd_args << '-sourcepath' << source_paths.join(File::PATH_SEPARATOR) unless source_paths.empty?
      cmd_args << '-d' << File.expand_path(target)
      cmd_args += scalac_args

      if enable_dep_tracing
        dep_dir = File.expand_path(target)
        Dir.mkdir dep_dir unless File.exists? dep_dir

        cmd_args << '-make:' + options[:make].to_s
        cmd_args << '-dependencyfile'
        cmd_args << File.expand_path('.scala-deps', dep_dir)
      end

      cmd_args += files_from_sources(sources)

      unless Buildr.application.options.dryrun
        trace((['scalac'] + cmd_args).join(' '))

        if Scalac.use_fsc
          system(([File.expand_path('bin/fsc', Scalac.scala_home)] + cmd_args).join(' ')) or
            fail 'Failed to compile, see errors above'
        else
          Java.load
          begin
            Java.scala.tools.nsc.Main.process(cmd_args.to_java(Java.java.lang.String))
          rescue => e
            fail "Scala compiler crashed:\n#{e.inspect}"
          end
          fail 'Failed to compile, see errors above' if Java.scala.tools.nsc.Main.reporter.hasErrors
        end

        unless java_sources.empty?
          trace 'Compiling mixed Java/Scala sources'

          # TODO  includes scala-compiler.jar
          deps = dependencies + Scalac.dependencies + [ File.expand_path(target) ]
          @java.compile(java_sources, target, deps)
        end
      end
    end

    def compile_with_zinc(sources, target, dependencies) #:nodoc:

      dependencies.unshift target

      cmd_args = []
      cmd_args << '-sbt-interface' << REQUIRES.sbt_interface.artifact
      cmd_args << '-compiler-interface' << REQUIRES.compiler_interface_sources.artifact
      cmd_args << '-scala-library' << dependencies.find { |d| d =~ /scala-library/ }
      cmd_args << '-scala-compiler' << dependencies.find { |d| d =~ /scala-compiler/ }
      cmd_args << '-scala-extra' << dependencies.find { |d| d =~ /scala-reflect/ }
      cmd_args << '-classpath' << (dependencies + [ File.join(File.dirname(__FILE__)) ]).join(File::PATH_SEPARATOR)
      source_paths = sources.select { |source| File.directory?(source) }
      cmd_args << '-Ssourcepath' << ("-S" + source_paths.join(File::PATH_SEPARATOR)) unless source_paths.empty?
      cmd_args << '-d' << File.expand_path(target)
      cmd_args += scalac_args
      cmd_args << '-debug' if trace?(:scalac)

      cmd_args.map!(&:to_s)

      cmd_args += files_from_sources(sources)

      unless Buildr.application.options.dryrun
        trace((%w(io.tmio.scalamain.Main com.typesafe.zinc.Main main) + cmd_args).join(' '))
        begin
          Java::Commands.java 'io.tmio.scalamain.Main', *(%w(com.typesafe.zinc.Main main) + cmd_args + [{:classpath => Scalac.dependencies + [File.join(File.dirname(__FILE__)) ]}])
        rescue => e
          fail "Zinc compiler crashed:\n#{e.inspect}\n#{e.backtrace.join("\n")}"
        end
      end
    end

  protected

    # :nodoc: see Compiler:Base
    def compile_map(sources, target)
      target_ext = self.class.target_ext
      ext_glob = Array(self.class.source_ext).join(',')
      sources.flatten.map{|f| File.expand_path(f)}.inject({}) do |map, source|
        sources = if File.directory?(source)
          FileList["#{source}/**/*.{#{ext_glob}}"].reject { |file| File.directory?(file) }
        else
          [source]
        end

        sources.each do |source|
          # try to extract package name from .java or .scala files
          if %w(.java .scala).include? File.extname(source)
            name = File.basename(source).split(".")[0]
            package = findFirst(source, /^\s*package\s+([^\s;]+)\s*;?\s*/)
            packages = count(source, /^\s*package\s+([^\s;]+)\s*;?\s*/)
            found = findFirst(source, /((trait)|(class)|(object))\s+(#{name})/)

            # if there's only one package statement and we know the target name, then we can depend
            # directly on a specific file, otherwise, we depend on the general target
            if (found && packages == 1)
              map[source] = package ? File.join(target, package[1].gsub('.', '/'), name.ext(target_ext)) : target
            else
              map[source] = target
            end

          elsif
            map[source] = target
          end
        end

        map.each do |key,value|
          map[key] = first_file unless map[key]
        end

        map
      end
    end

  private

    def zinc?
      (options[:incremental] || @project.scalac_options.incremental || (Buildr.settings.build['scalac.incremental'].to_s == "true"))
    end

    def count(file, pattern)
      count = 0
      File.open(file, 'r') do |infile|
        while (line = infile.gets)
          count += 1 if line.match(pattern)
        end
      end
      count
    end

    def java_sources(sources)
      sources.flatten.map { |source| File.directory?(source) ? FileList["#{source}/**/*.java"] : source } .
        flatten.reject { |file| File.directory?(file) || File.extname(file) != '.java' }.map { |file| File.expand_path(file) }.uniq
    end

    # Returns Scalac command line arguments from the set of options.
    def scalac_args #:nodoc:
      args = []
      args << '-nowarn' unless options[:warnings]
      args << '-verbose' if trace?(:scalac)
      if !!options[:debug]
        args << (Scala.version?(2.7, 2.8) ? '-g' : '-g:vars')
      elsif options[:debug]
        args << "-g:#{options[:debug]}"
      end
      args << '-deprecation' if options[:deprecation]
      args << '-optimise' if options[:optimise]
      args << '-target:jvm-' + options[:target].to_s if options[:target]
      args += Array(options[:other])
      if zinc?
        args.map { |arg| '-S' + arg } + Array(options[:zinc_options])
      else
        args
      end
    end
  end

  module ProjectExtension
    def scalac_options
      @scalac ||= ScalacOptions.new(self)
    end
  end

  class ScalacOptions
    attr_writer :incremental

    def initialize(project)
      @project = project
    end

    def incremental
      @incremental || (@project.parent ? @project.parent.scalac_options.incremental : nil)
    end
  end
end

# Scala compiler comes first, ahead of Javac, this allows it to pick
# projects that mix Scala and Java code by spotting Scala code first.
Buildr::Compiler.compilers.unshift Buildr::Scala::Scalac

class Buildr::Project #:nodoc:
  include Buildr::Scala::ProjectExtension
end
