# encoding: utf-8

# Copyright (c) 2012, HipByte SPRL and contributors
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

require 'motion/project/app'

App = Motion::Project::App
App.template = :android

require 'motion/project'
require 'motion/project/template/android/config'

desc "Create an application package file (.apk)"
task :build do
  # Prepare build dir.
  app_build_dir = App.config.versionized_build_dir
  mkdir_p app_build_dir

  # Compile Ruby files.
  ruby = App.config.bin_exec('ruby')
  init_func_n = 0
  ruby_objs = []
  bs_files = Dir.glob(File.join(App.config.versioned_datadir, 'BridgeSupport/*.bridgesupport'))
  bs_files += App.config.vendored_bs_files
  ruby_bs_flags = bs_files.map { |x| "--uses-bs \"#{x}\"" }.join(' ')
  objs_build_dir = File.join(app_build_dir, 'obj', 'local', App.config.armeabi_directory_name)
  kernel_bc = File.join(App.config.versioned_arch_datadir, "kernel-#{App.config.arch}.bc")
  ruby_objs_changed = false
  App.config.files.each do |ruby_path|
    bc_path = File.join(objs_build_dir, ruby_path + '.bc')
    init_func = "InitRubyFile#{init_func_n += 1}"
    if !File.exist?(bc_path) \
        or File.mtime(ruby_path) > File.mtime(bc_path) \
        or File.mtime(ruby) > File.mtime(bc_path) \
        or File.mtime(kernel_bc) > File.mtime(bc_path)
      App.info 'Compile', ruby_path
      FileUtils.mkdir_p(File.dirname(bc_path))
      sh "VM_PLATFORM=android VM_KERNEL_PATH=\"#{kernel_bc}\" arch -i386 \"#{ruby}\" #{ruby_bs_flags} --emit-llvm \"#{bc_path}\" #{init_func} \"#{ruby_path}\""
      ruby_objs_changed = true
    end
    ruby_objs << [bc_path, init_func]
  end

  # Generate payload main file.
  payload_c_txt = <<EOS
// This file has been generated. Do not modify by hands.
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <jni.h>
#include <assert.h>
#include <android/log.h>
EOS
  ruby_objs.each do |_, init_func|
    payload_c_txt << "void #{init_func}(void *rcv, void *sel);\n" 
  end
  payload_c_txt << <<EOS
void rb_vm_register_native_methods(void);
bool rb_vm_init(const char *app_package, JNIEnv *env);
jint
JNI_OnLoad(JavaVM *vm, void *reserved)
{
    __android_log_write(ANDROID_LOG_DEBUG, "#{App.config.package_path}", "Loading payload");
    JNIEnv *env = NULL;
    if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) {
	return -1;
    }
    assert(env != NULL);
    rb_vm_init("#{App.config.package_path}", env);
EOS
  ruby_objs.each do |_, init_func|
    payload_c_txt << "    (*env)->PushLocalFrame(env, 32);\n"
    payload_c_txt << "    #{init_func}(NULL, NULL);\n"
    payload_c_txt << "    (*env)->PopLocalFrame(env, NULL);\n"
  end
  payload_c_txt << <<EOS
    rb_vm_register_native_methods();
    __android_log_write(ANDROID_LOG_DEBUG, "#{App.config.package_path}", "Loaded payload");
    return JNI_VERSION_1_6;
}
EOS
  payload_c = File.join(app_build_dir, 'jni/payload.c')
  mkdir_p File.dirname(payload_c)
  if !File.exist?(payload_c) or File.read(payload_c) != payload_c_txt
    File.open(payload_c, 'w') { |io| io.write(payload_c_txt) }
  end

  # Compile and link payload library.
  libs_abi_subpath = "lib/#{App.config.armeabi_directory_name}"
  libpayload_subpath = "#{libs_abi_subpath}/#{App.config.payload_library_filename}"
  libpayload_path = "#{app_build_dir}/#{libpayload_subpath}"
  if !File.exist?(libpayload_path) \
      or ruby_objs_changed \
      or File.mtime(File.join(App.config.versioned_arch_datadir, "librubymotion-static.a")) > File.mtime(libpayload_path)
    payload_o = File.join(File.dirname(payload_c), 'payload.o')
    if !File.exist?(payload_o) or File.mtime(payload_c) > File.mtime(payload_o)
      sh "#{App.config.cc} #{App.config.cflags} -c \"#{payload_c}\" -o \"#{payload_o}\""
    end
    App.info 'Create', libpayload_path
    FileUtils.mkdir_p(File.dirname(libpayload_path))
    sh "#{App.config.cxx} #{App.config.ldflags} \"#{payload_o}\" #{ruby_objs.map { |o, _| "\"" + o + "\"" }.join(' ')} -o \"#{libpayload_path}\" #{App.config.ldlibs}"
  end

  # Create a build/libs -> build/lib symlink (important for ndk-gdb).
  Dir.chdir(app_build_dir) { ln_s 'lib', 'libs' unless File.exist?('libs') }

  # Create a build/jni/Android.mk file (important for ndk-gdb).
  File.open("#{app_build_dir}/jni/Android.mk", 'w') { |io| }

  # Copy the gdb server.
  gdbserver_subpath = "#{libs_abi_subpath}/gdbserver"
  gdbserver_path = "#{app_build_dir}/#{gdbserver_subpath}"
  if !File.exist?(gdbserver_path)
    App.info 'Create', gdbserver_path
    sh "/usr/bin/install -p #{App.config.ndk_path}/prebuilt/android-arm/gdbserver/gdbserver #{File.dirname(gdbserver_path)}"
  end

  # Create the gdb config file.
  gdbconfig_path = "#{app_build_dir}/#{libs_abi_subpath}/gdb.setup"
  if !File.exist?(gdbconfig_path)
    App.info 'Create', gdbconfig_path
    File.open(gdbconfig_path, 'w') do |io|
      io.puts <<EOS
set solib-search-path #{libs_abi_subpath}
EOS
    end
  end

  # Generate the Android manifest file.
  android_manifest_txt = ''
  android_manifest_txt << <<EOS
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="#{App.config.package}" android:versionCode="#{App.config.version_code}" android:versionName="#{App.config.version_name}">
	<uses-sdk android:minSdkVersion="#{App.config.api_version}"/>
EOS
    App.config.manifest_xml_lines(nil).each { |line| android_manifest_txt << "\t" + line + "\n" }
    android_manifest_txt << <<EOS
	<application android:label="#{App.config.name}" android:debuggable="#{App.config.development? ? 'true' : 'false'}" #{App.config.icon ? ('android:icon="@drawable/' + App.config.icon + '"') : ''}>
EOS
    App.config.manifest_xml_lines('application').each { |line| android_manifest_txt << "\t\t" + line + "\n" }
    android_manifest_txt << <<EOS
		<activity android:name="#{App.config.main_activity}" android:label="#{App.config.name}">
			<intent-filter>
                		<action android:name="android.intent.action.MAIN" />
                		<category android:name="android.intent.category.LAUNCHER" />
            		</intent-filter>
        	</activity>
EOS
  (App.config.sub_activities.uniq - [App.config.main_activity]).each do |activity|
    android_manifest_txt << <<EOS
		<activity android:name="#{activity}" android:label="#{activity}" android:parentActivityName="#{App.config.main_activity}">
			<meta-data android:name="android.support.PARENT_ACTIVITY" android:value="#{App.config.main_activity}"/>
		</activity>
EOS
  end
  android_manifest_txt << <<EOS
    </application>
</manifest> 
EOS
  android_manifest = File.join(app_build_dir, 'AndroidManifest.xml')
  if !File.exist?(android_manifest) or File.read(android_manifest) != android_manifest_txt
    App.info 'Create', android_manifest
    File.open(android_manifest, 'w') { |io| io.write(android_manifest_txt) }
  end

  # Create java files based on the classes map files.
  java_classes = {}
  Dir.glob(objs_build_dir + '/**/*.map') do |map|
    txt = File.read(map)
    current_class = nil
    txt.each_line do |line|
      if md = line.match(/^([^\s]+)\s*:\s*([^\s]+)\s*<([^>]*)>$/)
        current_class = java_classes[md[1]]
        if current_class
          if current_class[:super] != md[2]
            $stderr.puts "Class `#{md[1]}' already defined with a different super class (`#{current_class[:super]}')"
            exit 1
          end
        else
          infs = md[3].split(',').map { |x| x.strip }
          current_class = {:super => md[2], :methods => [], :interfaces => infs}
          java_classes[md[1]] = current_class
        end
      elsif md = line.match(/^\t(.+)$/)
        if current_class == nil
          $stderr.puts "Method definition outside class definition"
          exit 1
        end
        current_class[:methods] << md[1]
      else
        $stderr.puts "Ignoring line: #{line}"
      end
    end
  end
  java_dir = File.join(app_build_dir, 'java')
  java_app_package_dir = File.join(java_dir, *App.config.package.split(/\./))
  mkdir_p java_app_package_dir
  java_classes.each do |name, klass|
    java_file_txt = ''
    java_file_txt << <<EOS
// This file has been generated automatically. Do not edit.
package #{App.config.package};
EOS
    java_file_txt << "public class #{name} extends #{klass[:super]}"
    if klass[:interfaces].size > 0
      java_file_txt << " implements #{klass[:interfaces].join(', ')}"
    end
    java_file_txt << " {\n"
    klass[:methods].each do |method|
      java_file_txt << "\t#{method}\n"
    end
    if name == App.config.main_activity
      java_file_txt << "\tstatic {\n\t\tSystem.loadLibrary(\"#{App.config.payload_library_name}\");\n\t}\n"
    end
    java_file_txt << "}\n"
    java_file = File.join(java_app_package_dir, name + '.java')
    if !File.exist?(java_file) or File.read(java_file) != java_file_txt
      File.open(java_file, 'w') { |io| io.write(java_file_txt) }
    end
  end

  # Create R.java files.
  android_jar = "#{App.config.sdk_path}/platforms/android-#{App.config.api_version}/android.jar"
  resources_dirs = []
  App.config.resources_dirs.flatten.each do |dir|
    next unless File.exist?(dir)
    next unless File.directory?(dir)
    resources_dirs << dir
  end
  all_resources = (resources_dirs + App.config.vendored_projects.map { |x| x[:resources] }.compact)
  aapt_resources_flags = all_resources.map { |x| '-S "' + x + '"' }.join(' ')
  r_java_mtime = Dir.glob(java_dir + '/**/R.java').map { |x| File.mtime(x) }.max
  if !r_java_mtime or all_resources.any? { |x| Dir.glob(x + '/**/*').any? { |y| File.mtime(y) > r_java_mtime } }
    extra_packages = App.config.vendored_projects.map { |x| x[:package] }.compact.map { |x| "--extra-packages #{x}" }.join(' ')
    sh "\"#{App.config.build_tools_dir}/aapt\" package -f -M \"#{android_manifest}\" #{aapt_resources_flags} -I \"#{android_jar}\" -m -J \"#{java_dir}\" #{extra_packages} --auto-add-overlay"
  end

  # Compile java files.
  vendored_jars = App.config.vendored_projects.map { |x| x[:jar] }
  vendored_jars += [File.join(App.config.versioned_datadir, 'rubymotion.jar')]
  classes_dir = File.join(app_build_dir, 'classes')
  mkdir_p classes_dir
  class_path = [classes_dir, "#{App.config.sdk_path}/tools/support/annotations.jar", *vendored_jars].map { |x| "\"#{x}\"" }.join(':')
  classes_changed = false
  Dir.glob(File.join(app_build_dir, 'java', '**', '*.java')).each do |java_path|
    paths = java_path.split('/')
    paths[paths.index('java')] = 'classes'
    paths[-1].sub!(/\.java$/, '.class')
    java_class_path = paths.join('/')

    class_name = File.basename(java_path, '.java')
    if !java_classes.has_key?(class_name) and class_name != 'R'
      # This .java file is not referred in the classes map, so it must have been created in the past. We remove it as well as its associated .class file (if any).
      rm_rf java_path
      rm_rf java_class_path
      classes_changed = true
      next
    end

    if !File.exist?(java_class_path) or File.mtime(java_path) > File.mtime(java_class_path)
      App.info 'Create', java_class_path
      sh "/usr/bin/javac -d \"#{classes_dir}\" -classpath #{class_path} -sourcepath \"#{java_dir}\" -target 1.5 -bootclasspath \"#{android_jar}\" -encoding UTF-8 -g -source 1.5 \"#{java_path}\""
      classes_changed = true
    end
  end

  # Generate the dex file.
  dex_classes = File.join(app_build_dir, 'classes.dex')
  if !File.exist?(dex_classes) \
      or File.mtime(App.config.project_file) > File.mtime(dex_classes) \
      or classes_changed \
      or vendored_jars.any? { |x| File.mtime(x) > File.mtime(dex_classes) }
    App.info 'Create', dex_classes
    sh "\"#{App.config.build_tools_dir}/dx\" --dex --output \"#{dex_classes}\" \"#{classes_dir}\" \"#{App.config.sdk_path}/tools/support/annotations.jar\" #{vendored_jars.join(' ')}"
  end

  keystore = nil
  if App.config.development?
    # Create the debug keystore if needed.
    keystore = File.expand_path('~/.android/debug.keystore')
    unless File.exist?(keystore)
      App.info 'Create', keystore
      FileUtils.mkdir_p(File.expand_path('~/.android'))
      sh "/usr/bin/keytool -genkeypair -alias androiddebugkey -keypass android -keystore \"#{keystore}\" -storepass android -dname \"CN=Android Debug,O=Android,C=US\" -validity 9999"
    end
  else
    keystore = App.config.release_keystore_path
    App.fail "app.release_keystore(path, alias_name) must be called when doing a release build" unless keystore
  end

  # Generate the APK file.
  archive = App.config.apk_path
  if !File.exist?(archive) \
      or File.mtime(dex_classes) > File.mtime(archive) \
      or File.mtime(libpayload_path) > File.mtime(archive) \
      or File.mtime(android_manifest) > File.mtime(archive) \
      or resources_dirs.any? { |x| File.mtime(x) > File.mtime(archive) }
    App.info 'Create', archive
    assets_dirs = []
    App.config.assets_dirs.flatten.each do |dir|
      next unless File.exist?(dir)
      next unless File.directory?(dir)
      assets_dirs << dir
    end
    assets_flags = assets_dirs.map { |x| '-A "' + x + '"' }.join(' ')
    sh "\"#{App.config.build_tools_dir}/aapt\" package -f -M \"#{android_manifest}\" #{assets_flags} #{aapt_resources_flags} -I \"#{android_jar}\" -F \"#{archive}\" --auto-add-overlay"
    Dir.chdir(app_build_dir) do
      [File.basename(dex_classes), libpayload_subpath, gdbserver_subpath].each do |file|
        line = "\"#{App.config.build_tools_dir}/aapt\" add -f \"#{File.basename(archive)}\" \"#{file}\""
        line << " > /dev/null" unless Rake.application.options.trace
        sh line
      end
    end

    App.info 'Sign', archive
    if App.config.development?
      sh "/usr/bin/jarsigner -digestalg SHA1 -storepass android -keystore \"#{keystore}\" \"#{archive}\" androiddebugkey"
    else
      sh "/usr/bin/jarsigner -sigalg SHA1withRSA -digestalg SHA1 -keystore \"#{keystore}\" \"#{archive}\" \"#{App.config.release_keystore_alias}\""
    end

    App.info 'Align', archive
    sh "\"#{App.config.zipalign_path}\" -f 4 \"#{archive}\" \"#{archive}-aligned\""
    sh "/bin/mv \"#{archive}-aligned\" \"#{archive}\""
  end
end

desc "Create an application package file (.apk) for release (Google Play)"
task :release do
  App.config_without_setup.build_mode = :release
  App.config_without_setup.distribution_mode = true
  Rake::Task["build"].invoke
end

def adb_mode_flag(mode)
  case mode
    when :emulator
      '-e'
    when :device
      '-d'
    else
      raise
  end
end

def adb_path
  "#{App.config.sdk_path}/platform-tools/adb"
end

def install_apk(mode)
  App.info 'Install', App.config.apk_path
  line = "\"#{adb_path}\" #{adb_mode_flag(mode)} install -r \"#{App.config.apk_path}\""
  line << " > /dev/null" unless Rake.application.options.trace
  sh line
end

def run_apk(mode)
  if ENV['debug']
    App.fail "debug mode not implemented yet"
=begin
    Dir.chdir(App.config.build_dir) do
      App.info 'Debug', App.config.apk_path
      sh "\"#{App.config.ndk_path}/ndk-gdb\" #{adb_mode_flag(mode)} --adb=\"#{adb_path}\" --start"
    end
=end
  else
    # Clear log.
    sh "\"#{adb_path}\" #{adb_mode_flag(mode)} logcat -c"
    # Start main activity.
    activity_path = "#{App.config.package}/.#{App.config.main_activity}"
    App.info 'Start', activity_path
    line = "\"#{adb_path}\" #{adb_mode_flag(mode)} shell am start -a android.intent.action.MAIN -n #{activity_path}"
    line << " > /dev/null" unless Rake.application.options.trace
    sh line
    Signal.trap('INT') do
      # Kill the app on ^C.
      if `\"#{adb_path}\" -d shell ps`.include?(App.config.package)
        sh "\"#{adb_path}\" #{adb_mode_flag(mode)} shell am force-stop #{App.config.package}"
      end
      exit 0
    end
    # Show logs.
    sh "\"#{adb_path}\" #{adb_mode_flag(mode)} logcat -s #{App.config.logs_components.join(' ')}"
  end
end

namespace 'emulator' do
  desc "Create the Android Virtual Device for the emulator"
  task :create_avd do
    all_targets = `\"#{App.config.sdk_path}/tools/android\" list avd --compact`.split(/\n/)
    if !all_targets.include?(App.config.avd_config[:name])
      sh "/bin/echo -n \"no\" | \"#{App.config.sdk_path}/tools/android\" create avd --name \"#{App.config.avd_config[:name]}\" --target \"#{App.config.avd_config[:target]}\" --abi \"#{App.config.avd_config[:abi]}\" --snapshot"
    end
  end

  desc "Start the emulator in the background"
  task :start_avd do
    avd = (ENV['avd'] || App.config.avd_config[:name])
    unless `/bin/ps -a`.split(/\n/).any? { |x| x.include?('emulator64-arm') and x.include?(avd) }
      Rake::Task["emulator:create_avd"].invoke
      sh "\"#{App.config.sdk_path}/tools/emulator\" -avd \"#{avd}\" &"
      sh "\"#{App.config.sdk_path}/platform-tools/adb\" -e wait-for-device"
    end
  end

  desc "Install the app in the emulator"
  task :install do
    install_apk(:emulator)
  end

  desc "Start the app's main intent in the emulator"
  task :start => ['build', 'emulator:start_avd', 'emulator:install'] do
    run_apk(:emulator)
  end
end

namespace 'device' do
  desc "Install the app in the device"
  task :install do
    install_apk(:device)
  end

  desc "Start the app's main intent in the device"
  task :start do
    run_apk(:device)
  end
end

desc "Build the app then run it in the device"
task :device => ['build', 'device:install', 'device:start']

desc "Build the app then run it in the emulator"
task :default => 'emulator:start'
