#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (c) 2017 Computer Vision Center (CVC) at the Universitat Autonoma de
# Barcelona (UAB).
#
# This work is licensed under the terms of the MIT license.
# For a copy, see <https://opensource.org/licenses/MIT>.

import os
import yaml
import re

COLOR_METHOD = '#7fb800'
COLOR_PARAM = '#00a6ed'
COLOR_INSTANCE_VAR = '#f8805a'
COLOR_NOTE = '#8E8E8E'
COLOR_WARNING = '#ED2F2F'

QUERY = re.compile(r'([cC]arla(\.[a-zA-Z0-9_]+)+)')


def create_hyperlinks(text):
    return re.sub(QUERY, r'[\1](#\1)', text)


def join(elem, separator=''):
    return separator.join(elem)


class MarkdownFile:
    def __init__(self):
        self._data = ""
        self._list_depth = 0
        self.endl = '  \n'
    
    def data(self):
        return self._data

    def list_push(self, buf=''):
        if buf:
            self.text(join([
                '    ' * self._list_depth if self._list_depth != 0 else '', '- ', buf]))
        self._list_depth = (self._list_depth + 1)

    def list_pushn(self, buf):
        self.list_push(join([buf, self.endl]))

    def list_pop(self):
        self._list_depth = max(self._list_depth - 1, 0)

    def list_popn(self):
        self.list_pop()
        self._data = join([self._data, '\n'])

    def list_depth(self):
        if self._data.strip()[-1:] != '\n' or self._list_depth == 0:
            return ''
        return join(['    ' * self._list_depth])

    def separator(self):
        self._data = join([self._data, '\n---\n'])

    def new_line(self):
        self._data = join([self._data, self.endl])

    def text(self, buf):
        self._data = join([self._data, buf])

    def textn(self, buf):
        self._data = join([self._data, self.list_depth(), buf, self.endl])

    def first_title(self):
        self._data = join([
            self._data, '#Python API reference'])

    def title(self, strongness, buf):
        self._data = join([
            self._data, '\n', self.list_depth(), '#' * strongness, ' ', buf, '\n'])

    def title_html(self, strongness, buf):
        self._data = join([
            self._data, '\n', self.list_depth(), '<h', str(strongness), '>', buf, '</h', str(strongness), '>\n'])
    
    def inherit_join(self, inh):
        self._data = join([
            self._data,'<div style="padding-left:30px;margin-top:-20px"><small><b>Inherited from ',inh,'</b></small></div></p><p>'])

    def note(self, buf):
        self._data = join([self._data, buf])

    def code_block(self, buf, language=''):
        return join(['```', language, '\n', self.list_depth(), buf, '\n', self.list_depth(), '```\n'])

    def prettify_doc(self, doc):
        punctuation_marks = ['.', '!', '?']
        doc = doc.strip()
        doc += '' if doc[-1:] in punctuation_marks else '.'
        return doc


def italic(buf):
    return join(['_', buf, '_'])


def bold(buf):
    return join(['**', buf, '**'])


def code(buf):
    return join(['`', buf, '`'])


def brackets(buf):
    return join(['[', buf, ']'])


def parentheses(buf):
    return join(['(', buf, ')'])


def small(buf):
    return join(['<sub><sup>', buf, '</sup></sub>'])


def sub(buf):
    return join(['<sub>', buf, '</sub>'])


def html_key(buf):
    return join(['<a name="', buf, '"></a>'])


def color(col, buf):
    return join(['<font color="', col, '">', buf, '</font>'])


def valid_dic_val(dic, value):
    return value in dic and dic[value]


class YamlFile:
    """Yaml file class"""

    def __init__(self, path):
        self._path = path
        with open(path) as yaml_file:
            self.data = yaml.safe_load(yaml_file)
        self.validate()

    def validate(self):
        # print('Validating ' + str(self._path.replace('\\', '/').split('/')[-1:][0]))
        if self.data is None:
            print('\n[ERROR] File: ' + self._path)
            print("This file has no data:")
            exit(0)
        for module in self.data:
            if 'module_name' in module and module['module_name'] is None:
                print('\n[ERROR] File: ' + self._path)
                print("'module_name' is empty in:")
                exit(0)
            if 'classes' in module:
                if not module['classes']:
                    print('\n[ERROR] File: ' + self._path)
                    print("'classes' is empty in:")
                    exit(0)
                for cl in module['classes']:
                    if 'class_name' in cl and cl['class_name'] is None:
                        print('\n[ERROR] File: ' + self._path)
                        print("'class_name' is empty in:")
                        exit(0)
                    if 'instance_variables' in cl and cl['instance_variables']:
                        for iv in cl['instance_variables']:
                            if 'var_name' not in iv:
                                print('\n[ERROR] File: ' + self._path)
                                print("'var_name' not found inside 'instance_variables' of class: " + cl['class_name'])
                                exit(0)
                            if 'var_name' in iv and iv['var_name'] is None:
                                print('\n[ERROR] File: ' + self._path)
                                print("'var_name' is empty in:")
                                exit(0)
                    if 'methods' in cl and cl['methods']:
                        for met in cl['methods']:
                            if 'def_name' not in met:
                                print('\n[ERROR] File: ' + self._path)
                                print("'def_name' not found inside 'methods' of class: " + cl['class_name'])
                                exit(0)
                            if 'def_name' in met and met['def_name'] is None:
                                print('\n[ERROR] File: ' + self._path)
                                print("'def_name' is empty in:")
                                exit(0)
                            if 'params' in met and met['params']:
                                for param in met['params']:
                                    if 'param_name' not in param:
                                        print('\n[ERROR] File: ' + self._path)
                                        print("'param_name' not found inside 'params' of class: " + cl['class_name'])
                                        exit(0)
                                    if 'param_name' in param and param['param_name'] is None:
                                        print('\n[ERROR] File: ' + self._path)
                                        print("'param_name' is empty in:")
                                        exit(0)
                                    if 'type' in param and param['type'] is None:
                                        print('\n[ERROR] File: ' + self._path)
                                        print("'type' is empty in:")
                                        exit(0)

    def get_modules(self):
        return [module for module in self.data]


def gen_stub_method_def(method):
    """Return python def as it should be written in stub files"""
    param = ''
    method_name = method['def_name']
    for p in method['params']:
        p_type = join([': ', str(p['type'])]) if 'type' in p else ''
        default = join([' = ', str(p['default'])]) if 'default' in p else ''
        param = join([param, p['param_name'], p_type, default, ', '])
    param = param[:-2]  # delete the last ', '
    return_type = join([' -> ', method['return']]) if 'return' in method else ''
    return join([method_name, parentheses(param), return_type])


def gen_doc_method_def(method, is_indx=False, with_self=True):
    """Return python def as it should be written in docs"""
    param = ''
    method_name = method['def_name']
    if valid_dic_val(method, 'static'):
        with_self = False

    # to correclty render methods like __init__ in md
    if method_name[0] == '_':
        method_name = '\\' + method_name
    if is_indx:
        method_name = bold(method_name)
    else:
        method_name = bold(color(COLOR_METHOD, method_name))

    if with_self:
        if not 'params' in method or method['params'] is None:
            method['params'] = []
        method['params'].insert(0, {'param_name': 'self'})

    if valid_dic_val(method, 'params'):
        for p in method['params']:
            default = join(['=', str(p['default'])]) if 'default' in p else ''
            if is_indx:
                param = join([param, bold(p['param_name']), default, ', '])
            else:
                param = join([param, color(COLOR_PARAM, bold(p['param_name']) + create_hyperlinks(default)), ', '])

    if with_self:
        method['params'] = method['params'][1:]
        if not method['params']:  # if is empty delete it
            del method['params']

    param = param[:-2]  # delete the last ', '
    return join([method_name, parentheses(param)])


def gen_inst_var_indx(inst_var, class_key):
    inst_var_name = inst_var['var_name']
    inst_var_key = join([class_key, inst_var_name], '.')
    return join([
        brackets(bold(inst_var_name)),
        parentheses(inst_var_key), ' ',
        sub(italic('Instance variable'))])


def gen_method_indx(method, class_key):
    method_name = method['def_name']
    method_key = join([class_key, method_name], '.')
    method_def = gen_doc_method_def(method, True)
    return join([
        brackets(method_def),
        parentheses(method_key), ' ',
        sub(italic('Method'))])


def add_doc_method_param(md, param):
    param_name = param['param_name']
    param_type = ''
    param_doc = ''
    if valid_dic_val(param, 'type'):
        param_type = create_hyperlinks(param['type'])
    if valid_dic_val(param, 'doc'):
        param_doc = create_hyperlinks(md.prettify_doc(param['doc']))
    param_type = '' if not param_type else parentheses(italic(param_type))
    md.list_push(code(param_name))
    if param_type:
        md.text(' ' + param_type)
    if param_doc:
        md.textn(' – ' + param_doc)
    else:
        md.new_line()
    md.list_pop()


def add_doc_method(md, method, class_key):
    method_name = method['def_name']
    method_key = join([class_key, method_name], '.')
    method_def = gen_doc_method_def(method, False)
    md.list_pushn(join([html_key(method_key), method_def]))

    # Method doc
    if valid_dic_val(method, 'doc'):
        md.textn(create_hyperlinks(md.prettify_doc(method['doc'])))

    printed_title = False
    if valid_dic_val(method, 'params'):
        for param in method['params']:
            # is_self = valid_dic_val(param, 'param_name') and param['param_name'] == 'self'
            have_doc = valid_dic_val(param, 'doc')
            have_type = valid_dic_val(param, 'type')
            if not have_doc and not have_type:
                continue
            # Print the 'Parameters' title once
            if not printed_title:
                printed_title = True
                md.list_push(bold('Parameters:') + '\n')
            add_doc_method_param(md, param)
    if printed_title:
        md.list_pop()

    # Return doc
    if valid_dic_val(method, 'return'):
        md.list_push(bold('Return:') + ' ')
        md.textn(italic(create_hyperlinks(method['return'])))
        md.list_pop()

    # Note doc
    if valid_dic_val(method, 'note'):
        md.list_push(bold('Note:') + ' ')
        md.textn(color(COLOR_NOTE, italic(create_hyperlinks(method['note']))))
        md.list_pop()

    # Warning doc
    if valid_dic_val(method, 'warning'):
        md.list_push(bold('Warning:') + ' ')
        md.textn(color(COLOR_WARNING, italic(create_hyperlinks(method['warning']))))
        md.list_pop()

    # Raises error doc
    if valid_dic_val(method, 'raises'):
        md.list_pushn(bold('Raises:') + ' ' + method['raises'])
        md.list_pop()

    md.list_pop()


def add_doc_inst_var(md, inst_var, class_key):
    var_name = inst_var['var_name']
    var_key = join([class_key, var_name], '.')
    var_type = ''

    # Instance variable type
    if valid_dic_val(inst_var, 'type'):
        var_type = ' ' + parentheses(italic(create_hyperlinks(inst_var['type'])))

    md.list_pushn(
        html_key(var_key) +
        bold(color(COLOR_INSTANCE_VAR, var_name)) +
        var_type)

    # Instance variable doc
    if valid_dic_val(inst_var, 'doc'):
        md.textn(create_hyperlinks(md.prettify_doc(inst_var['doc'])))

    # Note doc
    if valid_dic_val(inst_var, 'note'):
        md.list_push(bold('Note:') + ' ')
        md.textn(color(COLOR_NOTE, italic(create_hyperlinks(inst_var['note']))))
        md.list_pop()

    # Warning doc
    if valid_dic_val(inst_var, 'warning'):
        md.list_push(bold('Warning:') + ' ')
        md.textn(color(COLOR_WARNING, italic(create_hyperlinks(inst_var['warning']))))
        md.list_pop()

    md.list_pop()


class Documentation:
    """Main documentation class"""

    def __init__(self, path):
        self._path = path
        self._files = [f for f in os.listdir(path) if f.endswith('.yml')]
        self._yamls = list()
        for yaml_file in self._files:
            self._yamls.append(YamlFile(os.path.join(path, yaml_file)))
        # Merge same modules of different files
        self.master_dict = dict()
        for yaml_file in self._yamls:
            for module in yaml_file.get_modules():
                module_name = module['module_name']
                if module_name not in self.master_dict:
                    self.master_dict[module_name] = module
                elif valid_dic_val(module, 'classes'):
                    for new_module in module['classes']:
                        # Create the 'classes' key if does not exist already
                        if not valid_dic_val(self.master_dict[module_name], 'classes'):
                            self.master_dict[module_name]['classes'] = []
                        self.master_dict[module_name]['classes'].append(new_module)

    def gen_overview(self):
        """Generates a referenced index for markdown file"""
        md = MarkdownFile()
        md.title(3, 'Overview')
        for module_name in sorted(self.master_dict):
            module = self.master_dict[module_name]
            module_key = '#' + module_name
            md.list_pushn(
                brackets(bold(module_key[1:])) +
                parentheses(module_key) + ' ' +
                sub(italic('Module')))
            # Generate class overview (if any)
            if 'classes' in module and module['classes']:
                for cl in sorted(module['classes']):
                    class_name = cl['class_name']
                    class_key = join([module_key, class_name], '.')
                    md.list_pushn(join([
                        brackets(bold(class_name)),
                        parentheses(class_key), ' ',
                        sub(italic('Class'))]))
                    # Generate class instance variables overview (if any)
                    if 'instance_variables' in cl and cl['instance_variables']:
                        for inst_var in cl['instance_variables']:
                            md.list_push(gen_inst_var_indx(inst_var, class_key))
                            md.list_popn()
                    # Generate class methods overview (if any)
                    if 'methods' in cl and cl['methods']:
                        for method in cl['methods']:
                            md.list_push(gen_method_indx(method, class_key))
                            md.list_popn()
                    md.list_pop()
            md.list_pop()
        return md.data()

    def gen_body(self):
        """Generates the documentation body"""
        md = MarkdownFile()
        md.first_title()
        for module_name in sorted(self.master_dict):
            module = self.master_dict[module_name]
            module_key = module_name
            # Generate class doc (if any)
            if valid_dic_val(module, 'classes'):
                for cl in sorted(module['classes'], key = lambda i: i['class_name']):
                    class_name = cl['class_name']
                    class_key = join([module_key, class_name], '.')
                    current_title = module_name+'.'+class_name
                    md.title(2, join([current_title,'<a name="'+current_title+'"></a>']))
                    inherits = ''
                    if valid_dic_val(cl, 'parent'):
                        inherits = italic(create_hyperlinks(cl['parent']))
                        md.inherit_join(inherits)
                    # Class main doc
                    if valid_dic_val(cl, 'doc'):
                        md.textn(create_hyperlinks(md.prettify_doc(cl['doc'])))
                    # Generate instance variable doc (if any)
                    if valid_dic_val(cl, 'instance_variables'):
                        md.title_html(3, 'Instance Variables')
                        for inst_var in cl['instance_variables']:
                            add_doc_inst_var(md, inst_var, class_key)
                    # Generate method doc (if any)
                    if valid_dic_val(cl, 'methods'):
                        md.title_html(3, 'Methods')
                        for method in cl['methods']:
                            add_doc_method(md, method, class_key)
                    md.separator()
        return md.data().strip()

    def gen_markdown(self):
        """Generates the whole markdown file"""
        return join([self.gen_body()], '\n').strip()


def main():
    """Main function"""
    print("Generating PythonAPI documentation...")
    script_path = os.path.dirname(os.path.abspath(__file__))
    docs = Documentation(script_path)
    with open(os.path.join(script_path, '../../Docs/python_api.md'), 'w') as md_file:
        md_file.write(docs.gen_markdown())
    print("Done!")


if __name__ == "__main__":
    main()
