#!/usr/bin/env python3

import argparse
import fnmatch
import sys
import os
import re
import shutil
import subprocess

# ---------- actual list of lints to apply (or disapply) ----------

WANT_LINTS = """
#![cfg_attr(not(ci_arti_stable), allow(renamed_and_removed_lints))]
#![cfg_attr(not(ci_arti_nightly), allow(unknown_lints))]
#![deny(missing_docs)]
#![warn(noop_method_call)]
#![deny(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![deny(clippy::missing_panics_doc)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
#![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
"""

# ---------- list of lints to apply or disapply *in tests* ----------

TEST_LINTS = """
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
"""

# ---------- some notes about lints we might use - NOT USED by any code here ----------

SOON = """
"""

WISH_WE_COULD = """
#![warn(unused_crate_dependencies)]
"""

DECIDED_NOT = """
#![deny(clippy::redundant_pub_crate)]
#![deny(clippy::future_not_send)]
#![deny(clippy::redundant_closure_for_method_calls)]
#![deny(clippy::panic)]
#![deny(clippy::if_then_some_else_none)]
#![deny(clippy::expect_used)]
#![deny(clippy::option_if_let_else)]
#![deny(missing_debug_implementations)]
#![deny(clippy::pub_enum_variant_names)]
"""

# ---------- code for autoprocessing Rust source files ----------

PAT = re.compile(r'^ *#!\[(?:cfg_attr\(.*)?(allow|deny|warn)')

opts = None
deferred_errors = []


class ImproperFile(Exception):
    def __init__(self, lno, message):
        self.lno = lno
        self.message = message


def filter_file(lints, inp, outp, insist):
    in_lint_list = None
    found_lint_list = False
    lno = 0
    for line in inp.readlines():
        lno += 1

        line_starts = None
        line_ends = None
        line_stripped = line.lstrip(' ')
        if line_stripped.startswith("// @@ begin lint list"):
            line_starts = 'main'
        elif line_stripped.startswith("// @@ begin test lint list"):
            line_starts = 'test'
        elif line_stripped.startswith("//! <!-- @@ end lint list"):
            line_ends = 'main'
        elif line_stripped.startswith("//! <!-- @@ end test lint list"):
            line_ends = 'test'

        if line_starts:
            if in_lint_list:
                raise ImproperFile(
                    lno, 'found "@@ begin lint list" but inside lint list')
            found_lint_list = True
            in_lint_list = line_starts
            indent = line[0: len(line) - len(line_stripped)]
        elif line_ends:
            # End delimiter is Rustdoc containing an HTML comment, because rustfmt
            # *really really* hates comments that come after things.
            # Finishing the automaintained block with just a blank line is too much of a hazard.
            # It does end up in the output HTML from Rustdoc, but it is harmless there.
            if not in_lint_list:
                raise ImproperFile(
                    lno, 'found "@@ end lint list" but not inside lint list')
            if in_lint_list != line_ends:
                raise ImproperFile(lno, 'found end tag ' +
                                   line_ends+' but expected '+in_lint_list)
            if in_lint_list == 'test':
                lints = TEST_LINTS
            else:
                lints = WANT_LINTS
            for lint in lints.strip().split('\n'):
                outp.write(indent + lint + '\n')
            in_lint_list = None
        elif in_lint_list:
            if not PAT.match(line):
                raise ImproperFile(
                    lno, 'entry in lint list does not look like a lint')
            # do not send to output
            continue
        outp.write(line)
    if in_lint_list:
        raise ImproperFile(
            lno, 'missing "@@ lint list" delimiter, still in lint list at EOF')
    if insist and not found_lint_list:
        raise ImproperFile(
            lno, 'standard lint list block seems to be missing (wrong delimiters?)')


def process(lints, fn, always_insist):
    insist = (always_insist or
              fnmatch.fnmatch(fn, 'crates/*/src/lib.rs') or
              fnmatch.fnmatch(fn, 'crates/*/src/main.rs'))

    tmp_name = fn+".tmp~"
    outp = open(tmp_name, 'w')
    inp = open(fn, 'r')
    try:
        filter_file(lints, inp, outp, insist)
    except ImproperFile as e:
        print('%s:%d: %s' % (fn, e.lno, e.message), file=sys.stderr)
        deferred_errors.append(fn)
        os.remove(tmp_name)  # this tmp file is probably partial
        return

    inp.close()
    outp.close()

    if opts.check:
        if subprocess.run(['diff', '-u', '--', fn, tmp_name]).returncode != 0:
            deferred_errors.append(fn)
    else:
        shutil.move(tmp_name, fn)


def main(lints, files):
    if not os.path.exists("./crates/tor-proto/src/lib.rs"):
        print("Run this from the top level of an arti repo.")
        sys.exit(1)

    always_insist = True
    if not files:
        files = subprocess.run(['find', '.', '-name', '*.rs'],
                               stdout=subprocess.PIPE, check=True).stdout
        files = files.decode('utf-8').rstrip('\n').split('\n')
        always_insist = False

    for fn in files:
        process(lints, fn, always_insist)

    if len(deferred_errors) > 0:
        print('\n' + sys.argv[0] + ': standard lint block mismatch in the following files:\n  '
              + ', '.join(deferred_errors), file=sys.stderr)
        print('Run ' + sys.argv[0] + ' (possibly after editing it) to fix.')
        sys.exit(1)


if __name__ == '__main__':
    parser = argparse.ArgumentParser('standardise Rust lint blocks')
    parser.add_argument('--check', action='store_true')
    parser.add_argument('file', nargs='*')
    opts = parser.parse_args()

    main(WANT_LINTS, opts.file)
