#! /usr/bin/env python3

"""
    A script that manages the VERSION of gregorio.

    See VersionUpdate.py -h for help

    Copyright (C) 2015-2025 The Gregorio Project (see CONTRIBUTORS.md)

    This file is part of Gregorio.

    Gregorio 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 of the License, or
    (at your option) any later version.

    Gregorio 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 Gregorio.  If not, see <http://www.gnu.org/licenses/>.
"""


import sys
import re
import argparse
import subprocess
import time
import os
import locale
import linecache
from datetime import date

locale.setlocale(locale.LC_TIME, 'C')

os.chdir(sys.path[0])

VERSION_FILE = '.gregorio-version'
CURRENTYEAR = str(date.today().year)
GREGORIO_FILES = ["configure.ac",
                  "ctan-o-mat.config",
                  "windows/gregorio-resources.rc",
                  "macosx/Gregorio.pkgproj",
                  "macosx/douninstall.sh",
                  "windows/gregorio.iss",
                  "doc/GregorioRef.tex",
                  "tex/gregoriotex.sty",
                  "tex/gregoriotex.lua",
                  "tex/gregoriotex.tex",
                  "tex/gregoriotex-chars.tex",
                  "tex/gregoriotex-signs.tex",
                  "tex/gregoriotex-signs.lua",
                  "tex/gregoriotex-spaces.tex",
                  "tex/gregoriotex-symbols.tex",
                  "tex/gregoriotex-symbols.lua",
                  "tex/gregoriotex-syllable.tex",
                  "tex/gregoriotex-main.tex",
                  "tex/gregoriotex-nabc.tex",
                  "tex/gregoriotex-nabc.lua",
                  "tex/gregoriosyms.sty",
                  "tex/gregoriotex-common.tex",
                  "tex/gregoriotex-gsp-default.tex",
                  "fonts/squarize.py",
                 ]
COPYRIGHT_FILES = ["install-gtex.sh",
                   "build-ctan.sh",
                   "ctan_upload.sh",
                   "tex/gregoriotex-signs.tex",
                   "tex/gregorio-vowels.dat",
                   "tex/gregoriotex-gsp-default.tex",
                   "tex/gregoriotex-nabc.lua",
                   "tex/gregoriotex-symbols.lua",
                   "tex/gregoriotex-chars.tex",
                   "tex/gregoriotex-main.tex",
                   "tex/gregoriotex-spaces.tex",
                   "tex/Makefile.am",
                   "tex/gregoriotex-common.tex",
                   "tex/gregoriotex-syllable.tex",
                   "tex/gregoriotex.lua",
                   "tex/gregoriotex.sty",
                   "tex/gregoriosyms.sty",
                   "tex/gregoriotex-nabc.tex",
                   "tex/gregoriotex.tex",
                   "tex/gregoriotex-signs.lua",
                   "tex/gregoriotex-symbols.tex",
                   "contrib/TeXShop/Makefile.am",
                   "contrib/vim/Makefile.am",
                   "contrib/vim/ftdetect/Makefile.am",
                   "contrib/vim/syntax/Makefile.am",
                   "contrib/900_gregorio.xml",
                   "contrib/Makefile.am",
                   "contrib/gabc.lang",
                   "contrib/gprocess",
                   "contrib/checkSyllabation.py",
                   "configure.ac",
                   "examples/Makefile.am",
                   "Makefile.am",
                   "install.sh",
                   "debian/copyright",
                   "debian/manpage.xml",
                   "doc/Command_Index_User.tex",
                   "doc/Makefile.am",
                   "doc/GregorioRef.tex",
                   "doc/Command_Index_gregorio.tex",
                   "doc/Command_Index_internal.tex",
                   "doc/GregorioNabcRef.tex",
                   "doc/Gabc.tex",
                   "doc/GregorioRef.lua",
                   "doc/Appendix_Font_Tables.tex",
                   "doc/gsp-sample.tex",
                   "VersionManager.py",
                   "coverage.sh",
                   "COPYING.md",
                   "src/characters.h",
                   "src/plugins.h",
                   "src/gregoriotex/gregoriotex-write.c",
                   "src/gregoriotex/gregoriotex.h",
                   "src/gregoriotex/gregoriotex-position.c",
                   "src/unicode.c",
                   "src/unicode.h",
                   "src/messages.h",
                   "src/support.c",
                   "src/sha1.h",
                   "src/messages.c",
                   "src/support.h",
                   "src/sha1.c",
                   "src/struct.h",
                   "src/bool.h",
                   "src/struct_iter.h",
                   "src/Makefile.am",
                   "src/dump/dump.c",
                   "src/encode_utf8strings.c",
                   "src/enum_generator.h",
                   "src/gabc/gabc-score-determination.c",
                   "src/gabc/gabc-elements-determination.c",
                   "src/gabc/gabc-write.c",
                   "src/gabc/gabc-notes-determination.l",
                   "src/gabc/gabc.h",
                   "src/gabc/gabc-score-determination.l",
                   "src/gabc/gabc-score-determination.y",
                   "src/gabc/gabc-score-determination.h",
                   "src/gabc/gabc-glyphs-determination.c",
                   "src/utf8strings.h.in",
                   "src/config.h",
                   "src/characters.c",
                   "src/vowel/vowel-rules.y",
                   "src/vowel/vowel.h",
                   "src/vowel/vowel-rules.h",
                   "src/vowel/vowel-rules.l",
                   "src/vowel/vowel.c",
                   "src/gregorio-utils.c",
                   "src/struct.c",
                   "fonts/gregorio-base.sfd",
                   "fonts/granapadano-base.sfd",
                   "fonts/squarize.py",
                   "fonts/convertsfdtottf.py",
                   "fonts/Makefile.am",
                   "fonts/simplify.py",
                   "fonts/stemsschemas.py",
                   "fonts/greciliae-base.sfd",
                   "fonts/gregall.sfd",
                   "fonts/grelaon.sfd",
                   "fonts/gresgmodern.sfd",
                   "fonts/install_supp_fonts.lua",
                   "windows/gregorio.iss",
                   "windows/install.lua",
                   "windows/uninstall.lua",
                   "macosx/Makefile.am",
                  ]

def strtobool(val):
    """Convert a string representation of truth to true (1) or false (0).
    True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
    are 'n', 'no', 'f', 'false', 'off', and '0'.  Raises ValueError if
    'val' is anything else.
    """
    val = val.lower()
    if val in ('y', 'yes', 't', 'true', 'on', '1'):
        return 1
    if val in ('n', 'no', 'f', 'false', 'off', '0'):
        return 0
    raise ValueError(f'invalid truth value: {val}')

def get_parser():
    "Return command line parser"
    parser = argparse.ArgumentParser(
        description='A script to manage the VERSION of gregorio.')
    parser.add_argument('-ni', '--not-interactive',
                        help='Do not ask confirmation',
                        action='store_true', default=False)
    parser.add_argument('-c', '--get-current',
                        help='Prints the current gregorio version',
                        action='store_true', default=False)
    parser.add_argument('-d', '--get-debian-stable',
                        help='Prints the version for Debian stable package',
                        action='store_true', default=False)
    parser.add_argument('-dg', '--get-debian-git',
                        help='Prints the version for Debian git package',
                        action='store_true', default=False)

    modify = parser.add_mutually_exclusive_group()
    modify.add_argument('-b', '--beta',
                        help='Increment -betax+1',
                        action='store_true', default=False,
                        dest='beta')
    modify.add_argument('--manual=',
                        help='Manually set the version.',
                        action='store',
                        dest='manual_version')
    modify.add_argument('-m', '--major',
                        help='Increment the major version: x+1.0.0-beta1',
                        action='store_true', default=False,
                        dest='major')
    modify.add_argument('-e', '--enhancement',
                        help='Increment the minor version: x.y+1.0-beta1',
                        action='store_true', default=False,
                        dest='minor')
    modify.add_argument('-p', '--patch',
                        help='Increment the patch version: x.y.z+1',
                        action='store_true', default=False,
                        dest='patch')
    modify.add_argument('-rc', '--release-candidate',
                        help='Change version to a -rc1, or increment -rcx+1',
                        action='store_true', default=False,
                        dest='release_candidate')
    modify.add_argument('-r', '--release',
                        help='Make a release. Removes -rcx',
                        action='store_true', default=False,
                        dest='release')
    return parser

class Version():
    "Class for version manipulation."

    def __init__(self, versionfile):
        self.versionfile = versionfile
        self.version = linecache.getline(self.versionfile, 1).strip('\n')
        self.filename_version = self.filename_version_from_version(self.version)
        self.short_tag = None
        self.date = None
        self.binary_version = self.binary_version_from_version(self.version)

    def filename_version_from_version(self, version):
        "Return filename-compatible version"
        return version.replace('.', '_')

    def binary_version_from_version(self, version):
        "Return binary version number for Windows FILEVERSION"
        binary = version.replace('.', ',')
        if '-' in binary:
            binary = binary.replace('-beta', ',1')
            binary = binary.replace('-rc', ',2')
        else:
            binary += ',30'
        return binary

    def fetch_version(self):
        "Prints version"
        print(self.version)
        sys.exit(0)

    def fetch_version_debian_stable(self):
        "Prints version for Debian stable package and exits"
        print(self.version.replace('-', '~'))
        sys.exit(0)

    def fetch_version_debian_git(self):
        "Prints version for Debian git package and exits"
        self.short_tag = subprocess.check_output(
            ['git', 'rev-parse', '--short', 'HEAD'])
        self.short_tag = self.short_tag.strip('\n')
        self.date = time.strftime("%Y%m%d%H%M%S")
        print(f"{self.version.replace('-', '~')}+git{self.date}+{self.short_tag}")
        sys.exit(0)

    def update_version(self, newversion):
        "Update self.version and .gregorio-version with the new version."
        self.version = newversion
        self.filename_version = self.filename_version_from_version(newversion)
        self.binary_version = self.binary_version_from_version(newversion)
        print(f'Updating {self.versionfile} with the new version: {self.version}\n')
        with open(self.versionfile, 'w', encoding='utf-8') as verfile:
            verfile.write(f'{self.version}\n{CURRENTYEAR}')
            verfile.write('\n\n*** Do not modify this file. ***\n')
            verfile.write('Use VersionManager.py to change the version.\n')

def replace_version(version_obj):
    "Change version in file according to heuristics."
    newver = version_obj.version
    newver_filename = version_obj.filename_version
    newbinver = version_obj.binary_version
    today = date.today()
    print(f'Updating source files to version {newver}\n')
    for myfile in GREGORIO_FILES:
        result = []
        following_line_filename = False
        with open(myfile, 'r', encoding='utf-8') as infile:
            for line in infile:
                if 'AC_INIT([' in line:
                    result.append(re.sub(r'(\d+\.\d+\.\d+(?:[-+~]\w+)*)', newver, line, 1))
                elif 'AppVersion' in line:
                    result.append(re.sub(r'(\d+\.\d+\.\d+(?:[-+~]\w+)*)', newver, line, 1))
                elif 'FILENAME_VERSION' in line:
                    result.append(re.sub(r'(\d+\_\d+\_\d+(?:[-+~]\w+)*)', newver_filename, line, 1))
                elif 'FileVersion' in line:
                    result.append(re.sub(r'(\d+\.\d+\.\d+(?:[-+~]\w+)*)', newver, line, 1))
                elif 'ProductVersion' in line:
                    result.append(re.sub(r'(\d+\.\d+\.\d+(?:[-+~]\w+)*)', newver, line, 1))
                elif 'GREGORIO_VERSION' in line:
                    result.append(re.sub(r'(\d+\.\d+\.\d+(?:[-+~]\w+)*)', newver, line, 1))
                elif 'GREGORIO_DATE_LTX' in line:
                    result.append(re.sub(r'(\d+\/\d+/\d+)', today.strftime("%Y/%m/%d"), line, 1))
                elif 'PARSE_VERSION_DATE_LTX' in line:
                    newline = re.sub(r'(\d+\.\d+\.\d+(?:[-+~]\w+)*)', newver, line, 1)
                    result.append(re.sub(r'(\d+\/\d+/\d+)', today.strftime("%Y/%m/%d"), newline, 1))
                elif 'PARSE_VERSION_DATE' in line:
                    newline = re.sub(r'(\d+\.\d+\.\d+(?:[-+~]\w+)*)', newver, line, 1)
                    result.append(re.sub(r'(\d{1,2} [A-Z][a-z]+ \d{4})',
                                         today.strftime("%-d %B %Y"), newline, 1))
                elif 'FILEVERSION' in line:
                    result.append(re.sub(r'\d+,\d+,\d+,\d+', newbinver, line, 1))
                elif 'PRODUCTVERSION' in line:
                    result.append(re.sub(r'\d+,\d+,\d+,\d+', newbinver, line, 1))
                elif 'PARSE_VERSION_FILE_NEXTLINE' in line:
                    result.append(line)
                    following_line_filename = True
                elif following_line_filename:
                    result.append(re.sub(r'(\d+\_\d+\_\d+(?:[-+~]\w+)*)', newver_filename, line, 1))
                    following_line_filename = False
                else:
                    result.append(line)
        with open(myfile, 'w', encoding='utf-8') as outfile:
            outfile.write(''.join(result))
    sys.exit(0)

def update_changelog(newver, upgradetype):
    "Insert the version number into CHANGELOG"
    today = date.today()
    with open('CHANGELOG.md', 'r', encoding='utf-8') as infile:
        result = []
        develop = False
        for line in infile:
            if upgradetype == "patch":
                if '[Unreleased][develop]' in line:
                    print("Found an unreleased develop section.")
                    print("Patch releases should be based on ctan branch.")
                    sys.exit(1)
                if '[Unreleased][CTAN]' in line:
                    result.append(line)
                    result.append('\n')
                    result.append('\n')
                    newline = '## [' + newver + '] - ' + today.strftime("%Y-%m-%d") + '\n'
                    result.append(newline)
                else:
                    result.append(line)
            else:
                if '[Unreleased][develop]' in line:
                    develop = True
                    result.append(line)
                    result.append('\n')
                    result.append('\n')
                    result.append('## [Unreleased][CTAN]\n')
                    result.append('\n')
                    result.append('\n')
                    newline = '## [' + newver + '] - ' + today.strftime("%Y-%m-%d") + '\n'
                    result.append(newline)
                elif '[Unreleased][CTAN]' in line and develop:
                    continue
                else:
                    result.append(line)
        if not develop and upgradetype != "patch":
            print("I didn't find a unreleased develop section.")
            print("Non-patch releases should be based on develop branch.")
            sys.exit(1)
    with open('CHANGELOG.md', 'w', encoding='utf-8') as outfile:
        outfile.write(''.join(result))

def confirm_replace(oldver, newver):
    "Query the user to confirm action"
    query = f'Update version from {oldver} --> {newver} [y/n]?'
    print(query)
    consent = None
    while True:
        try:
            consent = strtobool(input().lower())
            break
        except ValueError:
            print('Answer with y or n.')
    if not consent:
        print('Aborting.')
        sys.exit(1)

def release_candidate(version_obj, not_interactive):
    "Changes x.y.z-beta to x.y.z-rc1 OR increments x.y.z-rcx+1"
    oldversion = version_obj.version
    if '-rc' in oldversion:
        newversion = re.sub(r'\d+$', lambda x: str(int(x.group()) +1),
                            oldversion)
    elif '-' in oldversion:
        newversion = re.sub(r'-.*', '-rc1', oldversion)
    if (not not_interactive):
        confirm_replace(oldversion, newversion)
    update_changelog(newversion,"releasecandidate")
    version_obj.update_version(newversion)
    replace_version(version_obj)

def beta(version_obj, not_interactive):
    "Increments x.y.z-betax+1"
    oldversion = version_obj.version
    if '-beta' in oldversion:
        newversion = re.sub(r'\d+$', lambda x: str(int(x.group()) +1),
                            oldversion)
    else:
        print('Version number not in beta stage')
        print('Use --major (-m) or --enhancement (-e) to start a new beta')
        sys.exit(1)
    if (not not_interactive):
        confirm_replace(oldversion, newversion)
    update_changelog(newversion,"beta")
    version_obj.update_version(newversion)
    replace_version(version_obj)

def bump_major(version_obj, not_interactive):
    "Changed the major version number: x.y.z --> x+1.0.0-beta1"
    oldversion = version_obj.version
    nums = re.search(r'(\d+)(\.\d+)(\.\d+)', oldversion)
    newversion = str(int(nums.group(1)) +1) + '.0.0-beta1'
    if (not not_interactive):
        confirm_replace(oldversion, newversion)
    update_changelog(newversion,"major")
    version_obj.update_version(newversion)
    replace_version(version_obj)

def bump_minor(version_obj, not_interactive):
    "Changed the minor version number: x.y.z --> x.y+1.0-beta1"
    oldversion = version_obj.version
    nums = re.search(r'(\d+\.)(\d+)(\.\d+)', oldversion)
    newversion = nums.group(1) + str(int(nums.group(2)) +1) + '.0-beta1'
    if (not not_interactive):
        confirm_replace(oldversion, newversion)
    update_changelog(newversion,"minor")
    version_obj.update_version(newversion)
    replace_version(version_obj)

def bump_patch(version_obj, not_interactive):
    "Changed the patch version number: x.y.z --> x.y.z+1"
    oldversion = version_obj.version
    nums = re.search(r'(\d+\.\d+\.)(\d+)', oldversion)
    newversion = nums.group(1) + str(int(nums.group(2)) +1)
    if (not not_interactive):
        confirm_replace(oldversion, newversion)
    update_changelog(newversion,"patch")
    version_obj.update_version(newversion)
    replace_version(version_obj)

def set_manual_version(version_obj, user_version, not_interactive):
    "Changed the version number to a user supplied value"
    oldversion = version_obj.version
    if not re.match(r'(\d+\.\d+\.\d+(?:[-+~]\w+)*)$', user_version):
        print('Bad version string. Use this style: x.y.z or x.y.z-betax')
        sys.exit(1)
    newversion = user_version
    if (not not_interactive):
        confirm_replace(oldversion, newversion)
    update_changelog(newversion,"manual")
    version_obj.update_version(newversion)
    replace_version(version_obj)

def do_release(version_obj, not_interactive):
    "Strips extra characters from the version leaving x.y.z"
    oldversion = version_obj.version
    newversion = re.sub(r'([\d.]+)-?.*', r'\1', oldversion)
    if (not not_interactive):
        confirm_replace(oldversion, newversion)
    update_changelog(newversion,"release")
    version_obj.update_version(newversion)
    replace_version(version_obj)

def copyright_year():
    "Check and update copyright year as needed"
    fileyear = linecache.getline(VERSION_FILE, 2).strip()
    def year_range(matchobj):
        "Check and add a year range to the copyright"
        if matchobj.group(1) is not None:
            return re.sub(fileyear, CURRENTYEAR, matchobj.group(0))
        return re.sub(fileyear, fileyear+'-'+CURRENTYEAR, matchobj.group(0))

    if int(fileyear) != int(CURRENTYEAR):
        print('Updating copyright year.')
        for myfile in COPYRIGHT_FILES:
            result = []
            with open(myfile, 'r', encoding='utf-8') as infile:
                for line in infile:
                    if re.search(r'[C|c]opyright.*Gregorio Project', line):
                        result.append(re.sub(r'(\d{4}-)?(\d{4})', year_range, line))
                    elif re.search(r'[C|c]opyright.*Elie Roux', line):
                        result.append(re.sub(r'(\d{4}-)?(\d{4})', year_range, line))
                    elif re.search(r'[C|c]opyright.*Richard Chonak', line):
                        result.append(re.sub(r'(\d{4}-)?(\d{4})', year_range, line))
                    elif re.search(r'[C|c]opyright.*Jakub Jelinek', line):
                        result.append(re.sub(r'(\d{4}-)?(\d{4})', year_range, line))
                    else:
                        result.append(line)
            with open(myfile, 'w', encoding='utf-8') as outfile:
                outfile.write(''.join(result))

def main():
    "Main function"
    parser = get_parser()
    if len(sys.argv) == 1:
        parser.print_help()
        sys.exit(1)
    args = parser.parse_args()
    gregorio_version = Version(VERSION_FILE)
    not_interactive = False
    if args.not_interactive:
        not_interactive = True
    if args.get_current:
        gregorio_version.fetch_version()
    elif args.get_debian_stable:
        gregorio_version.fetch_version_debian_stable()
    elif args.get_debian_git:
        gregorio_version.fetch_version_debian_git()
    copyright_year()
    if args.beta:
        beta(gregorio_version, not_interactive)
    elif args.major:
        bump_major(gregorio_version, not_interactive)
    elif args.minor:
        bump_minor(gregorio_version, not_interactive)
    elif args.patch:
        bump_patch(gregorio_version, not_interactive)
    elif args.release_candidate:
        release_candidate(gregorio_version, not_interactive)
    elif args.release:
        do_release(gregorio_version, not_interactive)
    elif args.manual_version:
        set_manual_version(gregorio_version, args.manual_version, not_interactive)

if __name__ == "__main__":
    main()
