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

import logging
import os
import re

from analysis.constants import CHROMIUM_REPO_URL
from analysis.constants import CHROMIUM_ROOT_PATH

GENERATED_CODE_FILE_PATH_PATTERN = re.compile(r'.*out/[^/]+/gen/')
THIRD_PARTY_FILE_PATH_MARKER = 'third_party'

JAVA_CHROMIUM_FUNCTION_MARKER = 'org.chromium.chrome.browser'
JAVA_CHROMIUM_CODE_REPOSITORY_PATH = 'src/chrome/android/java/src'


def GetFullPathForJavaFrame(function):
  """Uses java function package name to normalize and generate full file path.

  Args:
    function: Java function, for example, 'org.chromium.CrAct.onDestroy'.

  Returns:
    A string of normalized full path, for example, org/chromium/CrAct.java
  """
  file_path = '%s.java' % '/'.join(function.split('.')[:-1])
  if JAVA_CHROMIUM_FUNCTION_MARKER in function:
    file_path = '%s/%s' % (JAVA_CHROMIUM_CODE_REPOSITORY_PATH, file_path)

  return file_path


def GetCrashedLineRange(line_range_str):
  """Gets crashed line range.

  Args:
    line_range_str (str): Has the format of 'line_number:range' if crash line
      range provided, else 'line_number'.

  Returns:
    A list of line numbers (int) where crash happened. For example, 655:1 has
    crashed lines 655 and 656.
  """
  if ':' not in line_range_str:
    return [int(line_range_str)]

  line_number, range_length = map(int, line_range_str.split(':'))

  return range(line_number, line_number + range_length + 1)


def RemovePrefix(string, prefix):
  """Removes prefix from a string."""
  return string[len(prefix):] if string.startswith(prefix) else string


def GetDepPathAndNormalizedFilePath(path, deps, is_java=False,
                                    root_path=None,
                                    root_repo_url=None):
  """Determines the dep of a file path and normalizes the path.

  Args:
    path (str): Represents a path.
    deps (dict): Map dependency path to its corresponding Dependency.
    is_java (boolean): If the path is a java stack path.

  Returns:
    A tuple - (dep_path, normalized_path, repo_url)
    dep_path (str): Dependency path of this path.  (e.g 'src', 'src/v8'
      ...etc), '' is no match found.
    normalized_path (str): Normalized relative file path starting from dep_path.
    repo_url (str): Repository url corresponding to dep_path.
  """
  # Default the root repo to chromium if None provided.
  root_path = root_path or CHROMIUM_ROOT_PATH
  root_repo_url = root_repo_url or CHROMIUM_REPO_URL

  # According to crbug.com/810426, a breakage from something in build,
  # that caused paths relative to out/*asan directory. So strip the ../../ in
  # the front of file path.
  path = RemovePrefix(path, '../../')

  # First normalize the path by retrieving the normalized path.
  normalized_path = os.path.normpath(path).replace('\\', '/')

  if GENERATED_CODE_FILE_PATH_PATTERN.match(normalized_path):
    logging.info('Generated code path %s', normalized_path)
    return '', normalized_path, None

  # Iterate through all dep paths in the parsed DEPS in an order.
  for dep_path in sorted(deps.keys(), key=lambda path: -path.count('/')):
    # trim the 'src' in the beginning of the dep_path to match, because there
    # are many cases, especially in linux platform, the file paths are like,
    # 'third_party/WebKit/Source/...', or 'v8/...'.
    trimmed_dep_path = dep_path
    if (trimmed_dep_path.startswith(root_path + '/') and
        trimmed_dep_path != root_path):
      trimmed_dep_path = trimmed_dep_path[len(root_path + '/'):]

    # We need to consider when the lowercased dep path is in the path,
    # because syzyasan builds and win canary crashes return lowercased file
    # path.
    trimmed_dep_path_lower = trimmed_dep_path.lower()
    dep_path_lower = dep_path.lower()

    current_dep_path = ''

    if (normalized_path.startswith(trimmed_dep_path + '/') or
        normalized_path.startswith(trimmed_dep_path_lower + '/')):
      # Case when the retrieved path is in lowercase.
      if normalized_path.startswith(trimmed_dep_path + '/'):
        current_dep_path = trimmed_dep_path
      else:
        current_dep_path = trimmed_dep_path_lower

    # For some special cases, for example, a file like
    # 'https://chromium.googlesource.com/chromium/src/+/master/'
    # 'third_party/tcmalloc/chromium/src/common.cc', this path shouldn't be
    # matched with 'src' in between, it should be matched to the trimmed 'src'
    # in the beginning.
    if (not normalized_path.startswith(THIRD_PARTY_FILE_PATH_MARKER) and
        (dep_path + '/' in normalized_path or
         dep_path_lower + '/' in normalized_path)):
      if dep_path + '/' in normalized_path:
        current_dep_path = dep_path
      else:
        current_dep_path = dep_path_lower

    # Normalize the path by stripping everything off the dep's relative
    # path.
    if current_dep_path:
      normalized_path = normalized_path.split(current_dep_path + '/', 1)[1]
      return dep_path, normalized_path, deps[dep_path].repo_url

  # If path is a java stack path, don't default dep_path to src.
  if is_java:
    return '', normalized_path, None

  # For some crashes, the file path looks like this:
  # third_party/WebKit/Source/a.cc, the src in the beginning is trimmed, so
  # default the dep path to 'src' if no match found.
  return root_path, normalized_path, root_repo_url
