import re
from enum import Enum
from typing import List, Optional, Set

from pydantic import BaseModel, Field, root_validator, validator
from typing_extensions import deprecated

from .parser.comment import get_comment_symbol

BlockPath = List[str]


class SpanMarker(Enum):
    TAG = 1
    COMMENT = 2


class CodeBlockTypeGroup(str, Enum):
    STRUCTURE = 'Structures'
    IMPLEMENTATION = 'Implementation'
    IMPORT = 'Imports'

    BLOCK_DELIMITER = 'BlockDelimiter'
    SPACE = 'Space'

    COMMENT = 'Comment'

    ERROR = 'Error'


class CodeBlockType(Enum):
    MODULE = (
        'Module',
        CodeBlockTypeGroup.STRUCTURE,
    )  # TODO: Module shouldn't be a STRUCTURE
    CLASS = ('Class', CodeBlockTypeGroup.STRUCTURE)
    FUNCTION = ('Function', CodeBlockTypeGroup.STRUCTURE)

    # TODO: Remove and add sub types to functions and classes
    CONSTRUCTOR = ('Constructor', CodeBlockTypeGroup.STRUCTURE)
    TEST_SUITE = ('TestSuite', CodeBlockTypeGroup.STRUCTURE)
    TEST_CASE = ('TestCase', CodeBlockTypeGroup.STRUCTURE)

    IMPORT = ('Import', CodeBlockTypeGroup.IMPORT)

    EXPORT = ('Export', CodeBlockTypeGroup.IMPLEMENTATION)
    COMPOUND = ('Compound', CodeBlockTypeGroup.IMPLEMENTATION)
    # Dependent clauses are clauses that are dependent on another compound statement and can't be shown on their own
    DEPENDENT_CLAUSE = ('DependentClause', CodeBlockTypeGroup.IMPLEMENTATION)
    ASSIGNMENT = ('Assignment', CodeBlockTypeGroup.IMPLEMENTATION)
    CALL = ('Call', CodeBlockTypeGroup.IMPLEMENTATION)
    STATEMENT = ('Statement', CodeBlockTypeGroup.IMPLEMENTATION)

    CODE = ('Code', CodeBlockTypeGroup.IMPLEMENTATION)

    # TODO: Incorporate in code block?
    BLOCK_DELIMITER = ('BlockDelimiter', CodeBlockTypeGroup.BLOCK_DELIMITER)

    # TODO: Remove as it's just to fill upp spaces at the end of the file?
    SPACE = ('Space', CodeBlockTypeGroup.SPACE)

    COMMENT = ('Comment', CodeBlockTypeGroup.COMMENT)
    COMMENTED_OUT_CODE = (
        'Placeholder',
        CodeBlockTypeGroup.COMMENT,
    )  # TODO: Replace to PlaceholderComment

    ERROR = ('Error', CodeBlockTypeGroup.ERROR)

    def __init__(self, value: str, group: CodeBlockTypeGroup):
        # self.value = value  # self._value_ = value
        self.group = group

    @classmethod
    def from_string(cls, tag: str) -> Optional['CodeBlockType']:
        if not tag.startswith('definition'):
            return None

        tag_to_block_type = {
            'definition.assignment': cls.ASSIGNMENT,
            'definition.block_delimiter': cls.BLOCK_DELIMITER,
            'definition.call': cls.CALL,
            'definition.class': cls.CLASS,
            'definition.code': cls.CODE,
            'definition.comment': cls.COMMENT,
            'definition.compound': cls.COMPOUND,
            'definition.constructor': cls.CONSTRUCTOR,
            'definition.dependent_clause': cls.DEPENDENT_CLAUSE,
            'definition.error': cls.ERROR,
            'definition.export': cls.EXPORT,
            'definition.function': cls.FUNCTION,
            'definition.import': cls.IMPORT,
            'definition.module': cls.MODULE,
            'definition.statement': cls.STATEMENT,
            'definition.test_suite': cls.TEST_SUITE,
            'definition.test_case': cls.TEST_CASE,
        }
        return tag_to_block_type.get(tag)


NON_CODE_BLOCKS = [
    CodeBlockType.BLOCK_DELIMITER,
    CodeBlockType.COMMENT,
    CodeBlockType.COMMENTED_OUT_CODE,
    CodeBlockType.EXPORT,
    CodeBlockType.IMPORT,
    CodeBlockType.ERROR,
    CodeBlockType.SPACE,
]

INDEXED_BLOCKS = [
    CodeBlockType.FUNCTION,
    CodeBlockType.CLASS,
    CodeBlockType.TEST_SUITE,
    CodeBlockType.TEST_CASE,
]


@deprecated('Use BlockSpans to define code block visibility instead')
class PathTree(BaseModel):
    show: bool = Field(default=False, description='Show the block and all sub blocks.')
    tree: dict[str, 'PathTree'] = Field(default_factory=dict)

    @staticmethod
    def from_block_paths(block_paths: List[BlockPath]) -> 'PathTree':
        tree = PathTree()
        for block_path in block_paths:
            tree.add_to_tree(block_path)

        return tree

    def child_tree(self, key: str) -> Optional['PathTree']:
        return self.tree.get(key, None)

    def merge(self, other: 'PathTree'):
        if other.show:
            self.show = True

        for key, value in other.tree.items():
            if key not in self.tree:
                self.tree[key] = PathTree()
            self.tree[key].merge(value)

    def extend_tree(self, paths: list[list[str]]):
        for path in paths:
            self.add_to_tree(path)

    def add_to_tree(self, path: Optional[list[str]] = None):
        if path is None:
            return

        if len(path) == 0:
            self.show = True
            return

        if len(path) == 1:
            if path[0] not in self.tree:
                self.tree[path[0]] = PathTree(show=True)
            else:
                self.tree[path[0]].show = True

            return

        if path[0] not in self.tree:
            self.tree[path[0]] = PathTree(show=False)

        self.tree[path[0]].add_to_tree(path[1:])


class ReferenceScope(str, Enum):
    EXTERNAL = 'external'
    DEPENDENCY = 'dependency'  # External dependency
    FILE = 'file'  # File in repository
    PROJECT = 'project'
    CLASS = 'class'
    LOCAL = 'local'
    GLOBAL = 'global'


class RelationshipType(str, Enum):
    UTILIZES = 'utilizes'
    USES = 'uses'
    DEFINED_BY = 'defined_by'
    IS_A = 'is_a'
    PROVIDES = 'provides'
    IMPORTS = 'imports'
    CALLS = 'calls'
    DEPENDENCY = 'dependency'
    TYPE = 'type'


class Relationship(BaseModel):
    scope: ReferenceScope = Field(description='The scope of the reference.')
    identifier: Optional[str] = Field(default=None, description='ID')
    type: RelationshipType = Field(
        default=RelationshipType.USES, description='The type of the reference.'
    )
    external_path: List[str] = Field(
        default=[], description='The path to the referenced parent code block.'
    )
    resolved_path: List[str] = Field(
        default=[], description='The path to the file with the referenced code block.'
    )
    path: List[str] = Field(
        default=[], description='The path to the referenced code block.'
    )

    @root_validator(pre=True)
    def validate_path(cls, values):
        external_path = values.get('external_path')
        path = values.get('path')
        if not external_path and not path:
            raise ValueError('Cannot create Reference without external_path or path.')
        return values

    def __hash__(self):
        return hash((self.scope, tuple(self.path)))

    def __eq__(self, other):
        return (self.scope, self.path) == (other.scope, other.path)

    def full_path(self):
        return self.external_path + self.path

    def __str__(self):
        if self.identifier:
            start_node = self.identifier
        else:
            start_node = ''

        end_node = ''
        if self.external_path:
            end_node = '/'.join(self.external_path)
        if self.path:
            if self.external_path:
                end_node += '/'
            end_node += '.'.join(self.path)

        return f'({start_node})-[:{self.type.name} {{scope: {self.scope.value}}}]->({end_node})'


class Parameter(BaseModel):
    identifier: str = Field(description='The identifier of the parameter.')
    type: Optional[str] = Field(description='The type of the parameter.')


class SpanType(str, Enum):
    INITATION = 'init'
    DOCUMENTATION = 'docs'
    IMPLEMENTATION = 'impl'


class BlockSpan(BaseModel):
    span_id: str = Field()
    span_type: SpanType = Field(description='Type of span.')
    start_line: int = Field(description='Start line of the span.')
    end_line: int = Field(description='End line of the span.')

    initiating_block: 'CodeBlock' = Field(
        default=None,
        description='The block that initiated the span.',
    )

    @property
    def block_type(self):
        return self.initiating_block.type

    # TODO: Remove
    visible: bool = Field(default=True, description='If the span should be visible.')

    index: int = 0

    parent_block_path: BlockPath = Field(
        default=None,
        description='Path to the parent block of the span.',
    )

    is_partial: bool = Field(
        default=False,
        description='If the span is covering a partial part of the parent block.',
    )

    block_paths: List[BlockPath] = Field(
        default=[],
        description='Block paths that should be shown when the span is shown.',
    )

    tokens: int = Field(default=0, description='Number of tokens in the span.')

    def __str__(self):
        return f'{self.span_id} ({self.span_type.value}, {self.tokens} tokens)'

    def get_first_child_block_path(self):
        for block_path in self.block_paths:
            if len(block_path) == len(self.parent_block_path):
                continue
            return block_path


class ValidationError(BaseModel):
    error: str


class CodeBlock(BaseModel):
    content: str
    type: CodeBlockType
    identifier: Optional[str] = None
    parameters: List[Parameter] = []  # TODO: Move to Function sub class
    relationships: List[Relationship] = []
    span_ids: Set[str] = set()
    belongs_to_span: Optional[BlockSpan] = None
    content_lines: List[str] = []
    start_line: int = 0
    end_line: int = 0
    properties: dict = {}
    pre_code: str = ''
    pre_lines: int = 0
    indentation: str = ''
    tokens: int = 0
    children: List['CodeBlock'] = []
    validation_errors: List[ValidationError] = []
    parent: Optional['CodeBlock'] = None
    previous: Optional['CodeBlock'] = None
    next: Optional['CodeBlock'] = None

    @validator('type', pre=True, always=True)
    def validate_type(cls, v):
        if v is None:
            raise ValueError('Cannot create CodeBlock without type.')
        return v

    def __init__(self, **data):
        super().__init__(**data)
        for child in self.children:
            child.parent = self

        if self.pre_code and not re.match(r'^[ \n\\]*$', self.pre_code):
            raise ValueError(
                f'Failed to parse code block with type {self.type} and content `{self.content}`. '
                f'Expected pre_code to only contain spaces and line breaks. Got `{self.pre_code}`'
            )

        if self.pre_code and not self.indentation and not self.pre_lines:
            pre_code_lines = self.pre_code.split('\n')
            self.pre_lines = len(pre_code_lines) - 1
            if self.pre_lines > 0:
                self.indentation = pre_code_lines[-1]
            else:
                self.indentation = self.pre_code

        self.content_lines = self.content.split('\n')
        # if self.indentation and self.pre_lines:
        #    self.content_lines[1:] = [line[len(self.indentation):] for line in self.content_lines[1:]]

    def last(self):
        if self.next:
            return self.next.last()
        return self

    def insert_child(self, index: int, child: 'CodeBlock'):
        if index == 0 and self.children[0].pre_lines == 0:
            self.children[0].pre_lines = 1

        self.children.insert(index, child)
        child.parent = self

    def insert_children(self, index: int, children: List['CodeBlock']):
        for child in children:
            self.insert_child(index, child)
            index += 1

    def append_child(self, child: 'CodeBlock'):
        self.children.append(child)
        self.span_ids.update(child.span_ids)
        child.parent = self

    def append_children(self, children: List['CodeBlock']):
        for child in children:
            self.append_child(child)

    def replace_children(
        self, start_index: int, end_index: int, children: List['CodeBlock']
    ):
        self.children = (
            self.children[:start_index] + children + self.children[end_index:]
        )
        for child in children:
            child.parent = self

    def replace_child(self, index: int, child: 'CodeBlock'):
        # TODO: Do a proper update of everything when replacing child blocks
        child.pre_code = self.children[index].pre_code
        child.pre_lines = self.children[index].pre_lines
        self.sync_indentation(self.children[index], child)

        self.children[index] = child
        child.parent = self

    def remove_child(self, index: int):
        del self.children[index]

    def sync_indentation(self, original_block: 'CodeBlock', updated_block: 'CodeBlock'):
        original_indentation_length = len(original_block.indentation) + len(
            self.indentation
        )
        if not updated_block.parent:
            return
        updated_indentation_length = len(updated_block.indentation) + len(
            updated_block.parent.indentation
        )

        # To handle separate code blocks provdided out of context
        if (
            original_indentation_length == updated_indentation_length
            and len(updated_block.indentation) == 0
        ):
            updated_block.indentation = ' ' * original_indentation_length

        elif original_indentation_length > updated_indentation_length:
            additional_indentation = ' ' * (
                original_indentation_length - updated_indentation_length
            )
            updated_block.add_indentation(additional_indentation)

    def replace_by_path(self, path: List[str], new_block: 'CodeBlock'):
        if not path:
            return

        for i, child in enumerate(self.children):
            if child.identifier == path[0]:
                if len(path) == 1:
                    self.replace_child(i, new_block)
                    return
                else:
                    child.replace_by_path(path[1:], new_block)

    def __str__(self):
        return self.to_string()

    def to_string(self):
        return self._to_string()

    def sum_tokens(self):
        tokens = self.tokens
        tokens += sum([child.sum_tokens() for child in self.children])
        return tokens

    def get_all_child_blocks(self) -> List['CodeBlock']:
        blocks = []
        for child in self.children:
            blocks.append(child)
            blocks.extend(child.get_all_child_blocks())
        return blocks

    def get_children(
        self, exclude_blocks: Optional[List[CodeBlockType]] = None
    ) -> List['CodeBlock']:
        if exclude_blocks is None:
            exclude_blocks = []
        return [child for child in self.children if child.type not in exclude_blocks]

    def show_related_spans(
        self,
        span_id: Optional[str] = None,  # TODO: Set max tokens to show
    ):
        related_spans = self.find_related_spans(span_id)
        for span in related_spans:
            span.visible = True

    def has_visible_children(self):
        for child in self.children:
            if child.is_visible:
                return True

            if child.has_visible_children():
                return True

        return False

    @property
    def is_visible(self):
        return self.belongs_to_span and self.belongs_to_span.visible

    def _to_string(self) -> str:
        contents = ''

        if self.pre_lines:
            contents += '\n' * (self.pre_lines - 1)
            for i, line in enumerate(self.content_lines):
                if i == 0 and line:
                    contents += '\n' + self.indentation + line
                elif line:
                    contents += '\n' + line
                else:
                    contents += '\n'
        else:
            contents += self.pre_code + self.content

        for i, child in enumerate(self.children):
            contents += child._to_string()

        return contents

    def _build_path_tree(
        self, block_paths: List[str], include_references: bool = False
    ):
        path_tree = PathTree()

        for block_path in block_paths:
            if block_path:
                path = block_path.split('.')
                if include_references:
                    block = self.find_by_path(path)
                    if block:
                        if self.type == CodeBlockType.CLASS:
                            references = [
                                self._fix_reference_path(reference)
                                for reference in self.get_all_relationships(
                                    exclude_types=[
                                        CodeBlockType.FUNCTION,
                                        CodeBlockType.TEST_CASE,
                                    ]
                                )
                                if reference
                                and reference.scope != ReferenceScope.EXTERNAL
                            ]  # FIXME skip _fix_reference_path?
                        else:
                            references = [
                                self._fix_reference_path(reference)
                                for reference in self.get_all_relationships()
                                if reference
                                and reference.scope != ReferenceScope.EXTERNAL
                            ]  # FIXME skip _fix_reference_path?

                        for ref in references:
                            path_tree.add_to_tree(ref.path)

                path_tree.add_to_tree(path)
            elif block_path == '':
                path_tree.show = True

        return path_tree

    def _to_prompt_string(
        self,
        show_span_id: bool = False,
        span_marker: SpanMarker = SpanMarker.COMMENT,
        show_line_numbers: bool = False,
    ) -> str:
        contents = ''

        if show_span_id and self.belongs_to_span:
            contents += '\n\n'
            if span_marker == SpanMarker.COMMENT:
                span_comment = self.create_comment(
                    f'span_id: {self.belongs_to_span.span_id}'
                )
                contents += f'{self.indentation}{span_comment}'
            elif span_marker == SpanMarker.TAG:
                contents += f"\n<span id='{self.belongs_to_span.span_id}'>"

            if not self.pre_lines:
                contents += '\n'

        def print_line(line_number):
            if not show_line_numbers:
                return ''
            return str(line_number).ljust(6)

        # Just to write out the first line number when there are no pre_lines on first block
        if (
            self.parent
            and self.parent.type == CodeBlockType.MODULE
            and self.parent.children[0] == self
        ):
            contents += print_line(self.start_line)

        if self.pre_lines:
            for i in range(self.pre_lines):
                contents += '\n'
                contents += print_line(self.start_line - self.pre_lines + i + 1)

        contents += self.indentation + self.content_lines[0]

        for i, line in enumerate(self.content_lines[1:]):
            contents += '\n'
            contents += print_line(self.start_line + i + 1)
            contents += line

        return contents

    def to_prompt(
        self,
        span_ids: Optional[Set[str]] = None,
        start_line: Optional[int] = None,
        end_line: Optional[int] = None,
        show_outcommented_code: bool = True,
        outcomment_code_comment: str = '...',
        show_span_id: bool = False,
        current_span_id: Optional[str] = None,
        show_line_numbers: bool = False,
        exclude_block_types: Optional[List[CodeBlockType]] = None,
        include_block_types: Optional[List[CodeBlockType]] = None,
    ):
        contents = ''

        has_outcommented_code = False
        for i, child in enumerate(self.children):
            show_child = True

            if exclude_block_types and child.type in exclude_block_types:
                show_child = False

            if show_child and span_ids:
                show_child = child.has_any_span(span_ids)

            if show_child and include_block_types:
                show_child = child.has_blocks_with_types(include_block_types)

            if show_child and start_line and end_line:
                show_child = child.has_lines(
                    start_line, end_line
                ) or child.is_within_lines(start_line, end_line)

            if show_child:
                if has_outcommented_code:
                    contents += child.create_commented_out_block(
                        outcomment_code_comment
                    ).to_string()

                has_outcommented_code = False

                show_new_span_id = (
                    show_span_id
                    and child.belongs_to_span
                    and (
                        not current_span_id
                        or current_span_id != child.belongs_to_span.span_id
                    )
                )
                if child.belongs_to_span:
                    current_span_id = child.belongs_to_span.span_id

                show_span_id_ = (
                    bool(show_new_span_id) if show_new_span_id is not None else False
                )
                contents += child._to_prompt_string(
                    show_span_id=show_span_id_, show_line_numbers=show_line_numbers
                )
                contents += child.to_prompt(
                    span_ids=span_ids,
                    start_line=start_line,
                    end_line=end_line,
                    show_outcommented_code=show_outcommented_code,
                    outcomment_code_comment=outcomment_code_comment,
                    show_span_id=show_span_id,
                    current_span_id=current_span_id,
                    show_line_numbers=show_line_numbers,
                    exclude_block_types=exclude_block_types,
                    include_block_types=include_block_types,
                )
            elif show_outcommented_code and child.type not in [
                CodeBlockType.COMMENT,
                CodeBlockType.COMMENTED_OUT_CODE,
            ]:
                has_outcommented_code = True

        if (
            outcomment_code_comment
            and has_outcommented_code
            and child.type
            not in [
                CodeBlockType.COMMENT,
                CodeBlockType.COMMENTED_OUT_CODE,
                CodeBlockType.SPACE,
            ]
        ):
            contents += '\n.    ' if show_line_numbers else '\n'
            contents += child.create_commented_out_block(
                outcomment_code_comment
            ).to_string()
            contents += '\n'

        return contents

    def __eq__(self, other):
        if not isinstance(other, CodeBlock):
            return False

        return self.full_path() == other.full_path()

    def find_block_by_type(self, block_type: CodeBlockType) -> Optional['CodeBlock']:
        if self.type == block_type:
            return self

        for child in self.children:
            block = child.find_block_by_type(block_type)
            if block:
                return block

        return None

    def find_type_in_parents(self, block_type: CodeBlockType) -> Optional['CodeBlock']:
        if not self.parent:
            return None

        if self.parent.type == block_type:
            return self.parent

        if self.parent:
            return self.parent.find_type_in_parents(block_type)

        return None

    def structure_block(self):
        if self.type.group == CodeBlockTypeGroup.STRUCTURE:
            return self

        if self.parent:
            return self.parent.structure_block()

        return None

    def find_type_group_in_parents(
        self, block_type_group: CodeBlockTypeGroup
    ) -> Optional['CodeBlock']:
        if not self.parent:
            return None

        if self.parent.type.group == block_type_group:
            return self.parent

        if self.parent:
            return self.parent.find_type_group_in_parents(block_type_group)

        return None

    def find_spans_by_line_numbers(
        self, start_line: int, end_line: Optional[int] = None
    ) -> List[BlockSpan]:
        spans: List[BlockSpan] = []  # spans = []
        for child in self.children:
            if end_line is None:
                end_line = start_line

            if child.end_line < start_line:
                continue

            if child.start_line > end_line:
                return spans

            if child.belongs_to_span and child.belongs_to_span.span_id not in spans:
                if (
                    not child.children
                    or child.start_line >= start_line
                    and child.end_line <= end_line
                    or child.start_line == start_line
                    or child.end_line == end_line
                ):
                    spans.append(child.belongs_to_span)

            child_spans = child.find_spans_by_line_numbers(start_line, end_line)
            for span in child_spans:
                if span not in spans:
                    spans.append(span)

        return spans

    def dict(self, **kwargs):
        # TODO: Add **kwargs to dict call
        return super().dict(exclude={'parent', 'merge_history'})

    def path_string(self):
        return '.'.join(self.full_path())

    def full_path(self):
        path = []
        if self.parent:
            path.extend(self.parent.full_path())

        if self.identifier:
            path.append(self.identifier)

        return path

    @property
    def module(self):  # -> 'Module'
        if self.parent:
            return self.parent.root()
        return self

    @deprecated('Use module()')
    def root(self):  # -> 'Module'
        return self.module

    def get_blocks(
        self, has_identifier: bool, include_types: Optional[List[CodeBlockType]] = None
    ) -> List['CodeBlock']:
        blocks = [self]

        for child in self.children:
            if has_identifier and not child.identifier:
                continue

            if include_types and child.type not in include_types:
                continue

            blocks.extend(child.get_indexable_blocks())
        return blocks

    def find_reference(self, ref_path: List[str]) -> Optional[Relationship]:
        for child in self.children:
            if child.type == CodeBlockType.IMPORT:
                for reference in child.relationships:
                    if (
                        reference.path[len(reference.path) - len(ref_path) :]
                        == ref_path
                    ):
                        return reference

            child_path = child.full_path()

            if child_path[len(child_path) - len(ref_path) :] == ref_path:
                if self.type == CodeBlockType.CLASS:
                    return Relationship(scope=ReferenceScope.CLASS, path=child_path)
                if self.type == CodeBlockType.MODULE:
                    return Relationship(scope=ReferenceScope.GLOBAL, path=child_path)

                return Relationship(scope=ReferenceScope.LOCAL, path=child_path)

        if self.parent:
            reference_result = self.parent.find_reference(ref_path)
            if reference_result:
                return reference_result

        return None

    def get_all_relationships(
        self, exclude_types: Optional[List[CodeBlockType]] = None
    ) -> List[Relationship]:
        if exclude_types is None:
            exclude_types = []
        references = []
        references.extend(self.relationships)
        for childblock in self.children:
            if not exclude_types or childblock.type not in exclude_types:
                references.extend(
                    childblock.get_all_relationships(exclude_types=exclude_types)
                )

        return references

    def is_complete(self):
        if self.type == CodeBlockType.COMMENTED_OUT_CODE:
            return False
        for child in self.children:
            if not child.is_complete():
                return False
        return True

    def find_errors(self) -> List['CodeBlock']:
        errors = []

        if self.children:
            for child in self.children:
                errors.extend(child.find_errors())

        if self.type == CodeBlockType.ERROR:
            errors.append(self)

        return errors

    def find_validation_errors(self) -> List[ValidationError]:
        errors = []
        errors.extend(self.validation_errors)

        for child in self.children:
            errors.extend(child.find_validation_errors())

        return errors

    def create_commented_out_block(self, comment_out_str: str = '...'):
        return CodeBlock(
            type=CodeBlockType.COMMENTED_OUT_CODE,
            indentation=self.indentation,
            parent=self,
            pre_lines=1,
            content=self.create_comment(comment_out_str),
        )

    def create_comment_block(self, comment: str = '...', pre_lines: int = 1):
        return CodeBlock(
            type=CodeBlockType.COMMENT,
            indentation=self.indentation,
            parent=self,
            pre_lines=pre_lines,
            content=self.create_comment(comment),
        )

    def create_comment(self, comment: str) -> str:
        symbol = get_comment_symbol('python')  # FIXME: Derive language from Module
        return f'{symbol} {comment}'

    def add_indentation(self, indentation: str):
        if self.pre_lines:
            self.indentation += indentation

        # TODO: Find a more graceful way to solve multi line blocks
        if '\n' in self.content:
            lines = self.content.split('\n')
            content = lines[0]
            for line in lines[1:]:
                if line.startswith(' '):
                    content += '\n' + indentation + line
            self.content = content

        for child in self.children:
            child.add_indentation(indentation)

    def find_by_path(self, path: Optional[List[str]] = None) -> Optional['CodeBlock']:
        if path is None:
            return None

        if not path:
            return self

        for child in self.children:
            if child.identifier == path[0]:
                if len(path) == 1:
                    return child
                else:
                    return child.find_by_path(path[1:])

        return None

    def find_blocks_by_span_id(self, span_id: str) -> List['CodeBlock']:
        blocks = []
        if self.belongs_to_span and self.belongs_to_span.span_id == span_id:
            blocks.append(self)

        for child in self.children:
            # TODO: Optimize to just check relevant children (by mapping spans?
            blocks.extend(child.find_blocks_by_span_id(span_id))

        return blocks

    def find_last_before_span(
        self, span_id: str, last_before_span: Optional['CodeBlock'] = None
    ) -> Optional['CodeBlock']:
        if self.belongs_to_span and self.belongs_to_span.span_id == span_id:
            return last_before_span

        for child in self.children:
            if child.belongs_to_span and child.belongs_to_span.span_id == span_id:
                return last_before_span

            if child.belongs_to_span and child.belongs_to_span.span_id != span_id:
                last_before_span = child

            result = child.find_last_before_span(span_id, last_before_span)
            if result:
                return result

        return None

    def find_first_by_span_id(self, span_id: str) -> Optional['CodeBlock']:
        if self.belongs_to_span and self.belongs_to_span.span_id == span_id:
            return self

        for child in self.children:
            found = child.find_first_by_span_id(span_id)
            if found:
                return found

        return None

    def find_last_by_span_id(self, span_id: str) -> Optional['CodeBlock']:
        for child in reversed(self.children):
            if child.belongs_to_span and child.belongs_to_span.span_id == span_id:
                return child

            found = child.find_last_by_span_id(span_id)
            if found:
                return found

        return None

    def has_any_block(self, blocks: List['CodeBlock']) -> bool:
        for block in blocks:
            if block.full_path()[: len(self.full_path())] == self.full_path():
                return True
        return False

    def find_by_identifier(
        self,
        identifier: str,
        type: Optional[CodeBlockType] = None,
        recursive: bool = False,
    ):
        for child in self.children:
            if child.identifier == identifier and (not type or child.type == type):
                return child

            if recursive:
                found = child.find_by_identifier(identifier, type, recursive)
                if found:
                    return found
        return None

    def find_blocks_with_identifier(self, identifier: str) -> List['CodeBlock']:
        blocks = []
        for child_block in self.children:
            if child_block.identifier == identifier:
                blocks.append(child_block)
            blocks.extend(child_block.find_blocks_with_identifier(identifier))
        return blocks

    def find_incomplete_blocks_with_type(self, block_type: CodeBlockType):
        return self.find_incomplete_blocks_with_types([block_type])

    def find_incomplete_blocks_with_types(self, block_types: List[CodeBlockType]):
        matching_blocks = []
        for child_block in self.children:
            if child_block.type in block_types and not child_block.is_complete():
                matching_blocks.append(child_block)

            if child_block.children:
                matching_blocks.extend(
                    child_block.find_incomplete_blocks_with_types(block_types)
                )

        return matching_blocks

    def find_blocks_with_types(
        self, block_types: List[CodeBlockType]
    ) -> List['CodeBlock']:
        matching_blocks = []
        if self.type in block_types:
            matching_blocks.append(self)
        for child_block in self.children:
            matching_blocks.extend(
                child_block.find_blocks_with_types(block_types=block_types)
            )
        return matching_blocks

    def has_blocks_with_types(self, block_types: List[CodeBlockType]) -> bool:
        if self.type in block_types:
            return True
        for child_block in self.children:
            if child_block.has_blocks_with_types(block_types):
                return True
        return False

    def find_blocks_with_type(self, block_type: CodeBlockType) -> List['CodeBlock']:
        return self.find_blocks_with_types([block_type])

    def find_first_by_start_line(self, start_line: int) -> Optional['CodeBlock']:
        for child in self.children:
            if child.start_line >= start_line:
                return child

            if child.end_line >= start_line:
                if not child.children:
                    return child

                found = child.find_first_by_start_line(start_line)
                if found:
                    return found

        return None

    def find_last_by_end_line(
        self, end_line: int, tokens: Optional[int] = None
    ) -> Optional['CodeBlock']:
        last_child = None
        for child in self.children:
            if child.start_line > end_line or (tokens and child.tokens > tokens):
                return last_child

            if tokens:
                tokens -= child.tokens

            last_child = child

            if child.end_line > end_line:
                found = child.find_last_by_end_line(end_line, tokens=tokens)
                if found:
                    return found

        return None

    def find_closest_indexed_parent(self) -> Optional['CodeBlock']:
        if self.is_indexed:
            return self

        if self.parent:
            return self.parent.find_closest_indexed_parent()

        return None

    def find_indexed_blocks(self):
        indexed_blocks = []
        for child in self.children:
            if child.is_indexed:
                indexed_blocks.append(child)
            indexed_blocks.extend(child.find_indexed_blocks())
        return indexed_blocks

    def get_indexed_blocks(self) -> List['CodeBlock']:
        blocks = []
        for child in self.children:
            if child.is_indexed:
                blocks.append(child)

            blocks.extend(child.get_indexed_blocks())

        return blocks

    def line_witin_token_context(self, line_number: int, tokens: int) -> bool:
        if tokens <= 0:
            return False

        if self.end_line < line_number:
            if not self.next:
                return False
            if self.next.start_line > line_number:
                return True
            else:
                return self.next.line_witin_token_context(
                    line_number, tokens - self.tokens
                )
        else:
            if not self.previous:
                return False
            elif self.previous.end_line < line_number:
                return True
            else:
                return self.previous.line_witin_token_context(
                    line_number, tokens - self.tokens
                )

    def tokens_from_line(self, line_number: int) -> Optional[int]:
        if not self.previous or self.previous.end_line < line_number:
            return self.tokens

        previous_tokens = self.previous.tokens_from_line(line_number)
        if previous_tokens is None:
            previous_tokens = 0
        return self.tokens + previous_tokens

    def last_block_until_line(self, line_number: int, tokens: int) -> 'CodeBlock':
        if self.end_line < line_number:
            if (
                not self.next
                or self.next.start_line > line_number
                or self.next.tokens > tokens
            ):
                return self
            else:
                return self.next.last_block_until_line(
                    line_number, tokens - self.tokens
                )
        else:
            if (
                not self.previous
                or self.previous.end_line < line_number
                or (self.next and self.next.tokens > tokens)
            ):
                return self
            else:
                return self.previous.last_block_until_line(
                    line_number, tokens - self.tokens
                )

    def get_all_span_ids(self, include_self: bool = True) -> Set[str]:
        span_ids = set()

        if include_self and self.belongs_to_span:
            span_ids.add(self.belongs_to_span.span_id)

        for child in self.children:
            span_ids.update(child.get_all_span_ids())

        return span_ids

    def has_span(self, span_id: str):
        return self.has_any_span({span_id})

    def has_any_span(self, span_ids: Optional[Set[str]]):
        all_span_ids = self.get_all_span_ids(include_self=False)
        return (
            any([span_id in all_span_ids for span_id in span_ids])
            if span_ids
            else False
        )

    def belongs_to_any_span(self, span_ids: Set[str]):
        return self.belongs_to_span and self.belongs_to_span.span_id in span_ids

    def has_lines(self, start_line: int, end_line: int):
        # Returns True if any part of the block is within the provided line range
        return not (self.end_line < start_line or self.start_line > end_line)

    def is_within_lines(self, start_line: int, end_line: int):
        return self.start_line >= start_line and self.end_line <= end_line

    def has_content(self, query: str, span_id: Optional[str] = None):
        if (
            self.content
            and query in self.content
            and (
                not span_id
                or (self.belongs_to_span and self.belongs_to_span.span_id == span_id)
            )
        ):
            return True

        if span_id and not self.has_span(span_id):
            return False

        for child in self.children:
            if child.has_content(query, span_id):
                return True

        return False
