#!/usr/bin/env python
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Blimp Client + Engine integration test system

   Set up Client and Engine
   Set up Forward to connect machine host with client device.
   Start Engine and run blimp on Android Client.
"""

import argparse
import json
import logging
import posixpath
import os
import re
import signal
import subprocess
import sys

SRC_PATH = os.path.abspath(
    os.path.join(os.path.dirname(__file__), '..', '..'))

DEVIL_PATH = os.path.join(SRC_PATH, 'third_party', 'catapult',
                          'devil')
DEVIL_CHROMIUM_PATH = os.path.join(SRC_PATH, 'build', 'android')

if DEVIL_CHROMIUM_PATH not in sys.path:
  sys.path.append(DEVIL_CHROMIUM_PATH)

import devil_chromium

if DEVIL_PATH not in sys.path:
  sys.path.append(DEVIL_PATH)

from devil.android import device_blacklist
from devil.android import device_utils
from devil.android import forwarder
from devil.android.sdk import intent
from devil.android.sdk import version_codes
from devil.utils import cmd_helper

_CLIENT_TOKEN_PATH = posixpath.join('/', 'data', 'data',
                                    'org.chromium.chrome',
                                    'blimp_client_token')
_TOKEN_FILE_PATH = os.path.join(SRC_PATH, 'blimp', 'test', 'data',
                                'test_client_token')
PORT_PATTERN = re.compile(r'.*Engine port #: (\d+)')
ARGS_JSON_FILE = 'blimp_script_args.json'

def AddStartArguments(parser):
  parser.add_argument('-d',
                      '--device',
                      help='Serial number of device we should use.')
  parser.add_argument('--blacklist-file',
                      default=None,
                      help='Device blacklist JSON file.')
  parser.add_argument('--engine-ip',
                      default='127.0.0.1',
                      help='Blimp engine IP.')


def _IsNonEmptyFile(fpath):
  return os.path.isfile(fpath) and os.path.getsize(fpath) > 0


def RunClient(device, optional_url):
  """Run Blimp client.

  Args:
    device: (DeviceUtils) device to run the tests on.
    optional_url: (str) URL to navigate to.
  """
  run_client_intent = intent.Intent(
      action='android.intent.action.VIEW',
      package='org.chromium.chrome',
      activity='com.google.android.apps.chrome.Main',
      data=optional_url)
  device.StartActivity(run_client_intent, blocking=True)


def RunEngine(output_linux_directory, token_file_path, device):
  """Start running engine

  Args:
    output_linux_directory: (str) Path to the root linux build directory.
    token_file_path: (str) Path to the client auth token file.

  Returns:
    port: (str) Engine port number generated by engine session.
  """
  port = '0'
  blimp_engine_app = os.path.join(output_linux_directory,
                                  'blimp_engine_app')

  sub_dir = "marshmallow"
  if device.build_version_sdk == version_codes.KITKAT:
    sub_dir = "kitkat"
  blimp_fonts_path = os.path.join(output_linux_directory, 'gen',
                                  'third_party', 'blimp_fonts',
                                  'font_bundle', sub_dir)

  run_engine_cmd = [
      blimp_engine_app +
      ' --android-fonts-path=' + blimp_fonts_path +
      ' --blimp-client-token-path=' + token_file_path +
      ' --enable-logging=stderr' +
      ' -v=0' +
      ' --vmodule="blimp*=1"']
  p = subprocess.Popen(run_engine_cmd, shell=True,
                       stdout=subprocess.PIPE,
                       stderr=subprocess.STDOUT,
                       preexec_fn=os.setsid)

  for line in iter(p.stdout.readline, ''):
    sys.stdout.write(line)
    l = line.rstrip()
    match = re.match(PORT_PATTERN, l)
    if match:
      port = match.group(1)
      break

  return port, p


def SetCommandFlag(device, engine_ip, engine_port):
  """Set up adb Chrome command line flags

  Args:
    device: (str) Serial number of device we should use.
    engine_ip: (str) Blimp engine IP address.
    engine_port: (str) Port on the engine.
  """
  cmd_helper.GetCmdStatusAndOutput([
      os.path.join(SRC_PATH, 'build', 'android',
                   'adb_chrome_public_command_line'),
      '--device=' + str(device),
      '--enable-blimp',
      '--engine-ip=' + engine_ip,
      '--engine-port=' + engine_port,
      '--engine-transport=tcp',
      '-v=0',
      '--vmodule=*blimp*=1',
      '--blimp-client-token-path=' + _CLIENT_TOKEN_PATH])


def _JsonEncodeDefault(obj):
  if isinstance(obj, argparse.Namespace):
    result = obj.__dict__
    result.update({'_type': 'args'})
    return result
  raise TypeError("Json object must be an instance of argparse.Namespace")


def _FromJson(json_object):
  if json_object.get('_type') == 'args':
    result = argparse.Namespace()
    for key, value in json_object.iteritems():
      setattr(result, key, value)
    return result
  return json_object


def _Start(args, json_file_path, device):
  """Start engine and forwarder

  Args:
    args: parsed command line arguments.
    json_file_path: json file path which keeps the script variables.
    device: (DeviceUtils) device to run the tests on.
  """
  if _IsNonEmptyFile(json_file_path):
    logging.error('Error: An engine instance is already running.')
    sys.exit(1)

  json_args = argparse.Namespace()
  for k in args.__dict__:
    if not k == "func":
      setattr(json_args, k, args.__dict__[k])
  json_object = {'args': json_args}
  device.EnableRoot()
  host_device_tuples = [(_TOKEN_FILE_PATH, _CLIENT_TOKEN_PATH)]
  device.PushChangedFiles(host_device_tuples)

  port_number, engine_process = RunEngine(
      args.output_linux_directory, _TOKEN_FILE_PATH, device)
  json_object['port_number'] = port_number
  json_object['pid'] = engine_process.pid
  logging.info('Engine port number: %s', port_number)
  logging.info('Engine running PID: %d', engine_process.pid)

  if engine_process.poll() is not None:
    logging.error('Engine failed to start. Return code: %d',
                  engine_process.poll())
  else:
    try:
      port_pairs = [(port_number, port_number)]
      SetCommandFlag(device, args.engine_ip, port_number)
      forwarder.Forwarder.Map(port_pairs, device)
      print "Blimp engine started"
      return engine_process

    finally:
      with open(json_file_path, 'w') as f:
        json.dump(json_object, f, default=_JsonEncodeDefault)


def _Run(args, json_file_path, device):
  """Start engine and forwarder and keep runnning.

  Args:
    args: parsed command line arguments.
    json_file_path: json file path which keeps the script variables.
    device: (DeviceUtils) device to run the tests on.
  """
  try:
    engine_process = _Start(args, json_file_path, device)
    while True:
      nextline = engine_process.stdout.readline()
      if nextline == '' and engine_process.poll() is not None:
        # The engine died.
        sys.exit(1)
      sys.stdout.write(nextline)
      sys.stdout.flush()

  except KeyboardInterrupt:
    sys.exit(0)
  finally:
    _Stop(args, json_file_path, device)


def _Load(args, json_file_path, device): # pylint: disable=unused-argument
  """Start client and load the url

  Args:
    args: parsed command line arguments.
    json_file_path: json file path which keeps the script variables.
    device: (DeviceUtils) device to run the tests on.
  """
  device.Install(os.path.join(SRC_PATH, args.apk_path),
                 reinstall=True)
  run_client_intent = intent.Intent(
      action='android.intent.action.VIEW',
      package='org.chromium.chrome',
      activity='com.google.android.apps.chrome.Main',
      data=args.optional_url)
  device.StartActivity(run_client_intent, blocking=True)


def _Stop(args, json_file_path, device): # pylint: disable=unused-argument
  """Stop engine and forwarder

  Args:
    args: (unused) parsed command line arguments.
    json_file_path: json file path which keeps the script variables.
    device: (DeviceUtils) device to run the tests on.
  """
  if not _IsNonEmptyFile(json_file_path):
    logging.error('Error: cannot find json file: ' + json_file_path)
    sys.exit(1)
  try:
    with open(json_file_path, 'r') as f:
      jsonarg = json.load(f, object_hook=_FromJson)
    pid = int(jsonarg['pid'])
    try:
      forwarder.Forwarder.UnmapAllDevicePorts(device)
    finally:
      os.kill(pid, signal.SIGKILL)
  finally:
    os.remove(json_file_path)


def main():
  parser = argparse.ArgumentParser()
  parser.add_argument('-l',
                      '--output-linux-directory',
                      required=True,
                      help='Path to the root linux build directory.'
                           ' Example: "out-linux/Debug"')
  parser.add_argument('--adb-path',
                      type=os.path.abspath,
                      help='Path to the adb binary.')
  subparsers = parser.add_subparsers(dest="subparser_name")

  start_parser = subparsers.add_parser('start')
  start_parser.set_defaults(func=_Start)
  AddStartArguments(start_parser)

  run_parser = subparsers.add_parser('run')
  run_parser.set_defaults(func=_Run)
  AddStartArguments(run_parser)

  load_parser = subparsers.add_parser('load')
  load_parser.set_defaults(func=_Load)
  load_parser.add_argument('-p',
                           '--apk-path',
                           required=True,
                           help='The path to the APK to install. '
                                'Including the name of the apk containing '
                                'the application (with the .apk extension).')
  load_parser.add_argument('-u',
                           '--optional-url',
                           help='URL to navigate to.')

  stop_parser = subparsers.add_parser('stop')
  stop_parser.set_defaults(func=_Stop)

  args = parser.parse_args()
  devil_chromium.Initialize(adb_path=args.adb_path)

  json_file_path = os.path.join(SRC_PATH, args.output_linux_directory,
                                ARGS_JSON_FILE)
  blacklist_file = ''
  serial = ''
  if args.subparser_name == 'start' or args.subparser_name == 'run':
    blacklist_file = args.blacklist_file
    serial = args.device
  else:
    if not _IsNonEmptyFile(json_file_path):
      logging.error('Cannot find json file: %s' + json_file_path)
      logging.error('Run `client_engine_integration.py {run,start}` first.')
      sys.exit(1)
    with open(json_file_path, 'r') as f:
      file_lines = f.readlines()
    jsonarg = json.loads(file_lines[0], object_hook=_FromJson)
    blacklist_file = jsonarg['args'].blacklist_file
    serial = jsonarg['args'].device

  blacklist = (device_blacklist.Blacklist(blacklist_file)
               if blacklist_file
               else None)
  device = device_utils.DeviceUtils.HealthyDevices(
      blacklist=blacklist, device_arg=serial)[0]

  args.func(args, json_file_path, device)


if __name__ == '__main__':
  main()
