# -*- coding: utf-8 -*-

# Copyright (c) 2013 - 2023 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Parse a JavaScript file and retrieve variables and functions.

It uses the JavaScript parser contained in the jasy web framework.
"""

import jasy.script.parse.Parser as jsParser
import jasy.script.tokenize.Tokenizer as jsTokenizer

from eric7 import Utilities
from eric7.Utilities import ClassBrowsers

from . import ClbrBaseClasses

SUPPORTED_TYPES = [ClassBrowsers.JS_SOURCE]


class VisibilityMixin(ClbrBaseClasses.ClbrVisibilityMixinBase):
    """
    Mixin class implementing the notion of visibility.
    """

    def __init__(self):
        """
        Constructor
        """
        if self.name.startswith("__"):
            self.setPrivate()
        elif self.name.startswith("_"):
            self.setProtected()
        else:
            self.setPublic()


class Function(ClbrBaseClasses.Function, VisibilityMixin):
    """
    Class to represent a Python function.
    """

    def __init__(self, module, name, file, lineno, signature="", separator=","):
        """
        Constructor

        @param module name of the module containing this function
        @param name name of this function
        @param file filename containing this class
        @param lineno linenumber of the class definition
        @param signature parameterlist of the method
        @param separator string separating the parameters
        """
        ClbrBaseClasses.Function.__init__(
            self, module, name, file, lineno, signature, separator
        )
        VisibilityMixin.__init__(self)


class Attribute(ClbrBaseClasses.Attribute, VisibilityMixin):
    """
    Class to represent a class attribute.
    """

    def __init__(self, module, name, file, lineno):
        """
        Constructor

        @param module name of the module containing this class
        @param name name of this class
        @param file filename containing this attribute
        @param lineno linenumber of the class definition
        """
        ClbrBaseClasses.Attribute.__init__(self, module, name, file, lineno)
        VisibilityMixin.__init__(self)


class Visitor:
    """
    Class implementing a visitor going through the parsed tree.
    """

    def __init__(self, src, module, filename):
        """
        Constructor

        @param src source to be parsed (string)
        @param module name of the module (string)
        @param filename file name (string)
        """
        self.__dict = {}
        self.__dict_counts = {}
        self.__root = None
        self.__stack = []

        self.__module = module
        self.__file = filename
        self.__source = src

        # normalize line endings
        self.__source = self.__source.replace("\r\n", "\n").replace("\r", "\n")

        # ensure source ends with an eol
        if bool(self.__source) and self.__source[-1] != "\n":
            self.__source = self.__source + "\n"

    def parse(self):
        """
        Public method to parse the source.

        @return dictionary containing the parsed information
        """
        try:
            self.__root = jsParser.parse(self.__source, self.__file)
            self.__visit(self.__root)
        except jsParser.SyntaxError:
            # ignore syntax errors of the parser
            pass
        except jsTokenizer.ParseError:
            # ignore syntax errors of the tokenizer
            pass

        return self.__dict

    def __visit(self, root):
        """
        Private method implementing the visit logic delegating to interesting
        methods.

        @param root root node to visit
        """

        def call(n):
            getattr(self, "visit_{0}".format(n.type), self.visit_noop)(n)

        call(root)
        for node in root:
            self.__visit(node)

    def visit_noop(self, node):
        """
        Public method to ignore the given node.

        @param node reference to the node (jasy.script.parse.Node.Node)
        """
        pass

    def visit_function(self, node):
        """
        Public method to treat a function node.

        @param node reference to the node (jasy.script.parse.Node.Node)
        """
        if (
            node.type == "function"
            and getattr(node, "name", None)
            and node.functionForm == "declared_form"
        ):
            if self.__stack and self.__stack[-1].endlineno < node.line:
                del self.__stack[-1]
            endline = node.line + self.__source.count("\n", node.start, node.end)
            if getattr(node, "params", None):
                func_sig = ", ".join([p.value for p in node.params])
            else:
                func_sig = ""
            if self.__stack:
                # it's a nested function
                cur_func = self.__stack[-1]
                f = Function(None, node.name, self.__file, node.line, func_sig)
                f.setEndLine(endline)
                cur_func._addmethod(node.name, f)
            else:
                f = Function(self.__module, node.name, self.__file, node.line, func_sig)
                f.setEndLine(endline)
                func_name = node.name
                if func_name in self.__dict_counts:
                    self.__dict_counts[func_name] += 1
                    func_name = "{0}_{1:d}".format(
                        func_name, self.__dict_counts[func_name]
                    )
                else:
                    self.__dict_counts[func_name] = 0
                self.__dict[func_name] = f
            self.__stack.append(f)

    def visit_property_init(self, node):
        """
        Public method to treat a property_init node.

        @param node reference to the node (jasy.script.parse.Node.Node)
        """
        if node.type == "property_init" and node[1].type == "function":
            if self.__stack and self.__stack[-1].endlineno < node[0].line:
                del self.__stack[-1]
            endline = node[0].line + self.__source.count("\n", node.start, node[1].end)
            if getattr(node[1], "params", None):
                func_sig = ", ".join([p.value for p in node[1].params])
            else:
                func_sig = ""
            if self.__stack:
                # it's a nested function
                cur_func = self.__stack[-1]
                f = Function(None, node[0].value, self.__file, node[0].line, func_sig)
                f.setEndLine(endline)
                cur_func._addmethod(node[0].value, f)
            else:
                f = Function(
                    self.__module, node[0].value, self.__file, node[0].line, func_sig
                )
                f.setEndLine(endline)
                func_name = node[0].value
                if func_name in self.__dict_counts:
                    self.__dict_counts[func_name] += 1
                    func_name = "{0}_{1:d}".format(
                        func_name, self.__dict_counts[func_name]
                    )
                else:
                    self.__dict_counts[func_name] = 0
                self.__dict[func_name] = f
            self.__stack.append(f)

    def visit_var(self, node):
        """
        Public method to treat a variable node.

        @param node reference to the node (jasy.script.parse.Node.Node)
        """
        if (
            node.type == "var"
            and node.parent.type == "script"
            and node.getChildrenLength()
        ):
            if self.__stack and self.__stack[-1].endlineno < node[0].line:
                del self.__stack[-1]
            if self.__stack:
                # function variables
                for var in node:
                    attr = Attribute(self.__module, var.name, self.__file, var.line)
                    self.__stack[-1]._addattribute(attr)
            else:
                # global variable
                if "@@Globals@@" not in self.__dict:
                    self.__dict["@@Globals@@"] = ClbrBaseClasses.ClbrBase(
                        self.__module, "Globals", self.__file, 0
                    )
                for var in node:
                    self.__dict["@@Globals@@"]._addglobal(
                        Attribute(self.__module, var.name, self.__file, var.line)
                    )

    def visit_const(self, node):
        """
        Public method to treat a constant node.

        @param node reference to the node (jasy.script.parse.Node.Node)
        """
        if (
            node.type == "const"
            and node.parent.type == "script"
            and node.getChildrenLength()
        ):
            if self.__stack and self.__stack[-1].endlineno < node[0].line:
                del self.__stack[-1]
            if self.__stack:
                # function variables
                for var in node:
                    attr = Attribute(
                        self.__module, "const " + var.name, self.__file, var.line
                    )
                    self.__stack[-1]._addattribute(attr)
            else:
                # global variable
                if "@@Globals@@" not in self.__dict:
                    self.__dict["@@Globals@@"] = ClbrBaseClasses.ClbrBase(
                        self.__module, "Globals", self.__file, 0
                    )
                for var in node:
                    self.__dict["@@Globals@@"]._addglobal(
                        Attribute(
                            self.__module, "const " + var.name, self.__file, var.line
                        )
                    )


def readmodule_ex(module, path=None, isTypeFile=False):
    """
    Read a JavaScript file and return a dictionary of functions and variables.

    @param module name of the JavaScript file
    @type str
    @param path path the file should be searched in
    @type list of str
    @param isTypeFile flag indicating a file of this type
    @type bool
    @return the resulting dictionary
    @rtype dict
    """
    # search the path for the file
    f = None
    fullpath = [] if path is None else path[:]
    f, file, (suff, mode, type) = ClassBrowsers.find_module(module, fullpath)
    if f:
        f.close()
    if type not in SUPPORTED_TYPES:
        # not Javascript source, can't do anything with this module
        return {}

    try:
        src = Utilities.readEncodedFile(file)[0]
    except (UnicodeError, OSError):
        # can't do anything with this module
        return {}

    return scan(src, file, module)


def scan(src, file, module):
    """
    Public method to scan the given source text.

    @param src source text to be scanned
    @type str
    @param file file name associated with the source text
    @type str
    @param module module name associated with the source text
    @type str
    @return dictionary containing the extracted data
    @rtype dict
    """
    # convert eol markers the Python style
    src = src.replace("\r\n", "\n").replace("\r", "\n")

    dictionary = {}

    visitor = Visitor(src, module, file)
    dictionary = visitor.parse()
    return dictionary
