#!/usr/bin/env python2.7
#
# Copyright (c) 2013-present Facebook. All rights reserved.
#

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import argparse
import json
import os
import sys

# Infer imports
import utils

BASE_INDENT = 2
JSON_REPORT = 'report.json'
# how many lines of context around each report
SOURCE_CONTEXT = 2


base_parser = argparse.ArgumentParser(
    description='Explore the error traces in Infer reports.')
base_parser.add_argument('-o', '--out', metavar='<directory>',
                         default=utils.DEFAULT_INFER_OUT, dest='infer_out',
                         action=utils.AbsolutePathAction,
                         help='Set the Infer results directory')
base_parser.add_argument('--only-show',
                         action='store_true',
                         help='Show the list of reports and exit')
base_parser.add_argument('--no-source',
                         action='store_true',
                         help='Do not print code excerpts')
base_parser.add_argument('--select',
                         metavar='N',
                         nargs=1,
                         help='Select bug number N. '
                         'If omitted, prompts you for input.')
base_parser.add_argument('--max-level',
                         metavar='N',
                         nargs=1,
                         help='Level of nested procedure calls to show. '
                         'Can be "max", in which case all levels are shown. '
                         'If omitted, prompts you for input.')


def describe_report(report, indent=0):
    filename = report['file']
    kind = report['kind']
    line = report['line']
    error_type = report['type']
    msg = utils.remove_bucket(report['qualifier'])
    return '{0}:{1}: {2}: {3}\n  {4}{5}\n'.format(
        filename,
        line,
        kind.lower(),
        error_type,
        ' ' * indent,
        msg,
    )


def show_error_and_exit(err, show_help):
    print(err)
    if show_help:
        print('')
        base_parser.print_help()
    exit(1)


class Tracer(object):
    def __init__(self, args, level=sys.maxsize):
        self.args = args
        self.max_level = level

        self.text = ''
        self.indent = []

    def indent_get(self):
        indent = ''
        for i in self.indent:
            indent += i
        return indent

    def indent_push(self, n=1):
        self.indent.append(n * BASE_INDENT * ' ')

    def indent_pop(self):
        return self.indent.pop()

    def add(self, s):
        self.text += self.indent_get() + s

    def newline(self):
        self.text += '\n'

    def build_node_tags(self, node):
        pass

    def build_source_context(self, source_name, report_line):
        start_line = max(1, report_line - SOURCE_CONTEXT)
        # could go beyond last line, checked in the loop
        end_line = report_line + SOURCE_CONTEXT

        n_length = len(str(end_line))
        line_number = 1
        with open(source_name) as source_file:
            for line in source_file:
                if start_line <= line_number <= end_line:
                    num = str(line_number).zfill(n_length)
                    caret = '  '
                    if line_number == report_line:
                        caret = '> '
                    self.add(num + ' ' + caret + line)
                line_number += 1

    def build_node(self, node):
        if node['level'] > self.max_level:
            return

        report_line = node['line_number']
        fname = node['filename']

        self.add('%s:%d: %s\n' % (fname,
                                  report_line,
                                  node['description']))

        if not self.args.no_source:
            self.indent_push(node['level'])
            self.build_source_context(fname, report_line)
            self.indent_pop()
            self.newline()

    def build_trace(self, trace):
        total_nodes = len(trace)
        hidden_nodes = len([None for n in trace if n['level'] > self.max_level])
        shown_nodes = total_nodes - hidden_nodes
        hidden_str = ''
        all_str = 'all '
        if hidden_nodes > 0:
            hidden_str = ' (%d steps too deeply nested)' % hidden_nodes
            all_str = ''
        self.add('Showing %s%d steps of the trace%s\n\n'
                 % (all_str, shown_nodes, hidden_str))

        for node in trace:
            self.build_node(node)

    def build_report(self, report):
        self.add(describe_report(report))
        self.newline()

        traces = json.loads(report['trace'])
        self.build_trace(traces['trace'])

    def __str__(self):
        return self.text


class Selector(object):
    def __init__(self, args, reports):
        self.args = args

        def has_trace(report):
            trace = json.loads(report['trace'])
            return len(trace['trace']) > 0
        self.reports = [report for report in reports if has_trace(report)]

    def show_choices(self):
        n = 0
        n_length = len(str(len(self)))
        for report in self.reports:
            print(str(n).rjust(n_length) + '. ' +
                  describe_report(report, n_length + 2))
            n += 1

    def prompt_number(self):
        if self.args.select is not None:
            return self.parse_report_number(self.args.select[0], True)

        self.show_choices()

        report_number = 0
        if len(self) > 1:
            report_number_str = raw_input(
                'Choose report to display (default=0): ')
            if report_number_str != '':
                report_number = self.parse_report_number(report_number_str)
        elif len(self) == 1:
            print('Auto-selecting the only report.')

        return report_number

    def prompt_level(self):
        if self.args.max_level is not None:
            return self.parse_max_level(self.args.max_level[0], True)

        max_level_str = raw_input(
            'Choose maximum level of nested procedures calls (default=max): ')
        if max_level_str == '':
            max_level = sys.maxsize
        else:
            max_level = self.parse_max_level(max_level_str)

        print('')

        return max_level

    def parse_report_number(self, s, show_help=False):
        try:
            n = int(s)
        except ValueError:
            show_error_and_exit(
                'ERROR: integer report number expected',
                show_help)

        if n >= len(self) or n < 0:
            show_error_and_exit('ERROR: invalid report number.', show_help)

        return n

    def parse_max_level(self, s, show_help=False):
        if s == 'max':
            return sys.maxsize

        try:
            n = int(s)
        except ValueError:
            show_error_and_exit(
                'ERROR: integer max level or "max" expected',
                show_help)

        if n < 0:
            show_error_and_exit('ERROR: invalid max level.', show_help)

        return n

    def __len__(self):
        return len(self.reports)


def main():
    args = base_parser.parse_args()

    with open(os.path.join(args.infer_out, JSON_REPORT)) as report_file:
        reports = json.load(report_file)

    sel = Selector(args, reports)

    if len(sel) == 0:
        print('No issues found')
        exit(0)

    if args.only_show:
        sel.show_choices()
        exit(0)

    report_number = sel.prompt_number()
    max_level = sel.prompt_level()

    tracer = Tracer(args, max_level)
    tracer.build_report(reports[report_number])
    print(tracer)


if __name__ == '__main__':
    main()