"""node.py - Class node for creating a parse tree for jinja statements and
             checking jinja statement indentation.
"""
from j2lint.linter.indenter.statement import JinjaStatement, JINJA_STATEMENT_TAG_NAMES
from j2lint.linter.error import JinjaLinterError, JinjaBadIndentationError
from j2lint.utils import flatten, get_tuple, delimit_jinja_statement
from j2lint.logger import logger

BEGIN_TAGS = [item[0] for item in JINJA_STATEMENT_TAG_NAMES]
END_TAGS = [item[-1] for item in JINJA_STATEMENT_TAG_NAMES]
MIDDLE_TAGS = list(flatten([[i[1:-1] for i in JINJA_STATEMENT_TAG_NAMES]]))

INDENT_SHIFT = 4
DEFAULT_WHITESPACES = 1
BLOCK_START_INDENT = 0
JINJA_START_DELIMITERS = ['{%-', '{%+']

jinja_node_stack = []
jinja_delimiter_stack = []


class Node:
    """Node class which represents a jinja file as a tree
    """
    statement = None
    tag = None
    node_start = 0
    node_end = 0
    children = []
    expected_indent = 0

    def create_node(self, line, line_no, indent_level=0):
        """Initializes a Node class object

        Args:
            line (string): line to create node for
            line_no (int): line number
            indent_level (int, optional): expected indentation level. Defaults to 0.

        Returns:
            Node: new Node class object
        """
        node = Node()
        statement = JinjaStatement(line)
        node.statement = statement
        node.tag = statement.words[0]
        node.node_start = line_no
        node.node_end = line_no
        node.expected_indent = indent_level
        node.parent = self
        return node

    def create_indentation_error(self, node, message):
        """Creates indentation error tuple

        Args:
            node (Node): Node class object to create error for
            message (string): error message for the line

        Returns:
            tuple: tuple representing the indentation error
        """
        return (node.statement.start_line_no,
                delimit_jinja_statement(node.statement.line,
                                        node.statement.start_delimiter,
                                        node.statement.end_delimiter),
                message)

    def check_indent_level(self, result, node):
        """check if the actual and expected indent level for a line match

        Args:
            result (list): list of tuples of indentation errors
            node (Node): Node object for which to check the level is correct
        """
        actual = node.statement.begin
        if len(jinja_node_stack) and jinja_node_stack[0].statement.start_delimiter in JINJA_START_DELIMITERS:
            BLOCK_START_INDENT = 1
        elif node.expected_indent == 0 and node.statement.start_delimiter in JINJA_START_DELIMITERS:
            BLOCK_START_INDENT = 1
        else:
            BLOCK_START_INDENT = 0

        if node.statement.start_delimiter in JINJA_START_DELIMITERS:
            expected = node.expected_indent + BLOCK_START_INDENT
        else:
            expected = node.expected_indent + DEFAULT_WHITESPACES + BLOCK_START_INDENT
        if actual != expected:
            message = "Bad Indentation, expected %d, got %d" % (
                expected, actual)
            error = self.create_indentation_error(node, message)
            result.append(error)
            logger.debug(error)

    def check_indentation(self, result, lines, line_no=0, indent_level=0):
        """Checks indentation for a list of lines

        Args:
            result (list): list of indentation error tuples
            lines (list): lines which are to be checked for indentation
            line_no (int, optional):  the current lines number being evaluated.
                                      Defaults to 0.
            indent_level (int, optional): the expected indent level for the
                                          current line. Defaults to 0.

        Raises:
            JinjaLinterError: Raises error if the text file has jinja tags
                              which are not supported by this indenter

        Returns:
            list: updates the 'result' list argument with indentation errors
        """
        while line_no < len(lines):
            line = lines[line_no]
            node = self.create_node(line, line_no, indent_level)
            if node.tag in BEGIN_TAGS:
                jinja_node_stack.append(node)
                self.children.append(node)
                line_no = node.check_indentation(
                    result, lines, line_no + 1, indent_level + INDENT_SHIFT)
                self.check_indent_level(result, node)
                continue
            elif node.tag in END_TAGS:
                if ('end' + jinja_node_stack[-1].tag) == node.tag:
                    if len(jinja_node_stack) and jinja_node_stack[-1] != self:
                        del node
                        return line_no
                    matchnode = jinja_node_stack[-1]
                    matchnode.node_end = line_no
                    node.node_end = line_no
                    node.expected_indent = matchnode.expected_indent
                    self.parent.children.append(node)
                    if matchnode == self:
                        line_no = line_no + 1
                        self.check_indent_level(result, node)
                        jinja_node_stack.pop()
                        return line_no
                message = "Tag is out of order '{}'".format(node.tag)
                error = self.create_indentation_error(node, message)
                result.append(error)
                raise JinjaLinterError(
                    "Tag is out of order '{}'".format(node.tag))
            elif node.tag in MIDDLE_TAGS:
                begin_tag_tuple = get_tuple(
                    JINJA_STATEMENT_TAG_NAMES, jinja_node_stack[-1].tag)
                if node.tag in begin_tag_tuple:
                    if jinja_node_stack[-1] != self:
                        del node
                        return line_no
                    matchnode = jinja_node_stack[-1]
                    node.node_end = line_no
                    node.expected_indent = matchnode.expected_indent
                    indent_level = node.expected_indent
                    matchnode.parent.children.append(node)
                    node.parent = matchnode.parent
                    line_no = node.check_indentation(
                        result, lines, line_no + 1, indent_level + INDENT_SHIFT)
                    self.check_indent_level(result, node)
                    continue
                else:
                    message = "Unsupported tag '%s' found" % (node.tag)
                    error = self.create_indentation_error(node, message)
                    result.append(error)
                    raise JinjaLinterError(
                        "Unsupported tag '%s' found" % (node.tag))
            else:
                self.children.append(node)
                line_no = line_no + 1
                self.check_indent_level(result, node)
                continue

"""node.py - Class node for creating a parse tree for jinja statements and
             checking jinja statement indentation.
"""
from j2lint.linter.indenter.statement import JinjaStatement, JINJA_STATEMENT_TAG_NAMES
from j2lint.linter.error import JinjaLinterError, JinjaBadIndentationError
from j2lint.utils import flatten, get_tuple, delimit_jinja_statement
from j2lint.logger import logger

BEGIN_TAGS = [item[0] for item in JINJA_STATEMENT_TAG_NAMES]
END_TAGS = [item[-1] for item in JINJA_STATEMENT_TAG_NAMES]
MIDDLE_TAGS = list(flatten([[i[1:-1] for i in JINJA_STATEMENT_TAG_NAMES]]))

INDENT_SHIFT = 4
DEFAULT_WHITESPACES = 1
BLOCK_START_INDENT = 0
JINJA_START_DELIMITERS = ['{%-', '{%+']

jinja_node_stack = []
jinja_delimiter_stack = []


class Node:
    """Node class which represents a jinja file as a tree
    """
    statement = None
    tag = None
    node_start = 0
    node_end = 0
    children = []
    expected_indent = 0

    def create_node(self, line, line_no, indent_level=0):
        """Initializes a Node class object

        Args:
            line (string): line to create node for
            line_no (int): line number
            indent_level (int, optional): expected indentation level. Defaults to 0.

        Returns:
            Node: new Node class object
        """
        node = Node()
        statement = JinjaStatement(line)
        node.statement = statement
        node.tag = statement.words[0]
        node.node_start = line_no
        node.node_end = line_no
        node.expected_indent = indent_level
        node.parent = self
        return node

    def create_indentation_error(self, node, message):
        """Creates indentation error tuple

        Args:
            node (Node): Node class object to create error for
            message (string): error message for the line

        Returns:
            tuple: tuple representing the indentation error
        """
        return (node.statement.start_line_no,
                delimit_jinja_statement(node.statement.line,
                                        node.statement.start_delimiter,
                                        node.statement.end_delimiter),
                message)

    def check_indent_level(self, result, node):
        """check if the actual and expected indent level for a line match

        Args:
            result (list): list of tuples of indentation errors
            node (Node): Node object for which to check the level is correct
        """
        actual = node.statement.begin
        if len(jinja_node_stack) and jinja_node_stack[0].statement.start_delimiter in JINJA_START_DELIMITERS:
            BLOCK_START_INDENT = 1
        elif node.expected_indent == 0 and node.statement.start_delimiter in JINJA_START_DELIMITERS:
            BLOCK_START_INDENT = 1
        else:
            BLOCK_START_INDENT = 0

        if node.statement.start_delimiter in JINJA_START_DELIMITERS:
            expected = node.expected_indent + BLOCK_START_INDENT
        else:
            expected = node.expected_indent + DEFAULT_WHITESPACES + BLOCK_START_INDENT
        if actual != expected:
            message = "Bad Indentation, expected %d, got %d" % (
                expected, actual)
            error = self.create_indentation_error(node, message)
            result.append(error)
            logger.debug(error)

    def check_indentation(self, result, lines, line_no=0, indent_level=0):
        """Checks indentation for a list of lines

        Args:
            result (list): list of indentation error tuples
            lines (list): lines which are to be checked for indentation
            line_no (int, optional):  the current lines number being evaluated.
                                      Defaults to 0.
            indent_level (int, optional): the expected indent level for the
                                          current line. Defaults to 0.

        Raises:
            JinjaLinterError: Raises error if the text file has jinja tags
                              which are not supported by this indenter

        Returns:
            list: updates the 'result' list argument with indentation errors
        """
        while line_no < len(lines):
            line = lines[line_no]
            node = self.create_node(line, line_no, indent_level)
            if node.tag in BEGIN_TAGS:
                jinja_node_stack.append(node)
                self.children.append(node)
                line_no = node.check_indentation(
                    result, lines, line_no + 1, indent_level + INDENT_SHIFT)
                self.check_indent_level(result, node)
                continue
            elif node.tag in END_TAGS:
                if ('end' + jinja_node_stack[-1].tag) == node.tag:
                    if len(jinja_node_stack) and jinja_node_stack[-1] != self:
                        del node
                        return line_no
                    matchnode = jinja_node_stack[-1]
                    matchnode.node_end = line_no
                    node.node_end = line_no
                    node.expected_indent = matchnode.expected_indent
                    self.parent.children.append(node)
                    if matchnode == self:
                        line_no = line_no + 1
                        self.check_indent_level(result, node)
                        jinja_node_stack.pop()
                        return line_no
                message = "Tag is out of order '{}'".format(node.tag)
                error = self.create_indentation_error(node, message)
                result.append(error)
                raise JinjaLinterError(
                    "Tag is out of order '{}'".format(node.tag))
            elif node.tag in MIDDLE_TAGS:
                begin_tag_tuple = get_tuple(
                    JINJA_STATEMENT_TAG_NAMES, jinja_node_stack[-1].tag)
                if node.tag in begin_tag_tuple:
                    if jinja_node_stack[-1] != self:
                        del node
                        return line_no
                    matchnode = jinja_node_stack[-1]
                    node.node_end = line_no
                    node.expected_indent = matchnode.expected_indent
                    indent_level = node.expected_indent
                    matchnode.parent.children.append(node)
                    node.parent = matchnode.parent
                    line_no = node.check_indentation(
                        result, lines, line_no + 1, indent_level + INDENT_SHIFT)
                    self.check_indent_level(result, node)
                    continue
                else:
                    message = "Unsupported tag '%s' found" % (node.tag)
                    error = self.create_indentation_error(node, message)
                    result.append(error)
                    raise JinjaLinterError(
                        "Unsupported tag '%s' found" % (node.tag))
            else:
                self.children.append(node)
                line_no = line_no + 1
                self.check_indent_level(result, node)
                continue

node.py - Class node for creating a parse tree for jinja statements and              checking jinja statement indentation. Node class which represents a jinja file as a tree Initializes a Node class object          Args:             line (string): line to create node for             line_no (int): line number             indent_level (int, optional): expected indentation level. Defaults to 0.          Returns:             Node: new Node class object Creates indentation error tuple          Args:             node (Node): Node class object to create error for             message (string): error message for the line          Returns:             tuple: tuple representing the indentation error check if the actual and expected indent level for a line match          Args:             result (list): list of tuples of indentation errors             node (Node): Node object for which to check the level is correct Checks indentation for a list of lines          Args:             result (list): list of indentation error tuples             lines (list): lines which are to be checked for indentation             line_no (int, optional):  the current lines number being evaluated.                                       Defaults to 0.             indent_level (int, optional): the expected indent level for the                                           current line. Defaults to 0.          Raises:             JinjaLinterError: Raises error if the text file has jinja tags                               which are not supported by this indenter          Returns:             list: updates the 'result' list argument with indentation errors