#!/usr/bin/python
#
# Copyright (C) 2020 The Android Open Source Project
#
# Licensed 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.
#

import json
import os
import sys

# If this doesn't help use PYTHONPATH=${PYTHONPATH}:<berberis>/android_api
sys.path.append(os.path.join(os.path.dirname(sys.argv[0]), '..', '..', 'android_api'))

import api_analysis


def _merge_syscalls(all_syscalls, custom_syscalls):
  arch_names = {'arm', 'arm64', 'x86', 'x86_64', 'riscv64'}

  for name, custom_syscall in custom_syscalls.items():
    # Top-level fields apply to all architecures
    common_update = {k: v for k, v in custom_syscall.items() if k not in arch_names}

    for arch in arch_names:
      arch_update = custom_syscall.get(arch, {})

      # Syscall for arch does not exist.
      # Create it only if update for arch is not empty.
      # Do not create syscall for arch from common update only!
      if not arch_update:
        if name not in all_syscalls or arch not in all_syscalls[name]:
          continue

      arch_syscall = all_syscalls.setdefault(name, {}).setdefault(arch, {})
      arch_syscall.update(common_update)
      arch_syscall.update(arch_update)


def _get_syscall_params(arch_syscall, arch_api):
  if 'params' in arch_syscall:
    return arch_syscall['params']

  if 'entry' not in arch_syscall:
    return None

  entry_symbol_name = '__do_' + arch_syscall['entry']
  if entry_symbol_name not in arch_api['symbols']:
    return None
  entry_symbol = arch_api['symbols'][entry_symbol_name]

  entry_type = arch_api['types'][entry_symbol['type']]
  assert entry_type['kind'] == 'function'
  return entry_type['params']


def _is_syscall_api_compatible(src_syscall, src_api):
  for param_type_name in _get_syscall_params(src_syscall, src_api):
    assert param_type_name in src_api['types']
    if not src_api['types'][param_type_name]['is_compatible']:
      return False

  return True


def _is_64bit_arch(arch):
  return arch == 'arm64' or arch == 'x86_64' or arch == 'riscv64'

def _match_syscall_args(src_arch, dst_arch, src_api, params):
  if _is_64bit_arch(src_arch) and _is_64bit_arch(dst_arch):
    args = ['arg_%d' % (i + 1) for i in range(len(params))]
    return args, args

  assert src_arch == 'arm' and dst_arch == 'x86'

  src_args = []
  dst_args = []

  for param_type_name in params:
    param_type = src_api['types'][param_type_name]
    param_size = int(param_type['size'])

    i = len(src_args)
    if param_size <= 32:
      src_args.append('arg_%d' % (i + 1))
      dst_args.append('arg_%d' % (i + 1))
    else:
      assert param_size == 64
      if i % 2 != 0:
        src_args.append('arg_%d' % (i + 1))
        i += 1
      src_args.append('arg_%d' % (i + 1))
      src_args.append('arg_%d' % (i + 2))
      dst_args.append('arg_%d' % (i + 1))
      dst_args.append('arg_%d' % (i + 2))

  return src_args, dst_args


def _print_syscalls_translation(src_arch, dst_arch, all_syscalls, guest_api):
  print("""\
// This file automatically generated by gen_kernel_syscalls_translation.py
// DO NOT EDIT!

long RunGuestSyscallImpl(long guest_nr,
                         long arg_1,
                         long arg_2,
                         long arg_3,
                         long arg_4,
                         long arg_5,
                         long arg_6) {
  switch (guest_nr) {""")

  # Sort syscalls by name
  for name, syscall in sorted(all_syscalls.items()):
    # Filter syscalls by src_arch
    if src_arch not in syscall:
      continue
    src_syscall = syscall[src_arch]

    print('    case %s:  // %s' % (src_syscall['id'], name))

    params = _get_syscall_params(src_syscall, guest_api)
    if params is None:
      print('      // missing prototype')
      print('      TRACE("unsupported syscall %s");' % (name))
      print('      errno = ENOSYS;')
      print('      return -1;')
      continue

    # Guest args might differ from host args.
    # For example, arm aligns register pairs for 64-bit args and x86 doesn't.
    # For arm to x86, matching should remove padding - skip certain args.
    # For x86 to arm, matching should insert padding - insert zero args.
    # Host system call obiously receives _host_ args.
    # For RunGuestSyscall_* we need a convention. Assume we compile same guest for multiple hosts,
    # then guest args are stable while host args might vary. For better source compatibility, let
    # RunGuestSyscall_* always receive _guest_ args.
    src_args, dst_args = _match_syscall_args(src_arch, dst_arch, guest_api, params)

    # Translate to syscall with the same name.
    # This is a simplification, but it works for architectures of the same bitness.
    if dst_arch not in syscall:
      print('      // missing on %s' % (dst_arch))
      print('      return RunGuestSyscall_%s(' % (name) + ', '.join(src_args) + ');')
      continue

    dst_syscall = syscall[dst_arch]

    if 'custom_reason' in src_syscall:
      print('      // %s' % (src_syscall['custom_reason']))
      print('      return RunGuestSyscall_%s(' % (name) + ', '.join(src_args) + ');')
      continue

    if 'custom_reason' in dst_syscall:
      print('      // %s' % (dst_syscall['custom_reason']))
      print('      return RunGuestSyscall_%s(' % (name) + ', '.join(src_args) + ');')
      continue

    # If no custom reason given, syscall must have ids and entries.
    assert 'id' in src_syscall
    assert 'id' in dst_syscall
    assert 'entry' in src_syscall
    assert 'entry' in dst_syscall

    if not _is_syscall_api_compatible(src_syscall, guest_api):
      print('      // incompatible prototype')
      print('      return RunGuestSyscall_%s(' % (name) + ', '.join(src_args) + ');')
      continue

    # Translate to syscall with the same entry.
    if src_syscall['entry'] != dst_syscall['entry']:
      print('      // %s on %s but %s on %s' % (src_syscall['entry'], src_arch, dst_syscall['entry'], dst_arch))
      print('      return RunGuestSyscall_%s(' % (name) + ', '.join(src_args) + ');')
      continue

    # Translate!
    print('      return syscall(' + ', '.join([dst_syscall['id']] + dst_args) + ');')

  print("""\
    default:
      return RunUnknownGuestSyscall(guest_nr, arg_1, arg_2, arg_3, arg_4, arg_5, arg_6);
  }
}""")


def main(argv):
  if (len(argv) != 3):
    print("Usage: " + argv[0] +" <src_arch> <dst_arch>")
    return 1

  workdir = os.path.dirname(argv[0])

  src_arch = argv[1]
  dst_arch = argv[2]

  with open(os.path.join(workdir, 'kernel_api_%s.json' % (src_arch))) as json_file:
    guest_api = json.load(json_file)

  with open(os.path.join(workdir, 'kernel_api_%s.json' % (dst_arch))) as json_file:
    host_api = json.load(json_file)

  api_analysis.mark_incompatible_api(guest_api, host_api, False)

  with open(os.path.join(workdir, 'kernel_syscalls.json')) as json_file:
    all_syscalls = json.load(json_file)

  with open(os.path.join(workdir, 'custom_syscalls.json')) as json_file:
    custom_syscalls = json.load(json_file)

  _merge_syscalls(all_syscalls, custom_syscalls)

  _print_syscalls_translation(src_arch, dst_arch, all_syscalls, guest_api)

  return 0


if __name__ == '__main__':
  sys.exit(main(sys.argv))
