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

"""
This file contains functions to retrieve and return BEL object specifications.
"""

import glob
import os
import re
import copy
import sys
import yaml
import json
import click
from typing import Mapping, List, Any

import logging
log = logging.getLogger(__name__)

# Custom Typing definitions
BELSpec = Mapping[str, Any]

'''
Keys available in enhanced spec_dict:

- version
- notes
- admin
    - 'version_underscored'
    - 'parser_path'
    - 'jinja_template'
- relations
    - 'list'
    - 'list_short'
    - 'list_long'
    - 'to_short'
    - 'to_long'
    - 'info'
- functions
    - 'argument_types'
    - 'entity_types'
    - 'info'
    - 'signatures'
    - 'list'
    - 'list_short'
    - 'list_long'
    - 'primary'
        - 'list_short'
        - 'list_long'
    - 'modifier'
        - 'list_short'
        - 'list_long'
    - 'to_short'
    - 'to_long'
- namespaces
    - 'default'
- computed_signatures
'''


def get_specification(version: str) -> Mapping[str, Any]:
    """Get BEL Specification

    The json file this depends on is generated by belspec_yaml2json from the
    manually created and updated BEL Specification yaml file and is kept up to date using
    `make update_ebnf` or `make update_parsers`.  You can also run `belspec_yaml2json`
    directly as it's added as a command by pip install.
    """

    spec_dict = {}

    bel_versions = get_bel_versions()
    if version not in bel_versions:
        log.error('Cannot get unknown version BEL specification')
        return spec_dict

    # use this variable to find our parser file since periods aren't recommended in python module names
    version_underscored = version.replace('.', '_')

    json_fn = f'{os.path.dirname(__file__)}/versions/bel_v{version_underscored}.json'
    with open(json_fn, 'r') as f:
        spec_dict = json.load(f)

    return spec_dict


@click.command()
def belspec_yaml2json():
    """Enhance BEL specification and save as JSON file

    Load all BEL Specification YAML files and convert to JSON files
    after enhancing them.  Also create a bel_versions.json file with
    all available BEL versions for fast loading.


    """

    current_dir = os.path.dirname(__file__)

    bel_versions = []
    bel_versions_fn = f'{current_dir}/versions/bel_versions.json'

    spec_files = glob.glob(f'{current_dir}/versions/bel_v*.yaml')
    for fn in spec_files:
        try:
            spec_dict = yaml.load(open(fn, 'r').read())
        except Exception as e:
            log.error('Warning: BEL Specification {fn} could not be read. Cannot proceed.'.format(fn))
            sys.exit()

        json_fn = fn.replace('yaml', 'json')

        bel_versions.append(spec_dict['version'])

        # admin-related keys
        spec_dict['admin'] = {}
        spec_dict['admin']['version_underscored'] = spec_dict['version'].replace('.', '_')

        # get the current directory name, and use that to find the version's parser file location
        cur_dir_name = os.path.basename(os.path.dirname(os.path.realpath(__file__)))
        parser_path = 'bel.{}.versions.parser_v{}'.format(cur_dir_name, spec_dict['admin']['version_underscored'])
        spec_dict['admin']['parser_path'] = parser_path

        # keys related to creation of EBNF file inside yaml_to_ebnf
        spec_dict['admin']['jinja_template'] = 'bel.ebnf.j2'

        # add relation keys list, to_short, to_long
        spec_dict = add_relations(spec_dict)
        # add function keys list, to_short, to_long
        spec_dict = add_functions(spec_dict)

        spec_dict = enhance_function_signatures(spec_dict)

        spec_dict = add_function_signature_help(spec_dict)

        with open(json_fn, 'w') as f:
            json.dump(spec_dict, f)

    with open(bel_versions_fn, 'w') as f:
        json.dump(bel_versions, f)


def add_function_signature_help(spec_dict: dict) -> dict:
    """Add function signature help

    Simplify the function signatures for presentation to BEL Editor users
    """
    for f in spec_dict['functions']['signatures']:
        for argset_idx, argset in enumerate(spec_dict['functions']['signatures'][f]['signatures']):
            args_summary = ''
            args_list = []
            arg_idx = 0
            for arg_idx, arg in enumerate(spec_dict['functions']['signatures'][f]['signatures'][argset_idx]['arguments']):
                if arg['type'] in ['Function', 'Modifier']:
                    vals = [spec_dict['functions']['to_short'].get(val, spec_dict['functions']['to_short'].get(val)) for val in arg['values']]
                    args_summary += '|'.join(vals) + "()"
                    arg_idx += 1

                    if arg.get('optional', False) and arg.get('multiple', False) is False:
                        args_summary += '?'
                        text = f'Zero or one of each function(s): {", ".join([val for val in arg["values"]])}'
                    elif arg.get('optional', False):
                        args_summary += "*"
                        text = f'Zero or more of each function(s): {", ".join([val for val in arg["values"]])}'
                    else:
                        text = f'One of following function(s): {", ".join([val for val in arg["values"]])}'

                elif arg['type'] in ['NSArg', 'StrArg', 'StrArgNSArg']:
                    args_summary += f'{arg["type"]}'
                    if arg.get('optional', False) and arg.get('multiple', False) is False:
                        args_summary += '?'
                        if arg['type'] in ['NSArg']:
                            text = f'Zero or one namespace argument of following type(s): {", ".join([val for val in arg["values"]])}'
                        elif arg['type'] == 'StrArgNSArg':
                            text = f'Zero or one amespace argument or default namespace argument (without prefix) of following type(s): {", ".join([val for val in arg["values"]])}'
                        else:
                            text = f'Zero or one string argument of following type(s): {", ".join([val for val in arg["values"]])}'
                    elif arg.get('optional', False):
                        args_summary += "*"
                        if arg['type'] in ['NSArg']:
                            text = f'Zero or more namespace arguments of following type(s): {", ".join([val for val in arg["values"]])}'
                        elif arg['type'] == 'StrArgNSArg':
                            text = f'Zero or more namespace arguments or default namespace arguments (without prefix) of following type(s): {", ".join([val for val in arg["values"]])}'
                        else:
                            text = f'Zero or more of string arguments of following type(s): {", ".join([val for val in arg["values"]])}'
                    else:
                        if arg['type'] in ['NSArg']:
                            text = f'Namespace argument of following type(s): {", ".join([val for val in arg["values"]])}'
                        elif arg['type'] == 'StrArgNSArg':
                            text = f'Namespace argument or default namespace argument (without prefix) of following type(s): {", ".join([val for val in arg["values"]])}'
                        else:
                            text = f'String argument of following type(s): {", ".join([val for val in arg["values"]])}'

                args_summary += ', '
                args_list.append(text)

            args_summary = re.sub(', $', '', args_summary)
            spec_dict['functions']['signatures'][f]['signatures'][argset_idx]['argument_summary'] = f'{f}({args_summary})'
            spec_dict['functions']['signatures'][f]['signatures'][argset_idx]['argument_help_listing'] = args_list

            # print(f'{f}({args_summary})')
            # print(args_list)

    return spec_dict


def add_relations(spec_dict: Mapping[str, Any]) -> Mapping[str, Any]:
    """Add relation keys to spec_dict

    Args:
        spec_dict (Mapping[str, Any]): bel specification dictionary

    Returns:
        Mapping[str, Any]: bel specification dictionary with added relation keys
    """

    # TODO: PyCharm gives warning when instantiating the list and the two dicts below in spec_dict:
    # Class 'Mapping' does not define '__setitem__', so the '[]' operator cannot be used on its instances
    spec_dict['relations']['list'] = []
    spec_dict['relations']['list_short'] = []
    spec_dict['relations']['list_long'] = []
    spec_dict['relations']['to_short'] = {}
    spec_dict['relations']['to_long'] = {}

    for relation_name in spec_dict['relations']['info']:

        abbreviated_name = spec_dict['relations']['info'][relation_name]['abbreviation']
        spec_dict['relations']['list'].extend((relation_name, abbreviated_name))
        spec_dict['relations']['list_long'].append(relation_name)
        spec_dict['relations']['list_short'].append(abbreviated_name)

        spec_dict['relations']['to_short'][relation_name] = abbreviated_name
        spec_dict['relations']['to_short'][abbreviated_name] = abbreviated_name

        spec_dict['relations']['to_long'][abbreviated_name] = relation_name
        spec_dict['relations']['to_long'][relation_name] = relation_name

    return spec_dict


def add_functions(spec_dict: Mapping[str, Any]) -> Mapping[str, Any]:
    """Add function keys to spec_dict

    Args:
        spec_dict (Mapping[str, Any]): bel specification dictionary

    Returns:
        Mapping[str, Any]: bel specification dictionary with added function keys
    """

    # TODO: PyCharm gives warning when instantiating the list and the two dicts below in spec_dict:
    # Class 'Mapping' does not define '__setitem__', so the '[]' operator cannot be used on its instances
    spec_dict['functions']['list'] = []
    spec_dict['functions']['list_long'] = []
    spec_dict['functions']['list_short'] = []

    spec_dict['functions']['primary'] = {}
    spec_dict['functions']['primary']['list_long'] = []
    spec_dict['functions']['primary']['list_short'] = []

    spec_dict['functions']['modifier'] = {}
    spec_dict['functions']['modifier']['list_long'] = []
    spec_dict['functions']['modifier']['list_short'] = []

    spec_dict['functions']['to_short'] = {}
    spec_dict['functions']['to_long'] = {}

    for func_name in spec_dict['functions']['info']:

        abbreviated_name = spec_dict['functions']['info'][func_name]['abbreviation']

        spec_dict['functions']['list'].extend((func_name, abbreviated_name))

        spec_dict['functions']['list_long'].append(func_name)
        spec_dict['functions']['list_short'].append(abbreviated_name)

        if spec_dict['functions']['info'][func_name]['type'] == 'primary':
            spec_dict['functions']['primary']['list_long'].append(func_name)
            spec_dict['functions']['primary']['list_short'].append(abbreviated_name)
        else:
            spec_dict['functions']['modifier']['list_long'].append(func_name)
            spec_dict['functions']['modifier']['list_short'].append(abbreviated_name)

        spec_dict['functions']['to_short'][abbreviated_name] = abbreviated_name
        spec_dict['functions']['to_short'][func_name] = abbreviated_name

        spec_dict['functions']['to_long'][abbreviated_name] = func_name
        spec_dict['functions']['to_long'][func_name] = func_name

    return spec_dict


def get_bel_versions() -> List[str]:
    """Get BEL Language versions supported

    Get the list of all BEL Language versions supported.  The file this depends
    on is generated by belspec_yaml2json and is kept up to date using
    `make update_ebnf` or `make update_parsers`.  You can also run `belspec_yaml2json`
    directly as it's added as a command by pip install.

    Returns:
        List[str]: list of versions
    """

    fn = f'{os.path.dirname(__file__)}/versions/bel_versions.json'
    with open(fn, 'r') as f:
        versions = json.load(f)

    return versions


def enhance_function_signatures(spec_dict: Mapping[str, Any]) -> Mapping[str, Any]:
    """Enhance function signatures

    Add required and optional objects to signatures objects for semantic validation
    support.

    Args:
        spec_dict (Mapping[str, Any]): bel specification dictionary

    Returns:
        Mapping[str, Any]: return enhanced bel specification dict
    """

    for func in spec_dict['functions']['signatures']:
        for i, sig in enumerate(spec_dict['functions']['signatures'][func]['signatures']):
            args = sig['arguments']
            req_args = []
            pos_args = []
            opt_args = []
            mult_args = []
            for arg in args:
                # Multiple argument types
                if arg.get('multiple', False):
                    if arg['type'] in ['Function', 'Modifier']:
                        mult_args.extend(arg.get('values', []))
                    elif arg['type'] in ['StrArgNSArg', 'NSArg', 'StrArg']:
                        # Complex signature has this
                        mult_args.append(arg['type'])

                # Optional, position dependent - will be added after req_args based on order in bel_specification
                elif arg.get('optional', False) and arg.get('position', False):
                    if arg['type'] in ['Function', 'Modifier']:
                        pos_args.append(arg.get('values', []))
                    elif arg['type'] in ['StrArgNSArg', 'NSArg', 'StrArg']:
                        pos_args.append(arg['type'])

                # Optional, position independent
                elif arg.get('optional', False):
                    if arg['type'] in ['Function', 'Modifier']:
                        opt_args.extend(arg.get('values', []))
                    elif arg['type'] in ['StrArgNSArg', 'NSArg', 'StrArg']:
                        opt_args.append(arg['type'])

                # Required arguments, position dependent
                else:
                    if arg['type'] in ['Function', 'Modifier']:
                        req_args.append(arg.get('values', []))
                    elif arg['type'] in ['StrArgNSArg', 'NSArg', 'StrArg']:
                        req_args.append(arg['type'])

            spec_dict['functions']['signatures'][func]['signatures'][i]['req_args'] = copy.deepcopy(req_args)
            spec_dict['functions']['signatures'][func]['signatures'][i]['pos_args'] = copy.deepcopy(pos_args)
            spec_dict['functions']['signatures'][func]['signatures'][i]['opt_args'] = copy.deepcopy(opt_args)
            spec_dict['functions']['signatures'][func]['signatures'][i]['mult_args'] = copy.deepcopy(mult_args)

    return spec_dict


def get_function_help(function: str, bel_spec: BELSpec):
    """Get function_help given function name

    This will get the function summary template (argument summary in signature)
    and the argument help listing.
    """

    function_long = bel_spec['functions']['to_long'].get(function)
    function_help = []

    if function_long:
        for signature in bel_spec['functions']['signatures'][function_long]['signatures']:
            function_help.append({
                "function_summary": signature['argument_summary'],
                "argument_help": signature['argument_help_listing'],
                "description": bel_spec['functions']['info'][function_long]['description'],
            })

    return function_help


def main():
    pass
    # import timy
    # with timy.Timer() as timer:
    #     belspec_yaml2json()
    #     print(timer)


if __name__ == '__main__':
    main()
