#!/usr/bin/env python3

# Copyright 2016, VIXL authors
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
#   * Neither the name of ARM Limited nor the names of its contributors may be
#     used to endorse or promote products derived from this software without
#     specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import argparse
import multiprocessing
import os
import re
import subprocess
import sys
import tempfile
import shutil

from threaded_tests import Test, TestQueue
import printer
import util

CLANG_TOOL_SUPPORTED_VERSIONS = range(11, 16)

DEFAULT_CLANG_FORMAT = 'clang-format'

CLANG_TOOL_VERSION_MATCH = r"(clang-format|LLVM) version ([\d]+)\.[\d]+\.[\d]+.*$"

is_output_redirected = not sys.stdout.isatty()

def BuildOptions():
  parser = argparse.ArgumentParser(
    description = '''This tool runs `clang-format` on C++ files.
    If no files are provided on the command-line, all C++ source files are
    processed, except for the test traces.
    When available, `colordiff` is automatically used to colour the output.''',
    # Print default values.
    formatter_class = argparse.ArgumentDefaultsHelpFormatter)
  parser.add_argument('files', nargs = '*')
  parser.add_argument('--clang-format', default=DEFAULT_CLANG_FORMAT,
                      help='Path to clang-format.')
  parser.add_argument('--in-place', '-i',
                      action = 'store_true', default = False,
                      help = 'Edit files in place.')
  parser.add_argument('--jobs', '-j', metavar = 'N', type = int, nargs = '?',
                      default = multiprocessing.cpu_count(),
                      const = multiprocessing.cpu_count(),
                      help = '''Runs the tests using N jobs. If the option is set
                      but no value is provided, the script will use as many jobs
                      as it thinks useful.''')
  return parser.parse_args()

def is_supported(tool):
  if not shutil.which(tool):
    return False

  cmd = '%s -version' % tool

  try:
    rc, version = util.getstatusoutput(cmd)
  except OSError:
    return False

  if rc != 0:
      util.abort("Failed to execute %s: %s" % (cmd, version))
  m = re.search(CLANG_TOOL_VERSION_MATCH, version, re.MULTILINE)
  if not m:
      util.abort("Failed to get clang tool version: %s" % version)
  _, major = m.groups()

  if int(major) in CLANG_TOOL_SUPPORTED_VERSIONS:
    return True

  return False

def detect_clang_tool(tool):
  supported_tools = [tool] + [tool + '-' + str(ver) for ver in CLANG_TOOL_SUPPORTED_VERSIONS]
  for tool in supported_tools:
    if is_supported(tool):
        return tool

  return None


def RunTest(test):
  filename = test.args['filename']
  clang_format = test.args['clang_format']
  in_place = test.args['in_place']

  rc = 0

  cmd_format = [clang_format, filename]
  temp_file, temp_file_name = tempfile.mkstemp(prefix = 'clang_format_')
  cmd_format_string = '$ ' + ' '.join(cmd_format) + ' > %s' % temp_file_name
  p_format = subprocess.Popen(cmd_format,
                              stdout = temp_file, stderr = subprocess.STDOUT)

  rc += p_format.wait()

  cmd_diff = ['diff', '--unified', filename, temp_file_name]
  cmd_diff_string = '$ ' + ' '.join(cmd_diff)

  p_diff = subprocess.Popen(cmd_diff,
                            stdout = subprocess.PIPE, stderr = subprocess.STDOUT)

  if util.IsCommandAvailable('colordiff') and not is_output_redirected:
    p_colordiff = subprocess.Popen(
            ['colordiff', '--unified'],
            stdin = p_diff.stdout,
            stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
    out, unused = p_colordiff.communicate()
    rc += p_colordiff.returncode
  else:
    out, unused = p_diff.communicate()
    rc += p_diff.returncode


  if in_place:
      cmd_format = [clang_format, '-i', filename]
      subprocess.run(cmd_format, stdout=temp_file, stderr=subprocess.STDOUT)

  if rc != 0:
    with Test.n_tests_failed.get_lock(): Test.n_tests_failed.value += 1
  else:
    with Test.n_tests_passed.get_lock(): Test.n_tests_passed.value += 1

  printer.__print_lock__.acquire()

  printer.UpdateProgress(test.shared.start_time,
                         Test.n_tests_passed.value,
                         Test.n_tests_failed.value,
                         test.shared.n_tests,
                         Test.n_tests_skipped.value,
                         test.shared.n_known_failures,
                         test.name,
                         prevent_next_overwrite = rc != 0,
                         has_lock = True,
                         prefix = test.shared.progress_prefix)

  if rc != 0:
    printer.Print('Incorrectly formatted file: ' + filename + '\n' + \
                  cmd_format_string + '\n' + \
                  cmd_diff_string + '\n' + \
                  out.decode(), has_lock = True)

  printer.__print_lock__.release()

  os.remove(temp_file_name)

# Returns the total number of files incorrectly formatted.
def ClangFormatFiles(files, clang_format, in_place = False, jobs = 1,
                     progress_prefix = ''):

  clang_format = detect_clang_tool("clang-format")

  if not clang_format:
    error_message = "clang-format not found. Please ensure it " \
                    "is installed, in your PATH and the correct version."
    print(printer.COLOUR_RED + error_message + printer.NO_COLOUR)
    return -1

  queue = TestQueue(prefix = progress_prefix)
  for f in files:
    queue.AddTest(f, filename = f, clang_format = clang_format, in_place = in_place)

  rc = queue.Run(jobs, True, RunTest)

  printer.PrintOverwritableLine(
      progress_prefix + '%d files are incorrectly formatted.' % rc,
      type = printer.LINE_TYPE_LINTER)
  printer.EnsureNewLine()

  return rc

if __name__ == '__main__':
  # Parse the arguments.
  args = BuildOptions()
  files = args.files or util.get_source_files(exclude_dirs=['.*', '*/traces/*', '*/aarch32/*'])

  rc = ClangFormatFiles(files, clang_format = args.clang_format,
                        in_place = args.in_place, jobs = args.jobs)

  sys.exit(rc)
