# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Generates dependency graph diffs.

As an input it takes 2 or more dependency graphs output from cros_extract_deps
and it finds all divergent packages (packages whose versions differ between
some of these dependency graphs) and outputs graphs that trace the divergence
in the dependency trees until common packages are found.
"""

import json
import optparse
import os

from chromite.lib import dot_helper

NORMAL_COLOR = 'black'
BASE_COLORS = ['red', 'green', 'blue']


def UnversionedName(dep):
  """Returns the name of the package, omitting the version."""
  return '%s/%s' % (dep['category'], dep['name'])


def GetColor(index):
  """Maps index to a color."""
  try:
    return BASE_COLORS[index]
  except IndexError:
    # Generate a color by splicing the bits to generate high contrast colors
    index -= len(BASE_COLORS) - 1
    chars = [0] * 3
    for bit in xrange(0, 24):
      chars[bit % 3] |= ((index >> bit) & 0x1) << (7-bit/3)
    return "#%02x%02x%02x" % tuple(chars)


def GetReverseDependencyClosure(full_name, deps_map, divergent_set):
  """Gets the closure of the reverse dependencies of a node.

  Walks the tree along all the reverse dependency paths to find all the nodes
  of the divergent set that transitively depend on the input node.
  """
  s = set()
  def GetClosure(name):
    node = deps_map[name]
    if UnversionedName(node) in divergent_set:
      s.add(name)
      for dep in node['rev_deps']:
        if dep in s:
          continue
        GetClosure(dep)

  GetClosure(full_name)
  return s


def GetVersionMap(input_deps):
  """Creates the version map for the input data.

  The version map maps an unversioned package name to its corresponding
  versioned name depending on the input dependency graph.

  For every package, it maps the input data index to the full name (versioned)
  of the package in that input data. E.g.
  map['x11-base/xorg-server'] = {0:'x11-base/xorg-server-1.6.5-r203',
                                 1:'x11-base/xorg-server-1.7.6-r8'}
  """
  version_map = {}
  i = 0
  for deps_map in input_deps:
    for full_name, dep in deps_map.iteritems():
      pkg = UnversionedName(dep)
      entry = version_map.setdefault(pkg, {})
      entry[i] = full_name
    i += 1
  return version_map


def GetDivergentSet(version_map, count):
  """Gets the set of divergent packages.

  Divergent packages are those that have a different version among the input
  dependency graphs (or missing version altogether).
  """
  divergent_set = set()
  for pkg, value in version_map.iteritems():
    if len(value.keys()) != count or len(set(value.values())) > 1:
      # The package doesn't exist for at least one ot the input, or there are
      # more than 2 versions.
      divergent_set.add(pkg)
  return divergent_set


def BuildDependencyGraph(pkg, input_deps, version_map, divergent_set):
  graph = dot_helper.Graph(pkg)

  # A subgraph for the divergent package we're considering. Add all its
  # versions as a sink.
  pkg_subgraph = graph.AddNewSubgraph('sink')

  # The outer packages are those that aren't divergent but depend on a
  # divergent package. Add them in their own subgraph, as sources.
  outer_subgraph = graph.AddNewSubgraph('source')

  emitted = set()
  for i in xrange(0, len(input_deps)):
    try:
      pkg_name = version_map[pkg][i]
    except KeyError:
      continue

    color = GetColor(i)

    if pkg_name not in emitted:
      pkg_subgraph.AddNode(pkg_name, pkg_name, color, None)
      emitted.add(pkg_name)

    # Add one subgraph per version for generally better layout.
    subgraph = graph.AddNewSubgraph()

    nodes = GetReverseDependencyClosure(pkg_name, input_deps[i], divergent_set)
    for node_name in nodes:
      if node_name not in emitted:
        subgraph.AddNode(node_name, node_name, color, None)
        emitted.add(node_name)

      # Add outer packages, and all the arcs.
      for dep in input_deps[i][node_name]['rev_deps']:
        dep_node = input_deps[i][dep]
        if (UnversionedName(dep_node) not in divergent_set
            and dep not in emitted):
          outer_subgraph.AddNode(dep, dep, NORMAL_COLOR, None)
          emitted.add(dep)
        graph.AddArc(dep, node_name)

  return graph


def main(argv):
  parser = optparse.OptionParser(
      usage='usage: %prog [options] input1 input2...')
  parser.add_option('-f', '--format', default='svg',
                    help='Dot output format (png, svg, etc.).')
  parser.add_option('-o', '--output-dir', default='.',
                    help='Output directory.')
  parser.add_option('-s', '--save-dot', action='store_true',
                    help='Save dot files.')
  options, inputs = parser.parse_args(argv)

  input_deps = []
  for i in inputs:
    with open(i) as handle:
      input_deps.append(json.loads(handle.read()))

  version_map = GetVersionMap(input_deps)
  divergent_set = GetDivergentSet(version_map, len(input_deps))

  # Get all the output directories
  all_dirs = set(os.path.dirname(pkg) for pkg in divergent_set)

  for i in all_dirs:
    try:
      os.makedirs(os.path.join(options.output_dir, i))
    except OSError:
      # The directory already exists.
      pass

  for pkg in divergent_set:
    filename = os.path.join(options.output_dir, pkg) + '.' + options.format

    save_dot_filename = None
    if options.save_dot:
      save_dot_filename = filename + '.dot'

    graph = BuildDependencyGraph(pkg, input_deps, version_map, divergent_set)
    lines = graph.Gen()
    dot_helper.GenerateImage(lines, filename, options.format, save_dot_filename)
