#!/usr/bin/env python

from __future__ import print_function

import argparse
import os
import re
import subprocess
import sys


def escapeCmdArg(arg):
    if '"' in arg or ' ' in arg:
        return '"%s"' % arg.replace('"', '\\"')
    else:
        return arg


def fail(errorMessage):
    print(errorMessage, file=sys.stderr)
    sys.exit(1)


def run_command(cmd):
    print(' '.join([escapeCmdArg(arg) for arg in cmd]))
    return subprocess.check_output(cmd, stderr=subprocess.STDOUT)


def parseLine(line, line_no, test_case, incremental_edit_args, reparse_args, 
              current_reparse_start):
    pre_column_offset = 1
    post_column_offset = 1
    pre_edit_line = ""
    post_edit_line = ""

    # We parse one tag at a time in the line while eating away a prefix of the 
    # line
    while line:
        # The regular expression to match the template markers
        subst_re = re.compile(r'^(.*?)<<(.*?)<(.*?)\|\|\|(.*?)>>>(.*\n?)')
        reparse_re = re.compile(r'^(.*?)<(/?)reparse ?(.*?)>(.*\n?)')
        subst_match = subst_re.match(line)
        reparse_match = reparse_re.match(line)
        if subst_match and reparse_match:
            # If both regex match use the one with the shorter prefix
            if len(subst_match.group(1)) < len(reparse_match.group(1)):
                reparse_match = None
            else:
                subst_match = None

        if subst_match:
            prefix = subst_match.group(1)
            match_test_case = subst_match.group(2)
            pre_edit = subst_match.group(3)
            post_edit = subst_match.group(4)
            suffix = subst_match.group(5)

            if match_test_case == test_case:
                pre_edit_line += prefix + pre_edit
                post_edit_line += prefix + post_edit

                # Compute the -incremental-edit argument for swift-syntax-test
                column = pre_column_offset + len(prefix)
                edit_arg = '%d:%d-%d:%d=%s' % \
                    (line_no, column, line_no, column + len(pre_edit), 
                     post_edit)
                incremental_edit_args.append('-incremental-edit')
                incremental_edit_args.append(edit_arg)
            else:
                # For different test cases just take the pre-edit text
                pre_edit_line += prefix + pre_edit
                post_edit_line += prefix + pre_edit

            line = suffix
            pre_column_offset += len(pre_edit_line)
            post_column_offset += len(post_edit_line)
        elif reparse_match:
            prefix = reparse_match.group(1)
            is_closing = len(reparse_match.group(2)) > 0
            match_test_case = reparse_match.group(3)
            suffix = reparse_match.group(4)
            if match_test_case == test_case:
                column = post_column_offset + len(prefix)
                if is_closing:
                    if not current_reparse_start:
                        fail('Closing unopened reparse tag in line %d' %
                             line_no)
                    reparse_args.append('-reparse-region')
                    reparse_args.append(
                        '%d:%d-%d:%d' % (current_reparse_start[0], 
                                         current_reparse_start[1], 
                                         line_no, column))
                    current_reparse_start = None
                else:
                    if current_reparse_start:
                        fail('Opening nested reparse tags for the same test '
                             'case in line %d' % line_no)
                    current_reparse_start = [line_no, column]

            pre_edit_line += prefix
            post_edit_line += prefix
            line = suffix
        else:
            pre_edit_line += line
            post_edit_line += line
            # Nothing more to do
            line = ''

    return (pre_edit_line, post_edit_line, current_reparse_start)


def main():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description='Utility for testing incremental syntax parsing',
        epilog='''
    Based of a single template the utility generates a pre-edit and a post-edit 
    file. It then verifies that incrementally parsing the post-edit file base 
    on the pre-edit file results in the same syntax tree as reparsing the 
    post-edit file from scratch.

    To generate the pre-edit and the post-edit file from the template, it 
    operates on markers of the form:

        <<test_case<pre|||post>>>

    These placeholders are replaced by:
      - 'pre' if a different test case than 'test_case' is run
      - 'pre' for the pre-edit version of 'test_case'
      - 'post' for the post-edit version of 'test_case'
    ''')
    parser.add_argument(
        'file', type=argparse.FileType(),
        help='The template file to test')
    parser.add_argument(
        '--temp-dir', required=True,
        help='A temporary directory where pre-edit and post-edit files can be \
              saved')
    parser.add_argument(
        '--swift-syntax-test', required=True,
        help='The path to swift-syntax-test')
    parser.add_argument(
        '--test-case', default='',
        help='The test case to execute. If no test case is specified all \
              unnamed substitutions are applied')
    parser.add_argument(
        '--print-visual-reuse-info', default=False, action='store_true',
        help='Print visual reuse information about the incremental parse \
              instead of diffing the syntax trees. This option is intended \
              for debug purposes only.')

    # Parse the arguments
    args = parser.parse_args(sys.argv[1:])
    test_file = args.file
    test_file_name = os.path.basename(test_file.name)
    temp_dir = args.temp_dir
    swift_syntax_test = args.swift_syntax_test
    test_case = args.test_case
    print_visual_reuse_info = args.print_visual_reuse_info

    if not os.path.exists(temp_dir):
        os.makedirs(temp_dir)

    # Generate pre-edit and post-edit files
    pre_edit_file = open(temp_dir + '/' + test_file_name + '.' + test_case + 
                         '.pre.swift', mode='w+')
    post_edit_file = open(temp_dir + '/' + test_file_name + '.' + test_case + 
                          '.post.swift', mode='w+')

    # Gather command line arguments for swift-syntax-test specifiying the 
    # performed edits in this list
    incremental_edit_args = []
    reparse_args = []
    current_reparse_start = None

    line_no = 1
    for line in args.file.readlines():
        parseLineRes = parseLine(line, line_no, test_case, 
                                 incremental_edit_args,
                                 reparse_args, current_reparse_start)
        (pre_edit_line, post_edit_line, current_reparse_start) = parseLineRes

        pre_edit_file.write(pre_edit_line)
        post_edit_file.write(post_edit_line)

        line_no += 1

    if current_reparse_start:
        fail('Unclosed reparse tag for test case %s' % test_case)

    pre_edit_file.close()
    post_edit_file.close()

    serialized_pre_edit_filename = pre_edit_file.name + '.serialized.json'
    serialized_incr_filename = temp_dir + '/' + test_file_name + '.' + \
        test_case + '.incr.swift.serialized.json'
    serialized_post_edit_filename = post_edit_file.name + '.serialized.json'

    try:
        # Serialise the pre-edit syntax tree
        run_command(
            [swift_syntax_test] + 
            ['-serialize-raw-tree'] +
            ['-input-source-filename', pre_edit_file.name] +
            ['-output-filename', serialized_pre_edit_filename])

        # Serialise the post-edit syntax tree from scratch
        run_command(
            [swift_syntax_test] +
            ['-serialize-raw-tree'] +
            ['-input-source-filename', post_edit_file.name] +
            ['-output-filename', serialized_post_edit_filename])

        if print_visual_reuse_info:
            print_visual_reuse_info_args = [
                '-print-visual-reuse-info', 
                '-force-colored-output'
            ]
        else:
            print_visual_reuse_info_args = []

        # Serialise the post-edit syntax tree incrementally based on the 
        # pre-edit syntax tree
        incr_parse_output = run_command(
            [swift_syntax_test] +
            ['-serialize-raw-tree'] +
            ['-old-syntax-tree-filename', serialized_pre_edit_filename] +
            ['-input-source-filename', post_edit_file.name] +
            ['--old-source-filename', pre_edit_file.name] +
            incremental_edit_args + 
            reparse_args + 
            ['-output-filename', serialized_incr_filename] +
            print_visual_reuse_info_args)

        if print_visual_reuse_info:
            print(incr_parse_output)
            exit(0)

    except subprocess.CalledProcessError as e:
        print('Test case "%s" of %s FAILed' % (test_case, test_file.name), 
              file=sys.stderr)
        print(e.output, file=sys.stderr)
        sys.exit(1)

    try:
        # Check if the two syntax trees are the same
        run_command(
            [
                'diff', '-u',
                serialized_post_edit_filename,
                serialized_incr_filename
            ])
    except subprocess.CalledProcessError as e:
        print('Test case "%s" of %s FAILed' % (test_case, test_file.name), 
              file=sys.stderr)
        print('Syntax tree of incremental parsing does not match '
              'from-scratch parsing of post-edit file:\n\n', file=sys.stderr)
        print(e.output, file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()
