#!python

import os
import sys
from tempfile import mkstemp
from typing import Optional, Type
from types import TracebackType
import tabulate
import text_table_view
import html_table_view

import compare_ranges
from check_rebase_meta import Meta
from compare_ranges import MultiRange, CompRes, \
    RowsHideLevel, NoBaseError, Column
from compare_commits import interactive_compare_commits, \
    check_git_clean_branch

from viewable import Span

tabulate.PRESERVE_WHITESPACE = True


def print_legend(viewer, ranges, html):
    def_style = (
        ('Critical bugs', 'bug-critical'),
        ('Matching, checked automatically', 'matching'),
        ('Matching, checked by hand', 'checked'),
        ('Dropped patches', 'drop'),
        ('Jira issues, non-critical', 'bug')
    )
    tab = [[Span(f'███████ - {desc}', style)] for desc, style in def_style]
    tab += [[r.legend] for r in ranges if r.legend]
    print(viewer.view_table(tab))


class GitCheckRebase:
    def __init__(self, range_defs, meta_path, html, jira, jira_issues, legend,
                 columns, rows_hide_level, interactive, color,
                 ign_commit_messages):
        self.range_defs = range_defs
        self.jira = jira
        self.jira_issues = jira_issues
        self.legend = legend
        self.rows_hide_level = rows_hide_level
        self.interactive = interactive
        self.ign_commit_messages = ign_commit_messages
        self.ranges = []  # see parse_range_defs
        self.tab = None  # see main

        self.headers = columns != 'short'
        if columns is None:
            self.columns = (Column.COMMITS, Column.SUBJECT)
        elif columns == 'all':
            self.columns = [c for c in Column]
        else:
            cols = columns.split(',')
            if 'full' in cols:
                i = cols.index('full')
                cols[i : i + 1] = 'feature', 'commits', 'date', 'author', 'subject'
            if 'short' in cols:
                i = cols.index('short')
                cols[i : i + 1] = 'commits', 'subject'
            self.columns = [Column[c.upper()] for c in cols]

        if jira_issues:
            assert jira is not None
            self.jira = jira
            self.jira_issues = jira_issues.split(',')

        self.html = html
        if html:
            self.fmt = 'html'
        elif color is None:
            self.fmt = 'colored' if sys.stdout.isatty() else 'plain'
        else:
            self.fmt = 'colored' if color else 'plain'

        if self.fmt == 'html':
            self.viewer = html_table_view.HtmlViewer()
        else:
            self.viewer = text_table_view.TextViewer(self.fmt == 'colored')

        self.created_meta = not meta_path
        if self.created_meta:
            fd, meta_path = mkstemp()
            os.close(fd)

        self.meta = Meta(meta_path)

    def __enter__(self):
        return self

    def __exit__(self,
                 exc_type: Optional[Type[BaseException]],
                 exc_value: Optional[BaseException],
                 traceback: Optional[TracebackType]) -> None:
        if self.created_meta:
            if os.stat(self.meta.fname).st_size == 0:
                os.unlink(self.meta.fname)
            else:
                print(f"""

Meta file is created: {self.meta.fname}
You may use it with next run, specifying option --meta {self.meta.fname}""")

    def parse_range_defs(self):
        """Must be called again, when git history changed"""
        try:
            last = MultiRange(self.range_defs[-1], meta=self.meta)
        except NoBaseError:
            sys.exit("Last range can't use default-base '..<commit>' syntax")

        try:
            self.ranges = [MultiRange(r, meta=self.meta,
                                      default_base=last.base)
                           for r in self.range_defs[:-1]]
        except NoBaseError:
            # last.base is None if last has several sub-ranges
            sys.exit("Can't use default-base '..<commit>' syntax when last "
                     "range is multi-range")

        self.ranges.append(last)

    def do_interactive_compare(self, row_ind: int, i1: int,
                               i2: int, branch: str) -> str:
        row = self.tab.rows[row_ind]
        i1, i2 = sorted((i1, i2))

        br = ['', '']
        if branch:
            if self.ranges[i1].top in ('HEAD', branch):
                br[0] = branch
            if self.ranges[i2].top in ('HEAD', branch):
                br[1] = branch

        h = [row.commits[i].commit_hash for i in (i1, i2)]
        res = interactive_compare_commits(h[0], h[1], br[0], br[1],
                                          c2_ind=row_ind+1,
                                          comment=row.get_comment())
        assert not res.equal  # that would be bug in compare_ranges
        if res.ok:
            if i1 == 0:
                row.commits[i1].comp = CompRes.BASE
                row.commits[i2].comp = CompRes.CHECKED
            else:
                row.commits[i2].comp = CompRes.BASE
                row.commits[i1].comp = CompRes.CHECKED
        self.meta.update_meta(row.subject, res.comment, h if res.ok else None)

        if res.stop:
            return 'STOP'

        return res.new_c1 or res.new_c2

    def main(self, start_from):
        if start_from:
            if not self.interactive:
                sys.exit('--start_from supported only in --interactive mode')

        self.parse_range_defs()

        self.tab = compare_ranges.Table(self.ranges, self.meta)
        self.tab.do_comparison(self.ign_commit_messages)
        if self.jira:
            self.tab.add_jira_info(self.jira, self.jira_issues)

        if self.interactive:
            branch = check_git_clean_branch()
            for row_ind, row in enumerate(self.tab.rows):
                if row.commits[0] is None:
                    base_ind = len(row.commits) - 1
                    other_inds = range(base_ind)
                else:
                    base_ind = 0
                    other_inds = range(1, len(row.commits))

                base = row.commits[base_ind]
                assert base is not None

                if start_from == base.commit_hash:
                    start_from = None

                stop = False
                for i in other_inds:
                    c = row.commits[i]
                    if c is None:
                        continue

                    if start_from == c.commit_hash:
                        start_from = None
                    if start_from is not None:
                        continue

                    if c.comp != CompRes.NONE:
                        continue

                    res = self.do_interactive_compare(row_ind, base_ind, i,
                                                      branch)
                    if res == 'STOP':
                        stop = True
                        break

                    if res:
                        self.main(start_from=res)
                        return
                if stop:
                    break

        out = self.tab.to_list(columns=self.columns,
                               headers=self.headers,
                               rows_hide_level=self.rows_hide_level)

        if self.html:
            print("""<!DOCTYPE html>
                  <meta charset="utf-8"/>
                  <style>
                  body {
                     font-family: monospace;
                  }
                  </style>
                  """)

        if self.legend:
            print_legend(self.viewer, self.ranges, self.html)

        print(self.viewer.view_table(out))


if __name__ == '__main__':
    import argparse

    p = argparse.ArgumentParser(description="Compare git commit ranges")

    p.add_argument('ranges', metavar='range', nargs='+',
                   help='ranges to compare, '
                   'in form [<name>:]<git range or ref>')
    p.add_argument('--meta', help='optional, file with additional metadata')
    p.add_argument('--html', help='output in html format', action='store_true')
    p.add_argument('--jira-issues',
                   help='optional, comma-separated jira issues list')
    p.add_argument('--jira', help='user:password@server')
    p.add_argument('--legend', help='print legend', action='store_true')
    p.add_argument('--columns',
                   help='which columns to show in table: "short" is default. '
                   '"full" adds author and date columns and also column '
                   'headers', default='short')
    p.add_argument('--rows-hide-level',
                   choices=[x.name.lower() for x in RowsHideLevel],
                   help='which rows to hide in table: "show_all" is default. ',
                   default=RowsHideLevel.SHOW_ALL.name.lower())
    p.add_argument('--interactive',
                   help='do interactive comparison of not-equal commits. '
                   'For each commit to compare, vimdiff is started as a '
                   'subprocess. User should exit it successfully (by :qa) to '
                   'mark commits "ok", and with error (by :cq) to don\'t '
                   'mark commits "ok"', action='store_true')
    p.add_argument('--color',
                   help='Highlight results. By default does coloring only '
                   'when stdout is tty', action=argparse.BooleanOptionalAction,
                   default=None)
    p.add_argument('--ignore-commit-messages',
                   help='Ignore commit message when compare commits',
                   action='store_true')
    p.add_argument('--start-from', help='hash commit of right column to '
                   'start from for interactive check.')

    args = p.parse_args()

    rows_hide_lvl = RowsHideLevel[args.rows_hide_level.upper()]
    try:
        gcr = GitCheckRebase(range_defs=args.ranges, meta_path=args.meta,
                             html=args.html,
                             jira=args.jira, jira_issues=args.jira_issues,
                             legend=args.legend, columns=args.columns,
                             rows_hide_level=rows_hide_lvl,
                             interactive=args.interactive,
                             color=args.color,
                             ign_commit_messages=args.ignore_commit_messages)
    except OSError as e:
        sys.exit(f'Failed to open "{args.meta}": {e.strerror}')

    with gcr:
        gcr.main(start_from=args.start_from)
