import datetime
import re

CHANGELOG_START_RE = re.compile(r'^\#\# \[Unreleased\]')
CHANGELOG_END_RE = re.compile(r'^\#\# \[.*\] - \d{4}-\d{2}-\d{2}')
# if we come across a section header between two release section headers
# then we probably have an improperly formatted changelog
CHANGELOG_ERROR_RE = re.compile(r'^\#\# ')
SECTION_RE = re.compile(r'^\#\#\# (.*)$')
EMPTY_RE = re.compile(r'^\w*\*\w*$')
ENTRY_RE = re.compile(r'\* (.*)')
VALID_SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security']

# allocate some entries to cut-down on merge conflicts
TEMPLATE = """### Added
  *
  *

### Changed
  *
  *

### Fixed
  *
  *

### Deprecated
  *
  *

### Removed
  *
  *

"""


class Changelog(object):
    def __init__(self, path):
        self.path = path
        self.start = []
        self.unreleased = []
        self.rest = []
        self._parse()

    def _parse(self):
        with open(self.path) as fp:
            lines = fp.readlines()

        unreleased_start_found = False
        unreleased_end_found = False

        for line in lines:
            if not unreleased_start_found:
                self.start.append(line)
                if CHANGELOG_START_RE.search(line):
                    unreleased_start_found = True
                continue
            if unreleased_end_found:
                self.rest.append(line)
                continue
            if CHANGELOG_END_RE.search(line):
                self.rest.append(line)
                unreleased_end_found = True
                continue
            if CHANGELOG_ERROR_RE.search(line):
                raise Exception(
                    'Failed to parse {}: {}'.format(self.path, 'unexpected section header found'))
            self.unreleased.append(line)

        self.unreleased = self._normalize_section(self.unreleased)

    @staticmethod
    def _normalize_section(lines):
        """Parse a changelog entry and output a normalized form"""
        sections = {}
        current_section_name = None
        current_section_contents = []
        for line in lines:
            line = line.strip()
            if not line or EMPTY_RE.match(line):
                continue
            match = SECTION_RE.match(line)
            if match:
                if current_section_contents:
                    sections[current_section_name] = current_section_contents
                current_section_contents = []
                current_section_name = match.group(1)
                if current_section_name not in VALID_SECTIONS:
                    raise ValueError("Section '{}' is not valid".format(current_section_name))
                continue
            match = ENTRY_RE.match(line)
            if match:
                current_section_contents.append(match.group(1))
                continue
            raise Exception('Something is wrong with line: {}'.format(line))
        if current_section_contents:
            sections[current_section_name] = current_section_contents

        output = []
        for section in VALID_SECTIONS:
            if section not in sections:
                continue
            output.append('### {}'.format(section))
            for entry in sections[section]:
                output.append(' * {}'.format(entry))
            output.append("\n")
        return output

    def get_unreleased(self):
        return '\n'.join(self.unreleased) if self.unreleased else None

    def bump(self, version):
        if not self.unreleased:
            return

        today = datetime.datetime.today()
        header = "## [{}] - {}\n\n".format(version, today.strftime('%Y-%m-%d'))

        changelog_data = (
            ''.join(self.start) +
            TEMPLATE +
            header +
            '\n'.join(self.unreleased) + '\n\n'
            + ''.join(self.rest)
        )

        with open(self.path, 'w') as fp:
            fp.write(changelog_data)