import re
import sys
import argparse
from pathlib import Path
from textwrap import fill, indent


INDENT = ' ' * 4

CLASS = """

class {name}({parents}):{doc}
"""

INIT = """
    def __init__({args}):{fields}
        super().__init__({format}"{message}")
"""

FUNCTIONS = ['claim_id']


class ErrorClass:

    def __init__(self, hierarchy, name, message):
        self.hierarchy = hierarchy.replace('**', '')
        self.other_parents = []
        if '(' in name:
            assert ')' in name, f"Missing closing parenthesis in '{name}'."
            self.other_parents = name[name.find('(')+1:name.find(')')].split(',')
            name = name[:name.find('(')]
        self.name = name
        self.class_name = name+'Error'
        self.message = message
        self.comment = ""
        if '--' in message:
            self.message, self.comment = message.split('--')
        self.message = self.message.strip()
        self.comment = self.comment.strip()

    @property
    def is_leaf(self):
        return 'x' not in self.hierarchy

    @property
    def code(self):
        return self.hierarchy.replace('x', '')

    @property
    def parent_codes(self):
        return self.hierarchy[0:2], self.hierarchy[0]

    def get_arguments(self):
        args = ['self']
        for arg in re.findall('{([a-z0-1_()]+)}', self.message):
            for func in FUNCTIONS:
                if arg.startswith(f'{func}('):
                    arg = arg[len(f'{func}('):-1]
                    break
            args.append(arg)
        return args

    @staticmethod
    def get_fields(args):
        if len(args) > 1:
            return f''.join(f'\n{INDENT*2}self.{field} = {field}' for field in args[1:])
        return ''

    @staticmethod
    def get_doc_string(doc):
        if doc:
            return f'\n{INDENT}"""\n{indent(fill(doc, 100), INDENT)}\n{INDENT}"""'
        return ""

    def render(self, out, parent):
        if not parent:
            parents = ['BaseError']
        else:
            parents = [parent.class_name]
        parents += self.other_parents
        args = self.get_arguments()
        if self.is_leaf:
            out.write((CLASS + INIT).format(
                name=self.class_name, parents=', '.join(parents),
                args=', '.join(args), fields=self.get_fields(args),
                message=self.message, doc=self.get_doc_string(self.comment), format='f' if len(args) > 1 else ''
            ))
        else:
            out.write(CLASS.format(
                name=self.class_name, parents=', '.join(parents),
                doc=self.get_doc_string(self.comment or self.message)
            ))


def get_errors():
    with open('README.md', 'r') as readme:
        lines = iter(readme.readlines())
        for line in lines:
            if line.startswith('## Exceptions Table'):
                break
        for line in lines:
            if line.startswith('---:|'):
                break
        for line in lines:
            if not line:
                break
            yield ErrorClass(*[c.strip() for c in line.split('|')])


def find_parent(stack, child):
    for parent_code in child.parent_codes:
        parent = stack.get(parent_code)
        if parent:
            return parent


def generate(out):
    out.write(f"from .base import BaseError, {', '.join(FUNCTIONS)}\n")
    stack = {}
    for error in get_errors():
        error.render(out, find_parent(stack, error))
        if not error.is_leaf:
            assert error.code not in stack, f"Duplicate code: {error.code}"
            stack[error.code] = error


def analyze():
    errors = {e.class_name: [] for e in get_errors() if e.is_leaf}
    here = Path(__file__).absolute().parents[0]
    module = here.parent
    for file_path in module.glob('**/*.py'):
        if here in file_path.parents:
            continue
        with open(file_path) as src_file:
            src = src_file.read()
            for error in errors.keys():
                found = src.count(error)
                if found > 0:
                    errors[error].append((file_path, found))

    print('Unused Errors:\n')
    for error, used in errors.items():
        if used:
            print(f' - {error}')
            for use in used:
                print(f'   {use[0].relative_to(module.parent)} {use[1]}')
            print('')

    print('')
    print('Unused Errors:')
    for error, used in errors.items():
        if not used:
            print(f' - {error}')


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("action", choices=['generate', 'analyze'])
    args = parser.parse_args()
    if args.action == "analyze":
        analyze()
    elif args.action == "generate":
        generate(sys.stdout)


if __name__ == "__main__":
    main()