#!/usr/bin/python
# 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 pretty dependency graphs for Chrome OS packages."""

import dot_helper
import optparse
import os
import sys


NORMAL_COLOR = 'black'
TARGET_COLOR = 'red'
SEED_COLOR = 'green'
CHILD_COLOR = 'grey'


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

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

  GetClosure(full_name)
  return s


def GetOutputBaseName(node, options):
  """Gets the basename of the output file for a node."""
  return '%s_%s-%s.%s' % (node['category'], node['name'], node['version'],
                          options.format)


def AddNodeToSubgraph(subgraph, node, options, color):
  """Gets the dot definition for a node."""
  name = node['full_name']
  href = None
  if options.link:
    filename = GetOutputBaseName(node, options)
    href = '%s%s' % (options.base_url, filename)
  subgraph.AddNode(name, name, color, href)



def GenerateDotGraph(package, deps_map, options):
  """Generates the dot source for the dependency graph leading to a node.

  The output is a list of lines."""
  deps = GetReverseDependencyClosure(package, deps_map)
  node = deps_map[package]

  # Keep track of all the emitted nodes so that we don't issue multiple
  # definitions
  emitted = set()

  graph = dot_helper.Graph(package)

  # Add all the children if we want them, all of them in their own subgraph,
  # as a sink. Keep the arcs outside of the subgraph though (it generates
  # better layout).
  children_subgraph = None
  if options.children and node['deps']:
    children_subgraph = graph.AddNewSubgraph('sink')
    for child in node['deps']:
      child_node = deps_map[child]
      AddNodeToSubgraph(children_subgraph, child_node, options, CHILD_COLOR)
      emitted.add(child)
      graph.AddArc(package, child)

  # Add the package in its own subgraph. If we didn't have children, make it
  # a sink
  if children_subgraph:
    rank = 'same'
  else:
    rank = 'sink'
  package_subgraph = graph.AddNewSubgraph(rank)
  AddNodeToSubgraph(package_subgraph, node, options, TARGET_COLOR)
  emitted.add(package)

  # Add all the other nodes, as well as all the arcs.
  for dep in deps:
    dep_node = deps_map[dep]
    if not dep in emitted:
      color = NORMAL_COLOR
      if dep_node['action'] == 'seed':
        color = SEED_COLOR
      AddNodeToSubgraph(graph, dep_node, options, color)
    for j in dep_node['rev_deps']:
      graph.AddArc(j, dep)

  return graph.Gen()


def GenerateImages(input, options):
  """Generate the output images for all the nodes in the input."""
  deps_map = eval(input.read())

  for package in deps_map:
    lines = GenerateDotGraph(package, deps_map, options)

    filename = os.path.join(options.output_dir,
                            GetOutputBaseName(deps_map[package], options))

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

    dot_helper.GenerateImage(lines, filename, options.format, save_dot_filename)


def main():
  parser = optparse.OptionParser(usage='usage: %prog [options] input')
  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('-c', '--children', action='store_true',
                    help='Also add children.')
  parser.add_option('-l', '--link', action='store_true',
                    help='Embed links.')
  parser.add_option('-b', '--base-url', default='',
                    help='Base url for links.')
  parser.add_option('-s', '--save-dot', action='store_true',
                    help='Save dot files.')
  (options, inputs) = parser.parse_args()

  try:
    os.makedirs(options.output_dir)
  except OSError:
    # The directory already exists.
    pass

  if not inputs:
    GenerateImages(sys.stdin, options)
  else:
    for i in inputs:
      file = open(i)
      GenerateImages(file, options)
      file.close()

if __name__ == '__main__':
  main()
