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

"""Utility for dart_pkg and dart_pkg_app rules"""

import argparse
import errno
import json
import os
import shutil
import subprocess
import sys

USE_LINKS = sys.platform != 'win32'

DART_ANALYZE = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), 'dart_analyze.py'
)


def dart_filter(path):
  if os.path.isdir(path):
    return True
  _, ext = os.path.splitext(path)
  # .dart includes '.mojom.dart'
  return ext == '.dart'


def ensure_dir_exists(path):
  abspath = os.path.abspath(path)
  if not os.path.exists(abspath):
    os.makedirs(abspath)


def has_pubspec_yaml(paths):
  for path in paths:
    _, filename = os.path.split(path)
    if filename == 'pubspec.yaml':
      return True
  return False


def link(from_root, to_root):
  ensure_dir_exists(os.path.dirname(to_root))
  try:
    os.unlink(to_root)
  except OSError as err:
    if err.errno == errno.ENOENT:
      pass

  try:
    os.symlink(from_root, to_root)
  except OSError as err:
    if err.errno == errno.EEXIST:
      pass


def copy(from_root, to_root, filter_func=None):
  if not os.path.exists(from_root):
    return
  if os.path.isfile(from_root):
    ensure_dir_exists(os.path.dirname(to_root))
    shutil.copy(from_root, to_root)
    return

  ensure_dir_exists(to_root)

  for root, dirs, files in os.walk(from_root):
    # filter_func expects paths not names, so wrap it to make them absolute.
    wrapped_filter = None
    if filter_func:
      wrapped_filter = lambda name, rt=root: filter_func(os.path.join(rt, name))

    for name in filter(wrapped_filter, files):
      from_path = os.path.join(root, name)
      root_rel_path = os.path.relpath(from_path, from_root)
      to_path = os.path.join(to_root, root_rel_path)
      to_dir = os.path.dirname(to_path)
      if not os.path.exists(to_dir):
        os.makedirs(to_dir)
      shutil.copy(from_path, to_path)

    dirs[:] = list(filter(wrapped_filter, dirs))


def copy_or_link(from_root, to_root, filter_func=None):
  if USE_LINKS:
    link(from_root, to_root)
  else:
    copy(from_root, to_root, filter_func)


def link_if_possible(from_root, to_root):
  if USE_LINKS:
    link(from_root, to_root)


def remove_if_exists(path):
  try:
    os.remove(path)
  except OSError as err:
    if err.errno != errno.ENOENT:
      raise


def list_files(from_root, filter_func=None):
  file_list = []
  for root, dirs, files in os.walk(from_root):
    # filter_func expects paths not names, so wrap it to make them absolute.
    wrapped_filter = None
    if filter_func:
      wrapped_filter = lambda name, rt=root: filter_func(os.path.join(rt, name))
    for name in filter(wrapped_filter, files):
      path = os.path.join(root, name)
      file_list.append(path)
    dirs[:] = list(filter(wrapped_filter, dirs))
  return file_list


def remove_broken_symlink(path):
  if not USE_LINKS:
    return
  try:
    link_path = os.readlink(path)
  except OSError as err:
    # Path was not a symlink.
    if err.errno == errno.EINVAL:
      pass
  else:
    if not os.path.exists(link_path):
      remove_if_exists(path)


def remove_broken_symlinks(root_dir):
  if not USE_LINKS:
    return
  for current_dir, _, child_files in os.walk(root_dir):
    for filename in child_files:
      path = os.path.join(current_dir, filename)
      remove_broken_symlink(path)


def analyze_entrypoints(dart_sdk, package_root, entrypoints):
  cmd = ['python', DART_ANALYZE]
  cmd.append('--dart-sdk')
  cmd.append(dart_sdk)
  cmd.append('--entrypoints')
  cmd.extend(entrypoints)
  cmd.append('--package-root')
  cmd.append(package_root)
  cmd.append('--no-hints')
  try:
    subprocess.check_output(cmd, stderr=subprocess.STDOUT)
  except subprocess.CalledProcessError as err:
    print('Failed analyzing %s' % entrypoints)
    print(err.output)
    return err.returncode
  return 0


def main():
  parser = argparse.ArgumentParser(description='Generate a dart-pkg')
  parser.add_argument(
      '--dart-sdk',
      action='store',
      metavar='dart_sdk',
      help='Path to the Dart SDK.'
  )
  parser.add_argument(
      '--package-name',
      action='store',
      metavar='package_name',
      help='Name of package',
      required=True
  )
  parser.add_argument(
      '--pkg-directory',
      metavar='pkg_directory',
      help='Directory where dart_pkg should go',
      required=True
  )
  parser.add_argument(
      '--package-root',
      metavar='package_root',
      help='packages/ directory',
      required=True
  )
  parser.add_argument(
      '--stamp-file',
      metavar='stamp_file',
      help='timestamp file',
      required=True
  )
  parser.add_argument(
      '--entries-file',
      metavar='entries_file',
      help='script entries file',
      required=True
  )
  parser.add_argument(
      '--package-sources',
      metavar='package_sources',
      help='Package sources',
      nargs='+'
  )
  parser.add_argument(
      '--package-entrypoints',
      metavar='package_entrypoints',
      help='Package entry points for analyzer',
      nargs='*',
      default=[]
  )
  parser.add_argument(
      '--sdk-ext-directories',
      metavar='sdk_ext_directories',
      help='Directory containing .dart sources',
      nargs='*',
      default=[]
  )
  parser.add_argument(
      '--sdk-ext-files',
      metavar='sdk_ext_files',
      help='List of .dart files that are part of sdk_ext.',
      nargs='*',
      default=[]
  )
  parser.add_argument(
      '--sdk-ext-mappings',
      metavar='sdk_ext_mappings',
      help='Mappings for SDK extension libraries.',
      nargs='*',
      default=[]
  )
  parser.add_argument(
      '--read_only',
      action='store_true',
      dest='read_only',
      help='Package is a read only package.',
      default=False
  )
  args = parser.parse_args()

  # We must have a pubspec.yaml.
  assert has_pubspec_yaml(args.package_sources)

  target_dir = os.path.join(args.pkg_directory, args.package_name)
  target_packages_dir = os.path.join(target_dir, 'packages')
  lib_path = os.path.join(target_dir, 'lib')
  ensure_dir_exists(lib_path)

  mappings = {}
  for mapping in args.sdk_ext_mappings:
    library, path = mapping.split(',', 1)
    mappings[library] = '../sdk_ext/%s' % path

  sdkext_path = os.path.join(lib_path, '_sdkext')
  if mappings:
    with open(sdkext_path, 'w') as stream:
      json.dump(
          mappings, stream, sort_keys=True, indent=2, separators=(',', ': ')
      )
  else:
    remove_if_exists(sdkext_path)

  # Copy or symlink package sources into pkg directory.
  common_source_prefix = os.path.dirname(
      os.path.commonprefix(args.package_sources)
  )
  for source in args.package_sources:
    relative_source = os.path.relpath(source, common_source_prefix)
    target = os.path.join(target_dir, relative_source)
    copy_or_link(source, target)

  entrypoint_targets = []
  for source in args.package_entrypoints:
    relative_source = os.path.relpath(source, common_source_prefix)
    target = os.path.join(target_dir, relative_source)
    copy_or_link(source, target)
    entrypoint_targets.append(target)

  # Copy sdk-ext sources into pkg directory
  sdk_ext_dir = os.path.join(target_dir, 'sdk_ext')
  for directory in args.sdk_ext_directories:
    sdk_ext_sources = list_files(directory, dart_filter)
    common_prefix = os.path.commonprefix(sdk_ext_sources)
    for source in sdk_ext_sources:
      relative_source = os.path.relpath(source, common_prefix)
      target = os.path.join(sdk_ext_dir, relative_source)
      copy_or_link(source, target)

  common_source_prefix = os.path.dirname(
      os.path.commonprefix(args.sdk_ext_files)
  )
  for source in args.sdk_ext_files:
    relative_source = os.path.relpath(source, common_source_prefix)
    target = os.path.join(sdk_ext_dir, relative_source)
    copy_or_link(source, target)

  # Symlink packages/
  package_path = os.path.join(args.package_root, args.package_name)
  copy_or_link(lib_path, package_path)

  # Link dart-pkg/$package/packages to dart-pkg/packages
  link_if_possible(args.package_root, target_packages_dir)

  # Remove any broken symlinks in target_dir and package root.
  remove_broken_symlinks(target_dir)
  remove_broken_symlinks(args.package_root)

  # If any entrypoints are defined, write them to disk so that the analyzer
  # test can find them.
  with open(args.entries_file, 'w') as file:
    for entrypoint in entrypoint_targets:
      file.write(entrypoint + '\n')

  # Write stamp file.
  with open(args.stamp_file, 'w'):
    pass

  return 0


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