#!/usr/bin/env python3
#
# boilerplate.py utility to rewrite the boilerplate with new dates.
#
# Copyright (C) 2018-2023 Free Software Foundation, Inc.
# Contributed by Gaius Mulley <gaius@glam.ac.uk>.
#
# This file is part of GNU Modula-2.
#
# GNU Modula-2 is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# GNU Modula-2 is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNU Modula-2; see the file COPYING3.  If not see
# <http://www.gnu.org/licenses/>.
#

import argparse
import datetime
import os
import sys


error_count = 0
seen_files = []
output_name = None

ISO_COPYRIGHT = 'Copyright ISO/IEC'
COPYRIGHT = 'Copyright (C)'
GNU_PUBLIC_LICENSE = 'GNU General Public License'
GNU_LESSER_GENERAL = 'GNU Lesser General'
GCC_RUNTIME_LIB_EXC = 'GCC Runtime Library Exception'
VERSION_2_1 = 'version 2.1'
VERSION_2 = 'version 2'
VERSION_3 = 'version 3'
Licenses = {VERSION_2_1: 'v2.1', VERSION_2: 'v2', VERSION_3: 'v3'}
CONTRIBUTED_BY = 'ontributed by'


def printf(fmt, *args):
    # printf - keeps C programmers happy :-)
    print(str(fmt) % args, end=' ')


def error(fmt, *args):
    # error - issue an error message.
    global error_count

    print(str(fmt) % args, end=' ')
    error_count += 1


def halt_on_error():
    if error_count > 0:
        os.sys.exit(1)


def basename(f):
    b = f.split('/')
    return b[-1]


def analyse_comment(text, f):
    # analyse_comment determine the license from the top comment.
    start_date, end_date = None, None
    contribution, summary, lic = None, None, None
    if text.find(ISO_COPYRIGHT) > 0:
        lic = 'BSISO'
        now = datetime.datetime.now()
        for d in range(1984, now.year+1):
            if text.find(str(d)) > 0:
                if start_date is None:
                    start_date = str(d)
                end_date = str(d)
        return start_date, end_date, '', '', lic
    elif text.find(COPYRIGHT) > 0:
        if text.find(GNU_PUBLIC_LICENSE) > 0:
            lic = 'GPL'
        elif text.find(GNU_LESSER_GENERAL) > 0:
            lic = 'LGPL'
        for license_ in Licenses.keys():
            if text.find(license_) > 0:
                lic += Licenses[license_]
        if text.find(GCC_RUNTIME_LIB_EXC) > 0:
            lic += 'x'
        now = datetime.datetime.now()
        for d in range(1984, now.year+1):
            if text.find(str(d)) > 0:
                if start_date is None:
                    start_date = str(d)
                end_date = str(d)
        if text.find(CONTRIBUTED_BY) > 0:
            i = text.find(CONTRIBUTED_BY)
            i += len(CONTRIBUTED_BY)
            j = text.index('. ', i)
            contribution = text[i:j]
    if text.find(basename(f)) > 0:
        i = text.find(basename(f))
        j = text.find('. ', i)
        if j < 0:
            error("summary of the file does not finish with a '.'")
            summary = text[i:]
        else:
            summary = text[i:j]
    return start_date, end_date, contribution, summary, lic


def analyse_header_without_terminator(f, start):
    text = ''
    for count, l in enumerate(open(f).readlines()):
        parts = l.split(start)
        if len(parts) > 1:
            line = start.join(parts[1:])
            line = line.strip()
            text += ' '
            text += line
        elif (l.rstrip() != '') and (len(parts[0]) > 0):
            return analyse_comment(text, f), count
    return [None, None, None, None, None], 0


def analyse_header_with_terminator(f, start, end):
    inComment = False
    text = ''
    for count, line in enumerate(open(f).readlines()):
        while line != '':
            line = line.strip()
            if inComment:
                text += ' '
                pos = line.find(end)
                if pos >= 0:
                    text += line[:pos]
                    line = line[pos:]
                    inComment = False
                else:
                    text += line
                    line = ''
            else:
                pos = line.find(start)
                if (pos >= 0) and (len(line) > len(start)):
                    before = line[:pos].strip()
                    if before != '':
                        return analyse_comment(text, f), count
                    line = line[pos + len(start):]
                    inComment = True
                elif (line != '') and (line == end):
                    line = ''
                else:
                    return analyse_comment(text, f), count
    return [None, None, None, None, None], 0


def analyse_header(f, start, end):
    # analyse_header -
    if end is None:
        return analyse_header_without_terminator(f, start)
    else:
        return analyse_header_with_terminator(f, start, end)


def add_stop(sentence):
    # add_stop - add a full stop to a sentance.
    if sentence is None:
        return None
    sentence = sentence.rstrip()
    if (len(sentence) > 0) and (sentence[-1] != '.'):
        return sentence + '.'
    return sentence


GPLv3 = """
%s

Copyright (C) %s Free Software Foundation, Inc.
Contributed by %s

This file is part of GNU Modula-2.

GNU Modula-2 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.

GNU Modula-2 is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Modula-2; see the file COPYING3.  If not see
<http://www.gnu.org/licenses/>.
"""

GPLv3x = """
%s

Copyright (C) %s Free Software Foundation, Inc.
Contributed by %s

This file is part of GNU Modula-2.

GNU Modula-2 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.

GNU Modula-2 is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

Under Section 7 of GPL version 3, you are granted additional
permissions described in the GCC Runtime Library Exception, version
3.1, as published by the Free Software Foundation.

You should have received a copy of the GNU General Public License and
a copy of the GCC Runtime Library Exception along with this program;
see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
<http://www.gnu.org/licenses/>.
"""

LGPLv3 = """
%s

Copyright (C) %s Free Software Foundation, Inc.
Contributed by %s

This file is part of GNU Modula-2.

GNU Modula-2 is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

GNU Modula-2 is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with GNU Modula-2.  If not, see <https://www.gnu.org/licenses/>.
"""

BSISO = """
Library module defined by the International Standard
   Information technology - programming languages
   BS ISO/IEC 10514-1:1996E Part 1: Modula-2, Base Language.

   Copyright ISO/IEC (International Organization for Standardization
   and International Electrotechnical Commission) %s.

   It may be freely copied for the purpose of implementation (see page
   707 of the Information technology - Programming languages Part 1:
   Modula-2, Base Language.  BS ISO/IEC 10514-1:1996).
"""

templates = {}
templates['GPLv3'] = GPLv3
templates['GPLv3x'] = GPLv3x
templates['LGPLv3'] = LGPLv3
templates['LGPLv2.1'] = LGPLv3
templates['BSISO'] = BSISO


def write_template(fo, magic, start, end, dates, contribution, summary, lic):
    if lic in templates:
        if lic == 'BSISO':
            # non gpl but freely distributed for the implementation of a
            # compiler
            text = templates[lic] % (dates)
            text = text.rstrip()
        else:
            summary = summary.lstrip()
            contribution = contribution.lstrip()
            summary = add_stop(summary)
            contribution = add_stop(contribution)
            if magic is not None:
                fo.write(magic)
                fo.write('\n')
            text = templates[lic] % (summary, dates, contribution)
            text = text.rstrip()
        if end is None:
            text = text.split('\n')
            for line in text:
                fo.write(start)
                fo.write(' ')
                fo.write(line)
                fo.write('\n')
        else:
            text = text.lstrip()
            fo.write(start)
            fo.write(' ')
            fo.write(text)
            fo.write('  ')
            fo.write(end)
            fo.write('\n')
        # add a blank comment line for a script for eye candy.
        if start == '#' and end is None:
            fo.write(start)
            fo.write('\n')
    else:
        error('no template found for: %s\n', lic)
        os.sys.exit(1)
    return fo


def write_boiler_plate(fo, magic, start, end,
                       start_date, end_date, contribution, summary, gpl):
    if start_date == end_date:
        dates = start_date
    else:
        dates = '%s-%s' % (start_date, end_date)
    return write_template(fo, magic, start, end,
                          dates, contribution, summary, gpl)


def rewrite_file(f, magic, start, end, start_date, end_date,
                 contribution, summary, gpl, lines):
    text = ''.join(open(f).readlines()[lines:])
    if output_name == '-':
        fo = sys.stdout
    else:
        fo = open(f, 'w')
    fo = write_boiler_plate(fo, magic, start, end,
                            start_date, end_date, contribution, summary, gpl)
    fo.write(text)
    fo.flush()
    if output_name != '-':
        fo.close()


def handle_header(f, magic, start, end):
    # handle_header keep reading lines of file, f, looking for start, end
    # sequences and comments inside.  The comments are checked for:
    # date, contribution, summary
    global error_count

    error_count = 0
    [start_date, end_date,
     contribution, summary, lic], lines = analyse_header(f, start, end)
    if lic is None:
        error('%s:1:no GPL found at the top of the file\n', f)
    else:
        if args.verbose:
            printf('copyright: %s\n', lic)
            if (start_date is not None) and (end_date is not None):
                if start_date == end_date:
                    printf('dates = %s\n', start_date)
                else:
                    printf('dates = %s-%s\n', start_date, end_date)
            if summary is not None:
                printf('summary: %s\n', summary)
            if contribution is not None:
                printf('contribution: %s\n', contribution)
        if start_date is None:
            error('%s:1:no date found in the GPL at the top of the file\n', f)
        if args.contribution is None:
            if contribution == '':
                error('%s:1:no contribution found in the ' +
                      'GPL at the top of the file\n', f)
            else:
                contribution = args.contribution
        if summary is None:
            if args.summary == '':
                error('%s:1:no single line summary found in the ' +
                      'GPL at the top of the file\n', f)
            else:
                summary = args.summary
    if error_count == 0:
        now = datetime.datetime.now()
        if args.no:
            print(f, 'suppressing change as requested: %s-%s %s'
                  % (start_date, end_date, lic))
        else:
            if lic == 'BSISO':
                # don't change the BS ISO license!
                pass
            elif args.extensions:
                lic = 'GPLv3x'
            elif args.gpl3:
                lic = 'GPLv3'
            rewrite_file(f, magic, start, end, start_date,
                         str(now.year), contribution, summary, lic, lines)
    else:
        printf('too many errors, no modifications will occur\n')


def bash_tidy(f):
    # bash_tidy tidy up dates using '#' comment
    handle_header(f, '#!/bin/bash', '#', None)


def python_tidy(f):
    # python_tidy tidy up dates using '#' comment
    handle_header(f, '#!/usr/bin/env python3', '#', None)


def bnf_tidy(f):
    # bnf_tidy tidy up dates using '--' comment
    handle_header(f, None, '--', None)


def c_tidy(f):
    # c_tidy tidy up dates using '/* */' comments
    handle_header(f, None, '/*', '*/')


def m2_tidy(f):
    # m2_tidy tidy up dates using '(* *)' comments
    handle_header(f, None, '(*', '*)')


def in_tidy(f):
    # in_tidy tidy up dates using '#' as a comment and check
    # the first line for magic number.
    first = open(f).readlines()[0]
    if (len(first) > 0) and (first[:2] == '#!'):
        # magic number found, use this
        handle_header(f, first, '#', None)
    else:
        handle_header(f, None, '#', None)


def do_visit(args, dirname, names):
    # do_visit helper function to call func on every extension file.
    global output_name
    func, extension = args
    for f in names:
        if len(f) > len(extension) and f[-len(extension):] == extension:
            output_name = f
            func(os.path.join(dirname, f))


def visit_dir(startDir, ext, func):
    # visit_dir call func for each file in startDir which has ext.
    global output_name, seen_files
    for dirName, subdirList, fileList in os.walk(startDir):
        for fname in fileList:
            if (len(fname) > len(ext)) and (fname[-len(ext):] == ext):
                fullpath = os.path.join(dirName, fname)
                output_name = fullpath
                if not (fullpath in seen_files):
                    seen_files += [fullpath]
                    func(fullpath)
            # Remove the first entry in the list of sub-directories
            # if there are any sub-directories present
        if len(subdirList) > 0:
            del subdirList[0]


def find_files():
    # find_files for each file extension call the appropriate tidy routine.
    visit_dir(args.recursive, '.h.in', c_tidy)
    visit_dir(args.recursive, '.in', in_tidy)
    visit_dir(args.recursive, '.sh', in_tidy)
    visit_dir(args.recursive, '.py', python_tidy)
    visit_dir(args.recursive, '.c', c_tidy)
    visit_dir(args.recursive, '.h', c_tidy)
    visit_dir(args.recursive, '.cc', c_tidy)
    visit_dir(args.recursive, '.def', m2_tidy)
    visit_dir(args.recursive, '.mod', m2_tidy)
    visit_dir(args.recursive, '.bnf', bnf_tidy)


def handle_arguments():
    # handle_arguments create and return the args object.
    parser = argparse.ArgumentParser()
    parser.add_argument('-c', '--contribution',
                        help='set the contribution string ' +
                        'at the top of the file.',
                        default='', action='store')
    parser.add_argument('-d', '--debug', help='turn on internal debugging.',
                        default=False, action='store_true')
    parser.add_argument('-f', '--force',
                        help='force a check to insist that the ' +
                        'contribution, summary and GPL exist.',
                        default=False, action='store_true')
    parser.add_argument('-g', '--gplv3', help='change to GPLv3',
                        default=False, action='store_true')
    parser.add_argument('-o', '--outputfile', help='set the output file',
                        default='-', action='store')
    parser.add_argument('-r', '--recursive',
                        help='recusively scan directory for known file ' +
                        'extensions (.def, .mod, .c, .h, .py, .in, .sh).',
                        default='.', action='store')
    parser.add_argument('-s', '--summary',
                        help='set the summary line for the file.',
                        default=None, action='store')
    parser.add_argument('-u', '--update', help='update all dates.',
                        default=False, action='store_true')
    parser.add_argument('-v', '--verbose',
                        help='display copyright, ' +
                        'date and contribution messages',
                        action='store_true')
    parser.add_argument('-x', '--extensions',
                        help='change to GPLv3 with GCC runtime extensions.',
                        default=False, action='store_true')
    parser.add_argument('-N', '--no',
                        help='do not modify any file.',
                        action='store_true')
    args = parser.parse_args()
    return args


def has_ext(name, ext):
    # has_ext return True if, name, ends with, ext.
    if len(name) > len(ext):
        return name[-len(ext):] == ext
    return False


def single_file(name):
    # single_file scan the single file for a GPL boilerplate which
    # has a GPL, contribution field and a summary heading.
    if has_ext(name, '.def') or has_ext(name, '.mod'):
        m2_tidy(name)
    elif has_ext(name, '.h') or has_ext(name, '.c') or has_ext(name, '.cc'):
        c_tidy(name)
    elif has_ext(name, '.in'):
        in_tidy(name)
    elif has_ext(name, '.sh'):
        in_tidy(name)  # uses magic number for actual sh/bash
    elif has_ext(name, '.py'):
        python_tidy(name)


def main():
    # main - handle_arguments and then find source files.
    global args, output_name
    args = handle_arguments()
    output_name = args.outputfile
    if args.recursive:
        find_files()
    elif args.inputfile is None:
        print('an input file must be specified on the command line')
    else:
        single_file(args.inputfile)
    halt_on_error()


main()