#!/usr/bin/python3
# SPDX-License-Identifier: GPL-2.0-only

import argparse
from contextlib import closing
import locale
import os
import sqlite3
import sys
from termcolor import cprint, colored

#from pprint import pp

locale.setlocale(locale.LC_ALL, 'C.UTF-8')

def checkOrder(order : str) -> str:
    if all(c.isdigit() or c in " ," for c in order):
        return order
    print(colored(f'You can specify order only with numbers, commas and spaces: "{order}"', 'red'),
          file=sys.stderr)
    sys.exit(1)

def handleArgs() -> dict:
    result = {}
    parser = argparse.ArgumentParser(description='Compare two conf_file_map databases')
    parser.add_argument('-b', '--branch', action='append', help='compare only these branches')
    parser.add_argument('-c', '--color', action='store_true', help='force the color output')
    parser.add_argument('--order-conf-file', help='order of the output of conf_file_map',
                        default='1, 2, 3')
    parser.add_argument('--order-conf-branch', help='order of the output of conf_branch_map',
                        default='1, 2, 3, 4, 5')
    parser.add_argument('--order-module-file', help='order of the output of module_file_map',
                        default='1, 2, 3')
    parser.add_argument('--order-module-details', help='order of the output of module_details_map',
                        default='1, 2, 3')
    parser.add_argument('--order-ignored-file-branch', help='order of the output of ignored_file_branch_map',
                        default='1, 2')
    parser.add_argument('-I', '--ignore', action='append', help='ignore these branches')
    parser.add_argument('old-db', help="database prefixed with '-' in the output")
    parser.add_argument('new-db', help="database prefixed with '+' in the output")
    args = parser.parse_args()

    if args.color:
        os.environ['FORCE_COLOR'] = '1'

    if args.branch and args.ignore:
        print(colored('You cannot specify both ignores and branches', 'red'), file=sys.stderr)
        sys.exit(1)

    result['order-conf_file_map_view'] = checkOrder(args.order_conf_file)
    result['order-conf_branch_map_view'] = checkOrder(args.order_conf_branch)
    result['order-module_file_map_view'] = checkOrder(args.order_module_file)
    result['order-module_details_map_view'] = checkOrder(args.order_module_details)
    result['order-ignored_file_branch_map_view'] = checkOrder(args.order_ignored_file_branch)
    result['branches'] = args.branch
    result['ignores'] = args.ignore
    result['old-db'] = getattr(args, 'old-db')
    result['new-db'] = getattr(args, 'new-db')

    return result

def buildSelect(sel : list[str], params : list[str], columns : str, db: str, table : str,
                opts : dict):
    sel.append(f'SELECT {columns} FROM {db}{table}')
    branches = opts['branches']
    ignores = opts['ignores']
    if branches or ignores:
        sel.append('WHERE branch')
        if branches:
            array = branches
        else:
            sel.append('NOT')
            array = ignores
        placeholders = ', '.join('?' * len(array))
        sel.append(f'IN ({placeholders})')
        params += array

def compareTablesDir(added : bool, old : str, new : str, table: str, columns : str,
                     cur : sqlite3.Cursor, opts : dict, changedBranches : dict[str, int]):
    params : list[str] = []
    sel : list[str] = []
    buildSelect(sel, params, columns, old, table, opts)
    sel.append('EXCEPT')
    buildSelect(sel, params, columns, new, table, opts)
    sel.append(f'ORDER BY {opts["order-" + table]}')
    selStr = ' '.join(sel)

    #pp(selStr)
    #pp(params)

    if added:
        prefix = '+'
        color = 'green'
    else:
        prefix = '-'
        color = 'red'

    for row in cur.execute(selStr, params):
        changedBranches[row[0]] = changedBranches.get(row[0], 0) + 1
        cprint(f'{prefix}{",".join(map(str, row))}', color)

def compareTables(table : str, columns : str, cur : sqlite3.Cursor, opts : dict):
    cprint(f'=== Comparing "{table}" ===', 'light_green')
    changedBranches : dict[str, int] = {}
    compareTablesDir(False, '', 'new.', table, columns, cur, opts, changedBranches)
    compareTablesDir(True, 'new.', '', table, columns, cur, opts, changedBranches)
    changes = [ f'{k} ({changedBranches[k]})' for k in changedBranches ]
    print(f'Changed branches: {", ".join(changes)}')

def getSHANote(sha : str | None, note: str, noteToAdd : str) -> tuple[str, str]:
    if sha is None:
        note += ' <' + noteToAdd + '>'
        return note, '0' * 12
    return note, sha[:12]

def dumpSHAs(cur : sqlite3.Cursor):
    cprint('=== Branches SHAS ===', 'light_green')
    for row in cur.execute('''SELECT main.branch.branch, main.branch.sha,
                                new.branch.branch, new.branch.sha
                              FROM main.branch FULL OUTER JOIN new.branch
                                ON main.branch.branch = new.branch.branch
                              ORDER BY 1,3;'''):
        branch = row[0] or row[2]
        branch += ':'
        note, sha1 = getSHANote(row[1], '', 'ADDED')
        note, sha2 = getSHANote(row[3], note, 'REMOVED')
        if sha1 == sha2:
            note += ' <UNCHANGED>'
        print(f'{branch:30} {sha1}..{sha2}{note}')

def compareDBs(cur : sqlite3.Cursor, opts : dict):
    dumpSHAs(cur)
    compareTables('conf_file_map_view', 'branch, config, path', cur, opts)
    compareTables('conf_branch_map_view', 'branch, flavor, arch, config, value', cur, opts)
    compareTables('module_file_map_view', 'branch, module, path', cur, opts)
    compareTables('module_details_map_view', 'branch, module, supported', cur, opts)
    compareTables('ignored_file_branch_map_view', 'branch, path', cur, opts)

opts = handleArgs()

with closing(sqlite3.connect(opts['old-db'])) as db:
    with closing(db.cursor()) as cur:
        cur.execute('PRAGMA foreign_keys = ON;')
        cur.execute('ATTACH ? AS new;', (opts['new-db'],))
        try:
            compareDBs(cur, opts)
        except BrokenPipeError:
            pass
