#!/usr/bin/env python3

import argparse
import itertools
import os
import re
import shlex
import subprocess
import sys

from pycman import config

LDD_TARGET_REGEX = re.compile(rb'.*\s+=>\s+(.*?)\s+\([0-9a-fA-Fx]+\)')
PKG_FIELDS = (
  'arch',
  'base',
  'builddate',
  'desc',
  'installdate',
  'name',
  'packager',
  'size',
  'url',
  'version'
)
DEFAULT_FORMAT = '{name}-{version}'



parser = argparse.ArgumentParser(description='List all packages required to satisfy shared object dependencies.')
parser.add_argument(
  'objects', metavar='<path>', nargs='+',
  help='The object files (executables or libraries).'
)
parser.add_argument(
  '-c', '--config', metavar='<path>', default='/etc/pacman.conf',
  help='Pacman configuration file.'
)
parser.add_argument(
  '-a', '--all', action='store_true',
  help='List all dependencies. Without this option, dependencies required by other dependencies are not listed.',
)
parser.add_argument(
  '-f', '--format', default=DEFAULT_FORMAT,
  help='Python format string for package information. Available fields are: {}'.format(', '.join(PKG_FIELDS)),
)
parser.add_argument(
  '-v', '--verbose', action='store_true',
  help='List object files owned by each dependency.',
)
# parser.add_argument(
#   '-g', '--graph', action='store_true',
#   help='Print a Graphviz dot file.',
# )



def parse_linked_objects(paths):
  cmd = ['ldd']
  cmd.extend(paths)
  if len(cmd) == 1:
    return
  ldd_output = subprocess.run(cmd, check=False, stdout=subprocess.PIPE).stdout
  for line in ldd_output.split(b'\n'):
    if b' => ' not in line:
      continue
    target = LDD_TARGET_REGEX.sub(rb'\1', line).decode()
    yield os.path.realpath(target)



def find_owner(handle, paths):
  paths = set(p.lstrip('/') for p in paths)
  for pkg in handle.get_localdb().pkgcache:
    if not paths:
      break
    owned_paths = set(p for p,_,_ in pkg.files) & paths
    if owned_paths:
      paths -= owned_paths
      yield pkg, sorted(owned_paths)
  yield None, paths



def filter_nested_deps(pkgs_and_objects):
  pkgs_and_objects = list(pkgs_and_objects)
  required_by = set(itertools.chain.from_iterable(pkg.compute_requiredby() for pkg,_ in pkgs_and_objects if pkg is not None))
  for pkg_and_objects in pkgs_and_objects:
    pkg = pkg_and_objects[0]
    if pkg is None or pkg.name not in required_by:
      yield pkg_and_objects



# def print_graph(pkgs_and_objects):
#   print('digraph g {')
#   pkgs_and_objects = list(pkgs_and_objects)
#   pkgnames = set(p.name for (p,_) in pkgs_and_objects)
#   for pkg, objects in pkgs_and_objects:
#     pass
#   print('}')



def print_list(pkgs_and_objects, fmt=DEFAULT_FORMAT, include_deps=False, verbose=False):
  if not include_deps:
    pkgs_and_objects = filter_nested_deps(pkgs_and_objects)
  for pkg, objects in pkgs_and_objects:
    if pkg is None:
      header = '{:d} target(s) with no owner'.format(len(objects))
    else:
      attrs = dict((a, getattr(pkg, a)) for a in PKG_FIELDS)
      header = fmt.format(**attrs)
    print(header)
    if verbose:
      for obj in objects:
        print('  {}'.format(obj))



def main(args=None):
  pargs = parser.parse_args(args)
  handle = config.init_with_config(pargs.config)
  targets = parse_linked_objects(pargs.objects)
  pkgs_and_objects = find_owner(handle, targets)
#   if pargs.graph:
#     print_graph(pkgs_and_objects)
#   else:
  print_list(pkgs_and_objects, include_deps=pargs.all, verbose=pargs.verbose)



if __name__ == '__main__':
  try:
    main()
  except (KeyboardInterrupt, BrokenPipeError):
    pass
