# Copyright 2017 Stefan Richthofer
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
#     http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Created on 13.12.2016

import inspect, typing

import pytypes
from .exceptions import TypeSyntaxError

try:
    from backports.typing import Any, Tuple
except ImportError:
    from typing import Any, Tuple


def _striptrailingcomment(s):
    pos = s.find('#')
    if pos > -1:
        return s[:pos].strip()
    else:
        return s.strip()


def _parse_typecomment_oneline(line):
    commStart = line.find('#')
    tp_delim = 'type'
    if commStart > -1 and len(line) > commStart+1:
        comment = line[commStart+1:].strip()
        if (comment.startswith(tp_delim) and len(comment) > len(tp_delim)+1):
            comment = comment[len(tp_delim):].strip()
            if (comment.startswith(':')):
                comment = comment[1:].strip()
                if len(comment) > 0:
                    return comment
    return None


def _get_typestrings(obj, slf):
    try:
        srclines = inspect.getsourcelines(obj)[0]
    except TypeError:
        try:
            srclines = inspect.getsourcelines(getattr(obj.__class__, obj.__name__))[0]
        except AttributeError:
            # This can happen e.g. for slot wrappers.
            # See https://github.com/Stewori/pytypes/issues/65
            return None
    except:
        return None
    funcstart = 0
    startInit = False
    result = []
    for line in srclines:
        ln = _striptrailingcomment(line)
        if len(ln) > 0:
            if ln.startswith('def '):
                startInit = True
            if startInit:
                if ln.endswith(':'):
                    if ln[:-1].strip().endswith(')') or ln.find('->') != -1:
                        break
                elif not ln[-1] == '(':
                    result.append(_parse_typecomment_oneline(line))
        funcstart += 1
    if len(srclines) <= funcstart:
        return None
    res = _parse_typecomment_oneline(srclines[funcstart])
    if not res is None:
        return res, result[1:] if slf else result
    if len(srclines) > funcstart+1:
        strp = srclines[funcstart+1].strip()
        if len(strp) > 0 and strp[0] == '#':
            res = _parse_typecomment_oneline(srclines[funcstart+1]), result[1:] if slf else result
            return res
    return None, result[1:] if slf else result


def _isargsellipsis(argStr):
    return argStr[1:-1].strip() == '...'


def _make_typestring_err_msg(msg, typestring, func, slf, func_class):
    fq_func_name = pytypes.util._fully_qualified_func_name(func, slf, func_class)
    return '\n  '+fq_func_name+'\n  '+msg+':\n'+typestring


def _outter_split(inpt, delim, openers, closers=None, opener_lookup=None):
    """Splits only at delims that are at outter-most level regarding
    openers/closers pairs.
    Unchecked requirements:
    Only supports length-1 delim, openers and closers.
    delim must not be member of openers or closers.
    len(openers) == len(closers) or closers == None
    """
    if closers is None:
        closers = openers
    if opener_lookup is None:
        opener_lookup = {}
        for i in range(len(openers)):
            opener_lookup[openers[i]] = i
    stack = []
    res = []
    splt = 0
    for i in range(len(inpt)):
        if inpt[i] == delim and len(stack) == 0:
            res.append(inpt[splt:i].strip())
            splt = i+1
        elif inpt[i] in opener_lookup:
            stack.append(opener_lookup[inpt[i]])
        elif len(stack) > 0 and inpt[i] == closers[stack[-1]]:
            stack.pop()
    res.append(inpt[splt:].strip())
    return res


_openers = ('[', '(', '{', '"', "'")
_closers = (']', ')', '}', '"', "'")
_opener_lookup = {'[': 0, '(': 1, '{': 2, '"': 3, "'": 4}
def _check_vararg_typestring(typestring, argString, argspec, func, slf, func_class):
    vkw_count = argString.count('**')
    args = None
    if vkw_count > 1:
        raise TypeSyntaxError(_make_typestring_err_msg(
                "Typestring contains multiple var-keywords ('**') args",
                typestring, func, slf, func_class))
    if vkw_count > 0:
        args = _outter_split(argString.strip()[1:-1], ',', _openers, _closers, _opener_lookup)
        argString = argString.replace('**', '')
    varg_count = argString.count('*')
    if varg_count > 1:
        raise TypeSyntaxError(_make_typestring_err_msg(
                "Typestring contains multiple var-length ('*') args",
                typestring, func, slf, func_class))
    if varg_count > 0:
        if args is None:
            args = _outter_split(argString.strip()[1:-1], ',', _openers, _closers, _opener_lookup)
        argString = argString.replace('*', '')
    if not args is None:
        tpnames = pytypes.util.getargnames(argspec)
        if slf:
            #if not tpnames[0] == 'self':
                #todo: maybe warn here
            tpnames = tpnames[1:]
        idx = 0
        assert_count = 0
        assert_count_kw = 0
        for arg in args:
            if arg.startswith('**'):
                assert_count_kw += 1
                kw_msg = "'**' misplaced in typestring"
                try:
                    if not tpnames[idx] == argspec.varkw:
                        raise TypeSyntaxError(_make_typestring_err_msg(
                                kw_msg, typestring, func, slf, func_class))
                except AttributeError:
                    if not tpnames[idx] == argspec.keywords:
                        raise TypeSyntaxError(_make_typestring_err_msg(
                                kw_msg, typestring, func, slf, func_class))
            elif arg.startswith('*'):
                assert_count += 1
                if idx >= len(tpnames) or not tpnames[idx] == argspec.varargs:
                    raise TypeSyntaxError(_make_typestring_err_msg(
                            "'*' misplaced in typestring", typestring, func, slf, func_class))
            idx += 1
        if varg_count != assert_count or vkw_count != assert_count_kw:
            raise TypeSyntaxError(_make_typestring_err_msg(
                    'Invalid typestring syntax', typestring, func, slf, func_class))

    if vkw_count == 1:
        vkw = None
        try:
            vkw = argspec.varkw
        except AttributeError:
            vkw = argspec.keywords
        if vkw is None:
            raise TypeSyntaxError(_make_typestring_err_msg(
                    'Typestring lists var-keywords not declared in %s header.'
                    % ('method' if slf else 'function'),
                    typestring, func, slf, func_class))
    elif vkw_count == 0:
        try:
            vkw = argspec.varkw
        except AttributeError:
            vkw = argspec.keywords
        if not vkw is None:
            raise TypeSyntaxError(_make_typestring_err_msg(
                    'Typestring does not account for declared var-keywords',
                    typestring, func, slf, func_class))
    if varg_count == 1:
        if argspec.varargs is None:
            raise TypeSyntaxError(_make_typestring_err_msg(
                    'Typestring lists var-length args not declared in %s header.'
                    % ('method' if slf else 'function'),
                    typestring, func, slf, func_class))
    elif varg_count == 0:
        if not argspec.varargs is None:
            raise TypeSyntaxError(_make_typestring_err_msg(
                    'Typestring does not account for declared var-length args',
                    typestring, func, slf, func_class))
    return argString


def _funcsigtypesfromstring(typestring, argTypes=None, argspec=None, glbls=globals(),
        selfType=None, argCount=None, unspecified_type=Any, defaults=None, func=None,
        slf=False, func_class=None):
    splt = typestring.find('->')
    if splt == -1:
        return None
    argString = typestring[:splt].strip()
    if not argspec is None:
        argString = _check_vararg_typestring(typestring, argString, argspec, func, slf, func_class)
    if _isargsellipsis(argString):
# 		useEllipsis = True
        if not argTypes is None:
            argString = ''.join(('(', ', '.join(['Any' if x is None else x for x in argTypes]), ')'))
# 	else:
# 		useEllipsis = False
    argTypes0 = argTypes
    resString = typestring[splt+2:].strip()
    if pytypes.tp_comment_parser_import_typing:
        for tpname in typing.__all__:
            if not tpname in glbls:
                glbls[tpname] = getattr(typing, tpname)
    argTp = eval(argString, glbls)
    if selfType is None:
        argTypes = []
    else:
        argTypes = [selfType]
    try:
        argTypes += argTp
    except TypeError:
        argTypes.append(argTp)
    uc = 0
    if not argCount is None:
        if argTypes0 is None or len(argTypes0) <= len(argTypes):
            while len(argTypes) < argCount:
                argTypes.append(unspecified_type)
                uc += 1
        else:
            while len(argTypes) < argCount:
                if len(argTypes) < len(argTypes0):
                    argTypes.append(argTypes0[len(argTypes)])
                else:
                    argTypes.append(unspecified_type)
                    uc += 1
    if not defaults is None:
        if len(defaults) < uc:
            uc = len(defaults)
        for i in range(uc):
            argTypes[-1-i] = pytypes.deep_type(defaults[-1-i])
    # Note: Tuple constructor automatically normalizes None to NoneType
    tpl = Tuple[tuple(argTypes)]
# 	if useEllipsis:
# 		tpl.__tuple_use_ellipsis__ = True

    # Normalize occurrence of None to type(None).
    # (Doing this in pre-eval manner/text-mode is easier than going
    #  through maybe nested type-vars, etc)
    # To avoid that this creates type(type(None)) if type(None) is already in place:
    resString = resString.replace('type(None)', 'None')
    resString = resString.replace('None', 'type(None)')	

    resType = eval(resString, glbls)
    return tpl, resType
