import ctypes
import logging
import os
import pyGHDL.libghdl.name_table as name_table
import pyGHDL.libghdl.files_map as files_map
import pyGHDL.libghdl.files_map_editor as files_map_editor
import pyGHDL.libghdl.libraries as libraries
import pyGHDL.libghdl.vhdl.nodes as nodes
import pyGHDL.libghdl.vhdl.sem_lib as sem_lib
import pyGHDL.libghdl.vhdl.sem as sem
import pyGHDL.libghdl.vhdl.formatters as formatters
import pyGHDL.libghdl.vhdl.prints as prints
import pyGHDL.libghdl.file_comments as file_comments

from . import symbols, references

log = logging.getLogger(__name__)


class Document(object):
    # The encoding used for the files.
    # Unfortunately this is not fully reliable.  The client can read the
    # file using its own view of the encoding.  It then pass the document
    # to the server using unicode(utf-8).  Then the document is converted
    # back to bytes using this encoding.  And we hope the result would be
    # the same as the file.  Because VHDL uses the iso 8859-1 character
    # set, we use the same encoding.  The client should also use 8859-1.
    encoding = "iso-8859-1"

    initial_gap_size = 4096

    def __init__(self, uri, sfe=None, lib=None, version=None):
        self.uri = uri
        self.version = version
        self._fe = sfe
        self.library = lib
        self.gap_size = Document.initial_gap_size
        self._tree = nodes.Null_Iir

    @staticmethod
    def load(src_bytes, dirname, filename):
        # Write text to file buffer.
        src_len = len(src_bytes)
        buf_len = src_len + Document.initial_gap_size
        fileid = name_table.Get_Identifier(filename)
        if os.path.isabs(filename):
            dirid = name_table.Null_Identifier
        else:
            dirid = name_table.Get_Identifier(dirname)
        sfe = files_map.Reserve_Source_File(dirid, fileid, buf_len)
        files_map_editor.Fill_Text(sfe, ctypes.c_char_p(src_bytes), src_len)
        return sfe

    def __extend_source_buffer(self, new_size):
        self.gap_size *= 2
        fileid = files_map.Get_File_Name(self._fe)
        dirid = files_map.Get_Directory_Name(self._fe)
        buf_len = files_map.Get_File_Length(self._fe) + new_size + self.gap_size
        files_map.Discard_Source_File(self._fe)
        new_sfe = files_map.Reserve_Source_File(dirid, fileid, buf_len)
        files_map_editor.Copy_Source_File(new_sfe, self._fe)
        files_map.Free_Source_File(self._fe)
        self._fe = new_sfe

    def reload(self, source):
        """Reload the source of a document."""
        src_bytes = source.encode(Document.encoding, "replace")
        l = len(src_bytes)
        if l >= files_map.Get_Buffer_Length(self._fe):
            self.__extend_source_buffer(l)
        files_map_editor.Fill_Text(self._fe, ctypes.c_char_p(src_bytes), l)

    def __str__(self):
        return str(self.uri)

    def apply_change(self, change):
        """Apply a change to the document."""
        text = change["text"]
        change_range = change.get("range")

        text_bytes = text.encode(Document.encoding, "replace")

        if not change_range:
            # The whole file has changed
            raise AssertionError
            # if len(text_bytes) < libghdl.Files_Map.Get_Buffer_Length(self._fe):
            #    xxxx_replace
            # else:
            #    xxxx_free
            #    xxxx_allocate
            # return

        start_line = change_range["start"]["line"]
        start_col = change_range["start"]["character"]
        end_line = change_range["end"]["line"]
        end_col = change_range["end"]["character"]

        status = files_map_editor._Replace_Text(
            self._fe,
            start_line + 1,
            start_col,
            end_line + 1,
            end_col,
            ctypes.c_char_p(text_bytes),
            len(text_bytes),
        )
        if status:
            return

        # Failed to replace text.
        # Increase size
        self.__extend_source_buffer(len(text_bytes))
        status = files_map_editor._Replace_Text(
            self._fe,
            start_line + 1,
            start_col,
            end_line + 1,
            end_col,
            ctypes.c_char_p(text_bytes),
            len(text_bytes),
        )
        assert status

    def check_document(self, text):
        log.debug("Checking document: %s", self.uri)

        text_bytes = text.encode(Document.encoding, "replace")

        files_map_editor.Check_Buffer_Content(self._fe, ctypes.c_char_p(text_bytes), len(text_bytes))

    @staticmethod
    def add_to_library(tree, library):
        # Set the target library
        if library is None:
            library = "work"
        libraries.Work_Library_Name.value = name_table.Get_Identifier(library)
        libraries.Load_Work_Library(False)
        # Detach the chain of units.
        unit = nodes.Get_First_Design_Unit(tree)
        nodes.Set_First_Design_Unit(tree, nodes.Null_Iir)
        # FIXME: free the design file ?
        tree = nodes.Null_Iir
        # Analyze unit after unit.
        while unit != nodes.Null_Iir:
            # Pop the first unit.
            next_unit = nodes.Get_Chain(unit)
            nodes.Set_Chain(unit, nodes.Null_Iir)
            lib_unit = nodes.Get_Library_Unit(unit)
            if lib_unit != nodes.Null_Iir and nodes.Get_Identifier(unit) != name_table.Null_Identifier:
                # Put the unit (only if it has a library unit) in the library.
                libraries.Add_Design_Unit_Into_Library(unit, False)
                tree = nodes.Get_Design_File(unit)
            unit = next_unit
        return tree

    def parse_document(self):
        """Parse a document and put the units in the library."""
        assert self._tree == nodes.Null_Iir
        tree = sem_lib.Load_File(self._fe)
        if tree == nodes.Null_Iir:
            return
        self._tree = Document.add_to_library(tree, self.library)
        log.debug("add_to_library(%u, '%s') -> %u", tree, self.library, self._tree)
        if self._tree == nodes.Null_Iir:
            return
        nodes.Set_Design_File_Source(self._tree, self._fe)

    def compute_diags(self):
        log.debug("parse doc %d %s", self._fe, self.uri)
        self.parse_document()
        if self._tree == nodes.Null_Iir:
            # No units, nothing to add.
            return
        # Semantic analysis.
        unit = nodes.Get_First_Design_Unit(self._tree)
        while unit != nodes.Null_Iir:
            sem.Semantic(unit)
            nodes.Set_Date_State(unit, nodes.DateStateType.Analyze)
            unit = nodes.Get_Chain(unit)

    def flatten_symbols(self, syms, parent):
        res = []
        for s in syms:
            s["location"] = {"uri": self.uri, "range": s["range"]}
            del s["range"]
            s.pop("detail", None)
            if parent is not None:
                s["containerName"] = parent
            res.append(s)
            children = s.pop("children", None)
            if children is not None:
                res.extend(self.flatten_symbols(children, s))
        return res

    def document_symbols(self):
        log.debug("document_symbols")
        if self._tree == nodes.Null_Iir:
            return []
        syms = symbols.get_symbols_chain(self._fe, nodes.Get_First_Design_Unit(self._tree))
        return self.flatten_symbols(syms, None)

    def position_to_location(self, position):
        pos = files_map.File_Line_To_Position(self._fe, position["line"] + 1)
        return files_map.File_Pos_To_Location(self._fe, pos) + position["character"]

    def find_definition(self, position):
        loc = self.position_to_location(position)
        return references.find_definition_by_loc(self._tree, loc)

    def hover(self, position):
        loc = self.position_to_location(position)
        t = references.find_definition_by_loc(self._tree, loc)
        if t is None:
            # At least vscode sends an hover request even on spaces.
            log.debug("hover: definition not found at {}.{}".format(position["line"], position["character"]))
            return None

        # Regenerate the declaration
        hand = prints.Allocate_Handle()
        prints.Print_String(t, hand)
        buffer = prints.Get_C_String(hand)
        buf_len = prints.Get_Length(hand)
        if buf_len == 0:
            # Not expected.
            log.info("hover: no string")
            res = None
        else:
            txt = ""
            # Extract comments
            t_loc = nodes.Get_Location(t)
            t_fe = files_map.Location_To_File(t_loc)
            comm = file_comments.Find_First_Comment(t_fe, t)
            while comm != file_comments.No_Comment_Index:
                # Add a comment in 'preformatted' mode
                txt += "    " + file_comments.Get_Comment(t_fe, comm) + "\n"
                comm = file_comments.Get_Next_Comment(t_fe, comm)
            if txt:
                # Add a separation line between comments and declaration.
                txt += "---\n"
            newtext = buffer[:buf_len].decode(Document.encoding)
            txt += "```vhdl\n" + newtext + "\n```"
            res = {"contents": {"kind": "markdown", "value": txt}}
        prints.Free_Handle(hand)
        return res

    def format_range(self, rng):
        first_line = rng["start"]["line"] + 1
        last_line = rng["end"]["line"] + (1 if rng["end"]["character"] != 0 else 0)
        if last_line < first_line:
            return None
        if self._tree == nodes.Null_Iir:
            return None
        hand = prints.Allocate_Handle()
        formatters.Indent_String(self._tree, hand, first_line, last_line)
        buffer = prints.Get_C_String(hand)
        buf_len = prints.Get_Length(hand)
        newtext = buffer[:buf_len].decode(Document.encoding)
        res = [
            {
                "range": {
                    "start": {"line": first_line - 1, "character": 0},
                    "end": {"line": last_line, "character": 0},
                },
                "newText": newtext,
            }
        ]
        prints.Free_Handle(hand)
        return res
