# 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 Kotlin Module
module Buildr::Kotlin
  DEFAULT_VERSION = '1.1.3-2'

  class << self

    def installed_version
      unless @installed_version
        @installed_version = if Kotlinc.installed?
          begin
            # try to read the value from the build.txt file
            version_str = File.read(File.expand_path('build.txt', Kotlinc.kotlin_home))

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

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

      @installed_version
    end

    def version
      Buildr.settings.build['kotlin.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

  end

  # Kotlin compiler:
  #   compile.using(:kotlin)
  # Used by default if .kt files are found in the src/main/kotlin directory (or src/test/kotlin)
  # and sets the target directory to target/classes (or target/test/classes).
  # Accepts the following options:
  # * :warnings    -- Issue warnings when compiling.  True when running in verbose mode.
  # * :debug       -- Generates bytecode with debugging information.  Set from the debug
  # environment variable/global option.
  # * :optimize    -- Optimize the byte code generation. False by default.
  # * :target      -- Bytecode compatibility.
  # * :noStdlib    -- Include the Kotlin runtime. False by default.
  # * :javac       -- Arguments for javac compiler.
  class Kotlinc < Buildr::Compiler::Base

    class << self
      def kotlin_home
        env_home = ENV['KOTLIN_HOME']

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

      def installed?
        !kotlin_home.nil?
      end

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

      def dependencies
        kotlin_dependencies = if use_installed?
          %w(kotlin-stdlib kotlin-compiler).map { |s| File.expand_path("lib/#{s}.jar", kotlin_home) }
        else
          REQUIRES.artifacts.map(&:to_s)
        end
        # Add Java utilities (eg KotlinMessageCollector)
        kotlin_dependencies |= [ File.join(File.dirname(__FILE__)) ]
        (kotlin_dependencies).compact
      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 .kt files
        paths.any? { |path| !Dir["#{path}/**/*.kt"].empty? }
      end
    end

    # The kotlin 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::Kotlinc'].library = '1.1.3-2'
    #
    # namespace before this file is required.  This is of course, only
    # if KOTLIN_HOME is not set or invalid.
    REQUIRES = ArtifactNamespace.for(self) do |ns|
      version = Buildr.settings.build['kotlin.version'] || DEFAULT_VERSION
      ns.compiler!     'org.jetbrains.kotlin:kotlin-compiler:jar:>=' + version
    end

    Javac = Buildr::Compiler::Javac

    OPTIONS = [:warnings, :optimize, :target, :debug, :noStdlib, :javac]

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

    specify :language=>:kotlin, :sources => [:kotlin, :java], :source_ext => [:kt, :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 || trace?(:kotlinc) if options[:debug].nil?
      options[:warnings] = verbose if options[:warnings].nil?
      options[:optimize] = false if options[:optimize].nil?
      options[:noStdlib] = true if options[:noStdlib].nil?
      @java = Javac.new(project, options[:javac])
    end



    def compile(sources, target, dependencies) #:nodoc:
      check_options(options, OPTIONS)

      java_sources = java_sources(sources)

      unless Buildr.application.options.dryrun
        messageCollector = Java.org.apache.buildr.KotlinMessageCollector.new

        Java.load
        begin
          compiler = Java.org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.new
          compilerArguments = kotlinc_args
          compilerArguments.destination = File.expand_path(target)
          compilerArguments.classpath = dependencies.join(File::PATH_SEPARATOR)
          sources.each do |source|
            compilerArguments.freeArgs.add(File.expand_path(source))
          end
          services = Buildr::Util.java_platform? ? Java.org.jetbrains.kotlin.config.Services::EMPTY : Java.org.jetbrains.kotlin.config.Services.EMPTY
          compiler.exec(messageCollector, services, compilerArguments)
        rescue => e
          fail "Kotlin compiler crashed:\n#{e.inspect}"
        end

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

          deps = dependencies + Kotlinc.dependencies + [ File.expand_path(target) ]
          @java.compile(java_sources, target, deps)
        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 .kt files
          if %w(.java .kt).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, /((class)|(object))\s+(#{name})Kt/)

            # 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 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 Kotlinc arguments from the set of options.
    def kotlinc_args #:nodoc:
      compilerArguments = Java.org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments.new
      compilerArguments.verbose = options[:debug]
      compilerArguments.suppressWarnings = !options[:warnings]
      compilerArguments.noStdlib = options[:noStdlib]
      compilerArguments.noOptimize = !options[:optimize]
      compilerArguments.reportOutputFiles = compilerArguments.verbose
      compilerArguments.jvmTarget = options[:target] unless options[:target].nil?
      compilerArguments
    end
  end

  module ProjectExtension
    def kotlinc_options
      @kotlinc ||= KotlincOptions.new(self)
    end
  end

  class KotlincOptions
    attr_writer :incremental

    def initialize(project)
      @project = project
    end

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

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

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