#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@author: Frank Brehm
@contact: frank@brehm.online.com
@copyright: © 2022 by Frank Brehm, Berlin
@summary: A script for converting a Debian changelog into
          log entries of a RPM spec file.
"""
from __future__ import print_function

# Standard modules
import sys
import os
import re
import warnings
import datetime
import textwrap
import logging
import argparse
import pprint
import platform

from pathlib import Path

# own modules:

my_path = Path(__file__)
my_real_path = my_path.resolve()
bin_path = my_real_path.parent
base_dir = bin_path.parent
lib_dir = base_dir.joinpath('lib')
module_dir = lib_dir.joinpath('fb_logging')

if module_dir.exists():
        sys.path.insert(0, str(lib_dir))

from fb_logging.deb_changelog import Changelog

# Own modules
from fb_logging.colored import ColoredFormatter

from fb_logging import __version__ as __pkg_version__

__version__ = '0.4.0'

LOG = logging.getLogger(__name__)


# =============================================================================
def pp(value, indent=4, width=150, depth=None):
    """
    Returns a pretty print string of the given value.

    @return: pretty print string
    @rtype: str
    """

    pretty_printer = pprint.PrettyPrinter(
        indent=indent, width=width, depth=depth)
    return pretty_printer.pformat(value)


# =============================================================================
class FileOptionAction(argparse.Action):

    # -------------------------------------------------------------------------
    def __init__(self, option_strings, *args, **kwargs):

        super(FileOptionAction, self).__init__(
            option_strings=option_strings, *args, **kwargs)

    # -------------------------------------------------------------------------
    def __call__(self, parser, namespace, values, option_string=None):

        if values is None:
            setattr(namespace, self.dest, None)
            return

        path = Path(values)
        if not path.exists():
            msg = "File {!r} does not exists.".format(values)
            raise argparse.ArgumentError(self, msg)
        if not path.is_file():
            msg = "File {!r} is not a regular file.".format(values)
            raise argparse.ArgumentError(self, msg)

        setattr(namespace, self.dest, path.resolve())


# =============================================================================
class Dch2SpecLogApp(object):
    """
    Class for the dch2speclog application.
    Converts a Debian changelog into log entries of a RPM spec file.
    """

    re_emptyline = re.compile(r'^\s*$')
    re_start_line = re.compile(r'^  \* (.*)')
    re_next_line = re.compile(r'^    (.*)')
    re_day_str = re.compile(r'\s+\d\d:\d\d:\d\d\s+[+-]?\d{4}$')

    # -------------------------------------------------------------------------
    @classmethod
    def get_generic_appname(cls, appname=None):

        if appname:
            v = str(appname).strip()
            if v:
                return v
        return os.path.basename(sys.argv[0])

    # -------------------------------------------------------------------------
    @classmethod
    def terminal_can_colors(cls, debug=False):
        """
        Method to detect, whether the current terminal (stdout and stderr)
        is able to perform ANSI color sequences.

        @return: both stdout and stderr can perform ANSI color sequences
        @rtype: bool

        """

        cur_term = ''
        if 'TERM' in os.environ:
            cur_term = os.environ['TERM'].lower().strip()

        colored_term_list = (
            r'ansi',
            r'linux.*',
            r'screen.*',
            r'[xeak]term.*',
            r'gnome.*',
            r'rxvt.*',
            r'interix',
        )
        term_pattern = r'^(?:' + r'|'.join(colored_term_list) + r')$'
        re_term = re.compile(term_pattern)

        ansi_term = False
        env_term_has_colors = False

        if cur_term:
            if cur_term == 'ansi':
                env_term_has_colors = True
                ansi_term = True
            elif re_term.search(cur_term):
                env_term_has_colors = True
        if debug:
            sys.stderr.write("ansi_term: {a!r}, env_term_has_colors: {h!r}\n".format(
                a=ansi_term, h=env_term_has_colors))

        has_colors = False
        if env_term_has_colors:
            has_colors = True
        for handle in [sys.stdout, sys.stderr]:
            if (hasattr(handle, "isatty") and handle.isatty()):
                if debug:
                    msg = "{} is a tty.".format(handle.name)
                    sys.stderr.write(msg + '\n')
                if (platform.system() == 'Windows' and not ansi_term):
                    if debug:
                        sys.stderr.write("Platform is Windows and not ansi_term.\n")
                    has_colors = False
            else:
                if debug:
                    msg = "{} is not a tty.".format(handle.name)
                    sys.stderr.write(msg + '\n')
                if ansi_term:
                    pass
                else:
                    has_colors = False

        return has_colors

    # -------------------------------------------------------------------------
    def __init__(self, appname=None, verbose=0, version=__pkg_version__):

        self.appname = self.get_generic_appname(appname)
        self.version = version
        self.terminal_has_colors = False

        self.description = (
            "Converts a Debian changelog file into log entries usable as "
            "log entries in a Spec file used to build RPM packages. "
            "If called without a file name, it reads the Debian changelog from STDIN. "
            "It returns the entries on STDOUT."
        )

        self.verbose = int(verbose)
        if self.verbose < 0:
            msg = "Wrong verbose level {!r}, must be >= 0".format(verbose)
            raise ValueError(msg)

        self.arg_parser = None
        self.args = None

        self.changelog_file = None

        self.init_arg_parser()
        self.perform_arg_parser()
        self.init_logging()

    # -------------------------------------------------------------------------
    def terminal_can_color(self):
        """
        Method to detect, whether the current terminal (stdout and stderr)
        is able to perform ANSI color sequences.

        @return: both stdout and stderr can perform ANSI color sequences
        @rtype: bool

        """

        if self.verbose > 3:
            return self.terminal_can_colors(debug=True)
        return self.terminal_can_colors(debug=False)

    # -------------------------------------------------------------------------
    def init_arg_parser(self):
        """
        Method to initiate the argument parser.

        @raise PBApplicationError: on some errors

        """

        self.arg_parser = argparse.ArgumentParser(
            prog=self.appname, description=self.description, add_help=False,)

        self.arg_parser.add_argument(
            '--color', action="store", dest='color', const='yes',
            default='auto', nargs='?', choices=['yes', 'no', 'auto'],
            help="Use colored output for messages.",
        )

        self.arg_parser.add_argument(
            "-v", "--verbose", action="count", dest='verbose',
            help='Increase the verbosity level',
        )

        self.arg_parser.add_argument(
            "-h", "--help", action='help', dest='help',
            help='Show this help message and exit.'
        )
        self.arg_parser.add_argument(
            "--usage", action='store_true', dest='usage',
            help="Display brief usage message and exit."
        )
        v_msg = "Version of %(prog)s: {}".format(self.version)
        self.arg_parser.add_argument(
            "-V", '--version', action='version', version=v_msg,
            help="Show program's version number and exit."
        )

        self.arg_parser.add_argument(
            'file', metavar='CHANGELOG_FILE', type=str, nargs='?',
            action=FileOptionAction, help=(
                "The Debian changelog file to convert into log entries of a RPM spec file. "
                "If omitted, STDIN will be used."),
        )

    # -------------------------------------------------------------------------
    def perform_arg_parser(self):
        """
        Method for parsing arguments.
        """

        self.args = self.arg_parser.parse_args()

        if self.args.usage:
            self.arg_parser.print_usage(sys.stdout)
            self.exit(0)

        if self.args.verbose is not None and self.args.verbose > self.verbose:
            self.verbose = self.args.verbose

        if self.args.color == 'yes':
            self.terminal_has_colors = True
        elif self.args.color == 'no':
            self.terminal_has_colors = False
        else:
            self.terminal_has_colors = self.terminal_can_color()

        if self.args.file:
            self.changelog_file = self.args.file

    # -------------------------------------------------------------------------
    def init_logging(self):
        """
        Initialize the logger object.
        It creates a colored loghandler with all output to STDERR.
        Maybe overridden in descendant classes.

        @return: None
        """

        log_level = logging.INFO
        if self.verbose:
            log_level = logging.DEBUG

        root_logger = logging.getLogger()
        root_logger.setLevel(log_level)

        # create formatter
        format_str = ''
        if self.verbose:
            format_str = '[%(asctime)s]: '
        format_str += self.appname + ': '
        if self.verbose:
            if self.verbose > 1:
                format_str += '%(name)s(%(lineno)d) %(funcName)s() '
            else:
                format_str += '%(name)s '
        format_str += '%(levelname)s - %(message)s'
        formatter = None
        if self.terminal_has_colors:
            formatter = ColoredFormatter(format_str)
        else:
            formatter = logging.Formatter(format_str)

        # create log handler for console output
        lh_console = logging.StreamHandler(sys.stderr)
        lh_console.setLevel(log_level)
        lh_console.setFormatter(formatter)

        root_logger.addHandler(lh_console)

        return

    # -------------------------------------------------------------------------
    def __call__(self):
        """
        Executes the main action of converting.
        Makes the application object callable.
        """

        if self.changelog_file:
            LOG.debug("Reading {!r} ...".format(str(self.changelog_file)))
            with self.changelog_file.open('r', encoding='utf-8', errors='backslashreplace') as fh:
                self.convert(fh, str(self.changelog_file))
        else:
            self.convert(sys.stdin, 'STDIN')

    # -------------------------------------------------------------------------
    def mangle_changes(self, changes):

        clist = []
        change = None

        for line in changes:

            if self.re_emptyline.match(line):
                continue

            m = self.re_start_line.match(line)
            if m:
                if change:
                    clist.append(change)
                change = m.group(1)
                continue

            m = self.re_next_line.match(line)
            if m:
                if change:
                    change += ' ' + m.group(1)
                else:
                    change = m.group(1)
                continue

            warnings.warn("Could not evaluate Changelog entry {!r}.".format(line), SyntaxWarning)

        if change:
            clist.append(change)

        return clist

    # -------------------------------------------------------------------------
    def convert(self, fh, filename):

        ch = None

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            ch = Changelog(fh)

            if len(w):
                msg = "There were {nr} warnings on reading {f!r}.".format(nr=len(w), f=filename)
                LOG.warning(msg)
                for msg in w:
                    category = msg.category.__name__
                    s = f"{category}: {msg.message}\n"
                    LOG.warning(s)
                sys.exit(5)

        LOG.debug("Changelog {f!r} has {nr} entries.".format(f=filename, nr=len(ch)))

        days = {}

        for block in ch:

            lines = []
            day_str = self.re_day_str.sub('', block.date)
            date = datetime.datetime.strptime(day_str, '%a, %d %b %Y')

            day = date.strftime('%Y-%m-%d')
            author = block.author
            version = str(block.version) + '-1'
            lines.append('*   {date} {author} {version}'.format(
                date=date, author=author, version=version))

            changes = self.mangle_changes(block._changes)

            if day not in days:
                days[day] = {
                    'date': date.strftime('%a %b %d %Y'),
                    'author': author,
                    'version': version,
                    'changes': changes,
                }
            else:
                for change in changes:
                    days[day]['changes'].append(change)

        for day in reversed(sorted(days.keys())):
            lines = []
            block = days[day]
            lines.append('*   {date} {author} {version}'.format(
                date=block['date'], author=block['author'], version=block['version']))

            for change in block['changes']:
                for line in textwrap.wrap(
                        change, width=70, initial_indent="-   ", subsequent_indent="    "):
                    lines.append(line)

            print('\n'.join(lines))


# =============================================================================

app = Dch2SpecLogApp()

if app.verbose > 2:
    print("{c}-Object:\n{a}".format(
        c=app.__class__.__name__, a=pp(app.__dict__)), file=sys.stderr)

app()

sys.exit(0)

# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
