"""Converter from typedoc output to jsdoc doclet format"""

import os
import sys
import json

from six import iteritems

# JSDoc entries used in sphinx-js:
# - optional access of [ public, private, protected ]
# - optional classdesc
# - optional comment (controls doclet inclusion)
# - optional description
# - optional exceptions
# - optional exclude-members
# -          kind of [ function, typedef, <other> ]
# -          longname
# - optional memberof
# -          meta.filename
# -          meta.lineno
# -          meta.code.paramnames
# -          meta.code.path
# -          name
# - optional params
# - optional properties
# - optional returns
# -          type.names
# - optional undocumented


class TypeDoc(object):
    """
    Encapsulation of the Typedoc to JSDoc conversion process.
    Upon construction this class will convert the typedoc JSON
    object to a list of JSDoc doclets in :py:attr:`jsdoc`.

    This class holds all the state used during the conversion making
    it easy to do multiple (sequential) conversions.

    :ivar jsdoc: the list of generated doclets
    :ivar nodelist: the flattened typedoc entries indexed by 'id'

    JSDoc JSON schema: https://github.com/jsdoc3/jsdoc/blob/master/lib/jsdoc/schema.js
    """

    def __init__(self, root):
        """
        Construct a list of jsdoc entries from the typedoc JSON object.

        :param root: a JSON object from a typedoc JSON file
        """
        self.jsdoc = []
        self.nodelist = {}
        self.make_node_list(root)
        self.convert_node(root)

    def get_parent(self, node):
        """
        Get the parent of a node.

        :param node: A Typedoc node
        :return: The parent Typedoc node, or None if node was the root.
        """
        parentId = node.get('__parentId')
        return self.nodelist[parentId] if parentId is not None else None

    def extend_doclet(self, result, **kwargs):
        """
        Extend a jsdoc entry.

        .. note::

           Filters out keywords with value None. This is used
           explicitely, for example in :py:func:`simple_doclet`, and
           implicitely when typedoc may lack an entry, for example in
           the description field in :py:func:`make_result`.
        """
        result.update(**kwargs)
        return {k: v for k, v in iteritems(result) if v is not None}

    def make_doclet(self, **kwargs):
        """Create a new jsdoc entry"""
        return self.extend_doclet({}, **kwargs)

    def make_longname(self, node):
        """Construct the jsdoc longname entry for a typedoc node"""
        parent = self.get_parent(node)
        longname = self.make_longname(parent) if parent is not None else ''

        kindString = node.get('kindString')
        if kindString in [None, 'Function', 'Constructor', 'Method']:
            return longname
        if longname != '':
            flags = node.get('flags')
            if (parent.get('kindString') in ['Class', 'Interface'] and
                    flags.get('isStatic') is not True):
                longname += '#'
            elif parent.get('kindString') in ['Function', 'Method']:
                longname += '.'
            else:
                longname += '~'
        if kindString == 'Module':
            return longname + 'module:' + node.get('name')[1:-1]
        elif kindString == 'External module':
            return longname + 'external:' + node.get('name')[1:-1]
        else:
            return longname + node.get('name')

    def make_meta(self, node):
        """Construct the jsdoc meta entry for a typedoc node"""
        source = node.get('sources')[0]
        return {
            'path': os.path.dirname(source.get('fileName')) or './',
            'filename': os.path.basename(source.get('fileName')),
            'lineno': source.get('line'),
            'code': {}
        }

    def make_type_name(self, type):
        """Construct the name of a type from a Typedoc type entry"""
        names = []
        if type.get('type') == 'reference' and type.get('id'):
            node = self.nodelist[type.get('id')]
            # Should be: names = [ self.make_longname(node)]
            parent = self.nodelist[node.get('__parentId')]
            if parent.get('kindString') == 'External module':
                names = [parent['name'][1:-1] + '.' + node['name']]
            else:
                names = [node['name']]
        elif type.get('type') in ['intrinsic', 'reference']:
            names = [type.get('name')]
        elif type.get('type') == 'stringLiteral':
            names = ['"' + type.get('value') + '"']
        elif type.get('type') == 'array':
            names = [self.make_type_name(type.get('elementType')) + '[]']
        elif type.get('type') == 'tuple':
            types = [self.make_type_name(t) for t in type.get('elements')]
            names = ['[' + ','.join(types) + ']']
        elif type.get('type') == 'union':
            types = [self.make_type_name(t) for t in type.get('types')]
            names = [' | '.join(types)]
        elif type.get('type') == 'typeOperator':
            target_name = self.make_type_name(type.get('target'))
            names = [type.get('operator'), target_name]
        elif type.get('type') == 'typeParameter':
            names = [type.get('name')]
            constraint = type.get('constraint')
            if constraint is not None:
                names.extend(['extends', self.make_type_name(constraint)])
        elif type.get('type') == 'reflection':
            names = ['<TODO>']
        return ' '.join(names)

    def make_type(self, type):
        """Construct a jsdoc type entry"""
        return {
            'names': [self.make_type_name(type)]
        }

    def make_description(self, comment):
        """Construct a jsdoc description entry"""
        if not comment:
            return ''
        else:
            return '\n\n'.join([
                comment.get('shortText', ''),
                comment.get('text', '')
            ])

    def make_param(self, param):
        """Construct a jsdoc parameter entry"""
        typeEntry = param.get('type')
        if typeEntry is None:
            return self.make_doclet(
                name=param.get('name'),
                description=self.make_description(param.get('comment'))
            )
        else:
            return self.make_doclet(
                name=param.get('name'),
                type=self.make_type(typeEntry),
                description=self.make_description(param.get('comment'))
            )

    def make_result(self, param):
        """Construct a jsdoc function result entry"""
        type = param.get('type')
        if type is None or type.get('name') == 'void':
            return []