#!/usr/bin/env ruby
require 'rubygems'
require 'zip/zip'
require 'digest'
require 'rest_client'
require 'json'
require 'rbconfig'
require 'tmpdir'

def host
  ENV["LP_HOST"] || "https://www.lesspainful.com"
end

def digest(file)
  Digest::SHA256.file(file).hexdigest
end

def unzip_file (file, destination)
  Zip::ZipFile.open(file) { |zip_file|
   zip_file.each { |f|
     f_path=File.join(destination, f.name)
     FileUtils.mkdir_p(File.dirname(f_path))
     zip_file.extract(f, f_path) unless File.exist?(f_path)
   }
  }
end

def workspace
  if ARGV[2]
    abort_unless(File.exist?(ARGV[2])) do
      puts "Provided workspace: #{ARGV[2]} does not exist."
    end
    File.join(File.expand_path(ARGV[2]),File::Separator)
  else
    ""
  end
end

def features_zip
  if ARGV[3] and ARGV[3].end_with?".zip"
    abort_unless(File.exist?(ARGV[3])) do
      puts "No file found #{ARGV[3]}"
    end
    File.expand_path(ARGV[3])
  end
end

def app
  ARGV[0]
end

def api_key
  ARGV[1]
end

def is_android?
  app.end_with? ".apk"
end

def calabash_android_version
  `bundle exec calabash-android version`.strip
end

def is_ios?
    app.end_with? ".ipa"
end

def test_server_path
  require 'digest/md5'
  digest = Digest::MD5.file(app).hexdigest
  File.join("test_servers","#{digest}_#{calabash_android_version}.apk")
end

def all_files
  dir = workspace
  if features_zip
    dir = Dir.mktmpdir
    unzip_file(features_zip, dir)
    dir = File.join(dir,File::Separator)
  end

  files = Dir.glob("#{dir}features/**/*") ##no need to File.join here
  files += Dir.glob("#{workspace}vendor/**/*")
  files += Dir.glob("#{workspace}/Gemfile*")
  if is_android?
    files << test_server_path
  end
  {:feature_prefix => dir, :workspace_prefix => workspace, :files => files.find_all { |file_or_dir| File.file? file_or_dir }}
end

def http_post(address, args, &block)
  if block_given?
    response = RestClient.post "#{host}/#{address}", args do |response, request, result, &other_block|
        block.call(response,request,result, &other_block)
    end
  else
    response = RestClient.post "#{host}/#{address}", args
  end

  response.body
end

def is_windows?
   (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
end

def is_macosx?
  (RbConfig::CONFIG['host_os'] =~ /darwin/)
end

def validate_ipa(ipa)
  result = false
  dir = Dir.mktmpdir #do |dir|

    unzip_file(ipa,dir)
    unless File.directory?("#{dir}/Payload")  #macos only
      abort do
        puts "Unzipping #{ipa} to #{dir} failed: Did not find a Payload directory (invalid .ipa)."
      end
    end
    app_dir = Dir.foreach("#{dir}/Payload").find {|d| /\.app$/.match(d)}
    app = app_dir.split(".")[0]
    res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/#{app}" -o 2> /dev/null | grep CalabashServer`

    if /CalabashServer/.match(res)
      puts "ipa: #{ipa} *contains* calabash.framework"
      result = :calabash
    end

    unless result
      res = `otool "#{File.expand_path(dir)}/Payload/#{app_dir}/#{app}" -o 2> /dev/null | grep FrankServer`
      if /FrankServer/.match(res)
        puts "ipa: #{ipa} *contains* FrankServer"
        result = :frank
      else
        puts "ipa: #{ipa} *does not contain* calabash.framework"
        result = false
      end
    end
  #end
  result
end


def self.log(message)
 puts "#{Time.now } #{message}"
 $stdout.flush
end

def self.log_header(message)
  if is_windows?
    puts "\n### #{message} ###"
  else
    puts "\n\e[#{35}m### #{message} ###\e[0m"
  end
end

def usage
  "Usage: lesspainful <app> <api_key> <opt workspace>? <opt feature.zip>?"
end

def verify_arguments
  server = nil
  log_and_abort(usage) unless app
  log_and_abort(usage) unless api_key
  abort_unless(File.exist?(app)) do
    puts usage
    puts "No such file: #{app}"
  end
  abort_unless(/\.(apk|ipa)$/ =~ app) do
    puts usage
    puts "<app> should be either an ipa or apk file"
  end
  if is_ios? and is_macosx? and not ENV['CHECK_IPA'] == '0'
    log_header("Checking ipa for linking with Calabash or Frank")
    server = validate_ipa(app)
    abort_unless(server) do
      puts "The .ipa file does not seem to be linked with Calabash."
      puts "Verify that your app is linked correctly."
      puts "To disable this check run with Environment Variable CHECK_IPA=0"
    end
  end
  server
end

def abort(&block)
  yield block
  exit 1
end

def abort_unless(condition, &block)
  unless condition
    yield block
    exit 1
  end
end

def log_and_abort(message)
  abort do 
    puts message
  end
end


def self.verify_files
  if is_android?
    abort_unless(File.exist?(test_server_path)) do
      puts "No test server found. Please run:"
      puts "  calabash-android build #{app}"
    end

    calabash_gem = Dir.glob("vendor/cache/calabash-android-*").first
    abort_unless(calabash_gem) do
      puts "calabash-android was not packaged correct."
      puts "Please tell contact@lesspainful.com about this bug."
    end
  end
end




start_at = Time.now

server = verify_arguments

log_header("Checking for Gemfile")
unless File.exist?("Gemfile")
  log("Gemfile missing.")
  if is_android?
    log("Creating Gemfile for Android")
    tgt = File.join(File.dirname(__FILE__),"..","lib","GemfileAndroid")
  elsif is_ios?
    log("Creating Gemfile for iOS")
    gemfile = "GemfileIOS"
    if server == :frank
      gemfile = "GemfileIOSFrank"
    end
    tgt = File.join(File.dirname(__FILE__),"..","lib",gemfile)
  else
    abort do
      puts usage
      puts "Your app (second argument) must be an ipa or apk file."
    end

  end
  FileUtils.cp(File.expand_path(tgt), "Gemfile")
end

log_header("Packaging")
log_and_abort "Bundler failed. Please check command: bundle package" unless system("bundle package --all")


log_header("Collecting files")
collected_files = all_files
file_paths = collected_files[:files]
feature_prefix = collected_files[:feature_prefix]
workspace_prefix = collected_files[:workspace_prefix]

hashes = file_paths.collect { |f| digest(f) }
hashes << digest(app)

log_header("Verifying files")

verify_files




log_header("Negotiating upload")

response = http_post("check_hash", {"hashes" => hashes})

cache_status = JSON.parse(response)

curl_args = []
files = []
paths = []
file_paths.each do |file|
  if cache_status[digest(file)]
    #Server already knows about this file. No need to upload it.
    files << digest(file)
  else
    #Upload file
    files << File.new(file)
  end

  if file.start_with?(feature_prefix)
    prefix = feature_prefix
  else
    prefix = workspace_prefix
  end
  paths << file.sub(prefix, "")
end

app_file = cache_status[digest(app)] ? digest(app) : File.new(app)

log_header("Uploading negotiated files")
response = http_post("upload", {"files" => files,
                                "paths" => paths,
                                "app" => app_file,
                                "api_key" => api_key,
                                "app_filename" => File.basename(app)}) do  |response, request, result, &block|
  case response.code
   when 200
     response
   when 403
     abort do
        puts "Invalid API key"
     end
    when 413
      abort do
        puts "Files too large"
     end
   else
     abort do
       log "Unexpected Error. Please contact contact@lesspainful.com and provide the timestamp on your left."
     end
   end
end

end_at = Time.now

log_header("Done (took %.1fs)" % (end_at - start_at))
log response