#!/usr/bin/env python
#
# Copyright 2009 The Closure Library Authors. All Rights Reserved.
#
# 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.


"""Generates out a Closure deps.js file given a list of JavaScript sources.

Paths can be specified as arguments or (more commonly) specifying trees
with the flags (call with --help for descriptions).

Usage: depswriter.py [path/to/js1.js [path/to/js2.js] ...]
"""

import json
import logging
import optparse
import os
import posixpath
import shlex
import sys

import source
import treescan


__author__ = 'nnaze@google.com (Nathan Naze)'


def MakeDepsFile(source_map):
  """Make a generated deps file.

  Args:
    source_map: A dict map of the source path to source.Source object.

  Returns:
    str, A generated deps file source.
  """

  # Write in path alphabetical order
  paths = sorted(source_map.keys())

  lines = []

  for path in paths:
    js_source = source_map[path]

    # We don't need to add entries that don't provide anything.
    if js_source.provides:
      lines.append(_GetDepsLine(path, js_source))

  return ''.join(lines)


def _GetDepsLine(path, js_source):
  """Get a deps.js file string for a source."""

  provides = _ToJsSrc(sorted(js_source.provides))
  requires = _ToJsSrc(sorted(js_source.requires))
  module = 'true' if js_source.is_goog_module else 'false'

  return 'goog.addDependency(\'%s\', %s, %s, %s);\n' % (
      path, provides, requires, module)


def _ToJsSrc(arr):
  """Convert a python arr to a js source string."""

  return json.dumps(arr).replace('"', '\'')


def _GetOptionsParser():
  """Get the options parser."""

  parser = optparse.OptionParser(__doc__)

  parser.add_option('--output_file',
                    dest='output_file',
                    action='store',
                    help=('If specified, write output to this path instead of '
                          'writing to standard output.'))
  parser.add_option('--root',
                    dest='roots',
                    default=[],
                    action='append',
                    help='A root directory to scan for JS source files. '
                    'Paths of JS files in generated deps file will be '
                    'relative to this path.  This flag may be specified '
                    'multiple times.')
  parser.add_option('--root_with_prefix',
                    dest='roots_with_prefix',
                    default=[],
                    action='append',
                    help='A root directory to scan for JS source files, plus '
                    'a prefix (if either contains a space, surround with '
                    'quotes).  Paths in generated deps file will be relative '
                    'to the root, but preceded by the prefix.  This flag '
                    'may be specified multiple times.')
  parser.add_option('--path_with_depspath',
                    dest='paths_with_depspath',
                    default=[],
                    action='append',
                    help='A path to a source file and an alternate path to '
                    'the file in the generated deps file (if either contains '
                    'a space, surround with whitespace). This flag may be '
                    'specified multiple times.')
  return parser


def _NormalizePathSeparators(path):
  """Replaces OS-specific path separators with POSIX-style slashes.

  Args:
    path: str, A file path.

  Returns:
    str, The path with any OS-specific path separators (such as backslash on
      Windows) replaced with URL-compatible forward slashes. A no-op on systems
      that use POSIX paths.
  """
  return path.replace(os.sep, posixpath.sep)


def _GetRelativePathToSourceDict(root, prefix=''):
  """Scans a top root directory for .js sources.

  Args:
    root: str, Root directory.
    prefix: str, Prefix for returned paths.

  Returns:
    dict, A map of relative paths (with prefix, if given), to source.Source
      objects.
  """
  # Remember and restore the cwd when we're done. We work from the root so
  # that paths are relative from the root.
  start_wd = os.getcwd()
  os.chdir(root)

  path_to_source = {}
  for path in treescan.ScanTreeForJsFiles('.'):
    prefixed_path = _NormalizePathSeparators(os.path.join(prefix, path))
    path_to_source[prefixed_path] = source.Source(source.GetFileContents(path))

  os.chdir(start_wd)

  return path_to_source


def _GetPair(s):
  """Return a string as a shell-parsed tuple.  Two values expected."""
  try:
    # shlex uses '\' as an escape character, so they must be escaped.
    s = s.replace('\\', '\\\\')
    first, second = shlex.split(s)
    return (first, second)
  except:
    raise Exception('Unable to parse input line as a pair: %s' % s)


def main():
  """CLI frontend to MakeDepsFile."""
  logging.basicConfig(format=(sys.argv[0] + ': %(message)s'),
                      level=logging.INFO)
  options, args = _GetOptionsParser().parse_args()

  path_to_source = {}

  # Roots without prefixes
  for root in options.roots:
    path_to_source.update(_GetRelativePathToSourceDict(root))

  # Roots with prefixes
  for root_and_prefix in options.roots_with_prefix:
    root, prefix = _GetPair(root_and_prefix)
    path_to_source.update(_GetRelativePathToSourceDict(root, prefix=prefix))

  # Source paths
  for path in args:
    path_to_source[path] = source.Source(source.GetFileContents(path))

  # Source paths with alternate deps paths
  for path_with_depspath in options.paths_with_depspath:
    srcpath, depspath = _GetPair(path_with_depspath)
    path_to_source[depspath] = source.Source(source.GetFileContents(srcpath))

  # Make our output pipe.
  if options.output_file:
    out = open(options.output_file, 'w')
  else:
    out = sys.stdout

  out.write(('// This file was autogenerated by %s.\n' %
             os.path.basename(__file__)))
  out.write('// Please do not edit.\n')

  out.write(MakeDepsFile(path_to_source))


if __name__ == '__main__':
  main()
