#!/usr/bin/python3

from functools import cmp_to_key
from textwrap import shorten
import argparse
import re
import subprocess
import sys

from common import shell, backports, KeyType, GitConfig, ChangesetStore


def describe(commit):
    return shell('git describe --abbrev=0 {}'.format(commit))

def chset_cmp(ch1, ch2):
    prefix1, postfix1, title1 = ch1[:3]
    prefix2, postfix2, title2 = ch2[:3]

    prefixes = PREFIXES + ['']
    postfixes = POSTFIXES + ['']

    prefix1 = prefixes.index(prefix1)
    prefix2 = prefixes.index(prefix2)
    postfix1 = postfixes.index(postfix1)
    postfix2 = postfixes.index(postfix2)

    if prefix1 < prefix2:
        return -1
    elif prefix1 == prefix2:
        if postfix1 < postfix2:
            return -1
        elif postfix1 == postfix2:
            if title1 < title2:
                return -1
            else:
                return 1
        else:
            return 1
    else:
        return 1


class Release(object):
    def __init__(self, range=None):
        if range is None:
            b = describe('HEAD')
            a = describe('{}^'.format(b))
            range = '{}..{}'.format(a, b)
        else:
            a, b = range.split('..')
            if not b:
                b = describe('HEAD')

        self.range = range
        self.name = shell("git tag -l --format='%(contents)' {}".format(b))

        if RELEASE_RE is None:
            return

        m = re.match(RELEASE_RE, self.name)
        if m is not None:
            self.name = m.groups()[0]


class PlainLog(object):
    def __init__(self, range, empty, no_preamble, width, refresh, quiet,
                 *args, **kwargs):
        self.range = range
        self.release = Release(range)
        self.empty = empty
        self.no_preamble = no_preamble
        self.width = width
        self.refresh = refresh
        self.quiet = quiet
        self._changesets = ChangesetStore()

    def _format_preamble(self):
        out = 'Changelog'
        if not self.release.name:
            return out
        return '{} {}'.format(self.release.name, out)

    def _format_section(self, section):
        return HEADINGS[section] + '\n'

    def _format_changeset(self, chset):
        fmt = '  * {}{} {}(#{})'
        prefix, postfix, title, num, url = chset
        if prefix:
            prefix = '{}: '.format(HEADINGS[prefix])
        if postfix:
            postfix = '[{}] '.format(HEADINGS[postfix])
        title = shorten(title.replace('`', ''),
                        self.width - len(fmt.format(prefix, '', postfix, num)))
        return fmt.format(prefix, title, postfix, num)

    @property
    def changesets(self):
        d = {}
        nums = set()
        ignore = set(IGNORE)
        sections = set(SECTIONS)

        if sys.stdin.isatty():
            commits = backports(self.range)
        else:
            commits = sys.stdin.read().splitlines()

        # Prefetch changesets
        self._changesets.load(commits, self.refresh, not self.quiet)

        for commit in commits:
            try:
                chset = self._changesets[commit]
            except KeyError:
                continue

            num = chset['number']
            labels = set(map(str.lower, chset['labels']))

            if num in nums:
                continue
            nums.add(num)

            for p in PREFIXES:
                if p in labels:
                    prefix = p
                    break
            else:
                prefix = ''

            for p in POSTFIXES:
                if p in labels:
                    postfix = p
                    break
            else:
                postfix = ''

            labels = set(map(lambda x: ALIASES[x]
                             if x in ALIASES else x, labels))
            if labels & ignore:
                continue

            labels = labels & sections
            if not labels:
                labels = set(['other'])

            for label in labels:
                d.setdefault(label, []).append(
                    (prefix, postfix, chset['title'], num, chset['url']))

        return d

    def __str__(self):
        out = []
        if not self.no_preamble:
            out = [self._format_preamble(), '']
        changesets = self.changesets
        for section in SECTIONS:
            heading = self._format_section(section)
            if section not in changesets:
                if self.empty:
                    out.append(heading)
                continue
            out.append(heading)
            for chset in sorted(changesets[section], key=cmp_to_key(chset_cmp)):
                out.append(self._format_changeset(chset))
            out.append('')
        return '\n'.join(out)


class MarkdownLog(PlainLog):
    def __init__(self, range, empty, no_preamble, width, refresh, quiet, level,
                 *args, **kwargs):
        super(MarkdownLog, self).__init__(
            range, empty, no_preamble, width, refresh, quiet)
        self.level = level - 1

    def _heading(self, level, name):
        return '{} {}'.format('#' * (self.level + level), name)

    def _format_preamble(self):
        return self._heading(1, super(MarkdownLog, self)._format_preamble())

    def _format_section(self, section):
        return self._heading(2, HEADINGS[section])

    def _format_changeset(self, chset):
        prefix, postfix, title, num, url = chset
        if prefix:
            prefix = '{}: '.format(HEADINGS[prefix])
        if postfix:
            postfix = '[{}] '.format(HEADINGS[postfix])
        return '* {}{} {}([#{}]({}))'.format(prefix, title, postfix, num, url)


CONFIG = GitConfig('changelog')
SECTIONS = CONFIG.get('sections', KeyType.LIST)
PREFIXES = CONFIG.get('prefixes', KeyType.LIST)
POSTFIXES = CONFIG.get('postfixes', KeyType.LIST)
IGNORE = CONFIG.get('ignore', KeyType.LIST)
HEADINGS = CONFIG.get('headings', KeyType.MAP)
ALIASES = CONFIG.get('aliases', KeyType.MAP)
RELEASE_RE = CONFIG.get('releasePattern', KeyType.SIMPLE)
RELEASE = Release()

parser = argparse.ArgumentParser(
    description='Generate a changelog for the given revision range.  '
                'If a commit list is given on stdin, that will be used instead.')
parser.add_argument('range', nargs='?', type=str, default=RELEASE.range,
                    help='git revision range (default: {})'.format(
                        RELEASE.range))
parser.add_argument('-e', '--empty', action='store_true',
                    help='include empty sections')
parser.add_argument('-n', '--no-preamble', action='store_true',
                    help='don\'t include preamble')
parser.add_argument('-m', '--markdown', action='store_true',
                    help='output in Markdown format')
parser.add_argument('-w', '--width', type=int, default=80,
                    help='maximum line width in plain-text mode '
                         '(default: 80)')
parser.add_argument('-l', '--level', type=int, default=1,
                    help='first heading level in Markdown format '
                         '(default: 1)')
parser.add_argument('-r', '--refresh', action='store_true',
                    help='always fetch changeset data from server '
                         '(do not use cache)')
parser.add_argument('-q', '--quiet', action='store_true',
                    help='do not print progress messages on stderr')
args = parser.parse_args()

if args.markdown:
    Log = MarkdownLog
else:
    Log = PlainLog

log = Log(args.range, args.empty, args.no_preamble, args.width, args.refresh,
          args.quiet, args.level)
print(log)
