# type: ignore # noqa: PGH003

"""RecordFlux support for GNAT Studio."""
from __future__ import annotations

import json
import re
from pathlib import Path

import GPS
import highlighter.common as hl
from gi.repository import Gdk, Gtk

try:
    from gs_utils import hook
except ImportError:
    from gps_utils import hook

XML = r"""<?xml version="1.0"?>
<GNAT_Studio>

   <project_attribute
      name="Output_Dir"
      package="RecordFlux"
      editor_page="RecordFlux"
      editor_section="Directories"
      description="Output directory for files generated by RecordFlux">
   </project_attribute>

   <Language>
      <Name>RecordFlux</Name>
      <Spec_Suffix>.rflx</Spec_Suffix>

      <!-- Dummy body suffix as file templates don't work when its missing -->
      <Body_Suffix>.workaround_rflx</Body_Suffix>

      <Categories>
         <Category>
            <Name>package</Name>
            <Pattern>^[ \\t]*package[ \\t]+((\\w|\\.)+)</Pattern>
            <Index>1</Index>
         </Category>
         <Category>
            <Name>type</Name>
               <Pattern>^[ \\t]*type[ \\t]+(\\w+)</Pattern>
               <Index>1</Index>
         </Category>
      </Categories>

      <Context>
         <New_Line_Comment_Start>--</New_Line_Comment_Start>
         <String_Delimiter>&quot;</String_Delimiter>
         <Can_Indent>True</Can_Indent>
         <Syntax_Highlighting>True</Syntax_Highlighting>
         <Case_Sensitive>False</Case_Sensitive>
      </Context>
   </Language>

   <!-- Filter -->
   <filter name="RecordFlux" language="RecordFlux"/>

   <!-- Actions -->
   <action name="rflx_check">
      <filter_and>
         <filter id="RecordFlux"/>
         <filter id="Source editor"/>
      </filter_and>
      <shell lang="python" show-command="false">recordflux.check(&quot;%F&quot;)</shell>
      <external>%1</external>
      <on-failure>
         <shell lang="python" show-command="false">recordflux.parse_output(&quot;&quot;&quot;%1&quot;&quot;&quot;)</shell>
      </on-failure>
   </action>

   <action name="rflx_check_all">
      <filter id="File"/>
      <shell lang="python" show-command="false">recordflux.check_all()</shell>
      <external>%1</external>
      <on-failure>
         <shell lang="python" show-command="false">recordflux.parse_output(&quot;&quot;&quot;%1&quot;&quot;&quot;)</shell>
      </on-failure>
   </action>

   <action name="rflx_generate">
      <filter_and>
         <filter id="RecordFlux"/>
         <filter id="Source editor"/>
      </filter_and>
      <shell lang="python" show-command="false">recordflux.generate(&quot;%F&quot;)</shell>
      <external>%1</external>
      <on-failure>
         <shell lang="python" show-command="false">recordflux.parse_output(&quot;&quot;&quot;%1&quot;&quot;&quot;)</shell>
      </on-failure>
   </action>

   <action name="rflx_generate_all">
      <filter id="File"/>
      <shell lang="python" show-command="false">recordflux.generate_all()</shell>
      <external>%1</external>
      <on-failure>
         <shell lang="python" show-command="false">recordflux.parse_output(&quot;&quot;&quot;%1&quot;&quot;&quot;)</shell>
      </on-failure>
   </action>

   <action name="rflx_display_graph">
      <filter id="RecordFlux"/>
      <shell lang="python" show-command="false">recordflux.graph(&quot;%F&quot;, True)</shell>
      <external>%1</external>
      <on-failure>
         <shell lang="python" show-command="false">recordflux.parse_output(&quot;&quot;&quot;%1&quot;&quot;&quot;)</shell>
      </on-failure>
      <shell lang="python" show-command="false">recordflux.display_message_graph(&quot;%F&quot;)</shell>
   </action>

   <!-- Aliases -->
   <alias name="rflx_package">
      <param name="name" description="The name of the RecordFlux package"/>
      <text>package %(name) is
   %_
end %(name);</text>
   </alias>

   <!-- Submenu -->
   <submenu>
      <title>RecordFlux</title>

      <menu after="SPARK" action="rflx_check">
         <title>Check</title>
      </menu>

      <menu action="rflx_check_all">
         <title>Check All</title>
      </menu>

      <menu action="rflx_generate">
         <title>Generate</title>
      </menu>

      <menu action="rflx_generate_all">
         <title>Generate All</title>
      </menu>
   </submenu>

    <!-- Context menu -->
    <contextual action="rflx_display_graph">
        <Title>RecordFlux/Display message graph</Title>
    </contextual>

</GNAT_Studio>
"""  # noqa: E501

GPS.parse_xml(XML)

# Highlighting

recordflux_types = [
    "Channel",
]

recordflux_attributes = [
    "Append",
    "Extend",
    "First",
    "Has_Data",
    "Last",
    "Present",
    "Read",
    "Reset",
    "Size",
    "Valid",
    "Valid_Checksum",
    "Write",
]

recordflux_attribute_expressions = [
    "Head",
    "Opaque",
]

recordflux_simple_aspects = [
    "Always_Valid",
    "Readable",
    "Writable",
]

recordflux_value_aspects = [
    "Byte_Order",
    "Checksum",
    "Desc",
    "First",
    "Size",
]

recordflux_keywords = [
    "all",
    "and",
    "begin",
    "case",
    "end",
    "exception",
    "for",
    "function",
    "generic",
    "goto",
    "if",
    "in",
    "is",
    "machine",
    "message",
    "mod",
    "new",
    "not",
    "null",
    "of",
    "or",
    "package",
    "range",
    "renames",
    "return",
    "sequence",
    "session",
    "some",
    "state",
    "then",
    "transition",
    "type",
    "unsigned",
    "use",
    "when",
    "where",
    "with",
]

recordflux_literals = [
    "False",
    "High_Order_First",
    "Low_Order_First",
    "True",
]

tag_aspect = hl.existing_style("Src-Editor-Aspects-Variant", "aspects")

hl_type = hl.simple(r"\b[^;\s]+", tag=hl.tag_type)
type_region = hl.region(r":", r"\b", highlighter=(hl_type,), tag=hl.tag_default)
string_literal = hl.region(r'"', r'"', matchall=False, tag=hl.tag_string)

hl.register_highlighter(
    language="recordflux",
    spec=(
        hl.simple(r"--[^\n]*", tag=hl.tag_comment),
        type_region,
        hl.words(recordflux_types, tag=hl.tag_keyword),
        hl.words([rf"\b'{w}" for w in recordflux_attributes], tag=tag_aspect),
        hl.words([rf"\b'{w}" for w in recordflux_attribute_expressions], tag=tag_aspect),
        hl.words(recordflux_simple_aspects, tag=tag_aspect),
        *[hl.simple(rf"\b{w}\s+=>", tag=tag_aspect) for w in recordflux_value_aspects],
        hl.words(recordflux_keywords, tag=hl.tag_keyword),
        hl.words(recordflux_literals, tag=hl.tag_keyword),
        hl.simple(r"16#[_A-Fa-f0-9]+#", tag=hl.tag_number),
        hl.simple(r"\b[_0-9]+\b", tag=hl.tag_number),
        string_literal,
    ),
)


@hook("gps_started")
def __on_gps_started():
    GPS.FileTemplate.register(
        alias_name="rflx_package",
        label="RecordFlux specification",
        unit_param="name",
        language="recordflux",
        is_impl=False,
    )


message_re = re.compile(
    r"^"
    r"(?P<filename>[^:]+):"
    r"(?P<line>\d+):"
    r"(?P<column>\d+): "
    r"(?P<severity>info|warning|error|note|help): "
    r"(?P<message>.*)"
    r"$",
)

generic_message_re = re.compile(
    r"^(?P<severity>info|warning|error|note|help): (?P<message>.*)$",
)


def to_importance(severity):
    assert severity != "info"

    if severity == "error":
        return GPS.Message.Importance.HIGH
    if severity == "warning":
        return GPS.Message.Importance.MEDIUM
    if severity == "help":
        return GPS.Message.Importance.INFORMATIONAL
    if severity == "note":
        return GPS.Message.Importance.LOW


def parse_output(output):
    for m in GPS.Message.list():
        m.remove()
    message = None
    for l in output.splitlines():
        result = message_re.match(l)
        if result:
            data = result.groupdict()

            if data["severity"] != "info":
                message = GPS.Message(
                    category="RecordFlux",
                    file=GPS.File(data["filename"]),
                    line=int(data["line"]),
                    column=int(data["column"]),
                    text="{severity}: {message}".format(**data),
                    importance=to_importance(data["severity"]),
                )
            elif message:
                data["relative"] = Path(data["filename"]).name
                message.create_nested_message(
                    file=GPS.File(data["filename"]),
                    line=int(data["line"]),
                    column=int(data["column"]),
                    text="{message} ({relative}:{line}:{column})".format(**data),
                )
            continue

        result = generic_message_re.match(l)
        if result:
            data = result.groupdict()

            # Severity "info" messages are restricted to logging within RecordFlux. Logs are not
            # diagnostics so we filter them out.
            if data["severity"] == "info":
                continue

            message = GPS.Message(
                category="RecordFlux",
                file=GPS.File(data["severity"]),
                line=1,
                column=1,
                text="{severity}: {message}".format(**data),
                importance=to_importance(data["severity"]),
            )


def get_source_files():
    files = (
        GPS.current_context().project().sources()
        if GPS.current_context().project()
        else GPS.current_context().files()
    )

    if files:
        return [s.name() for s in files if s.language() == "recordflux"]

    raise GPS.Exception("No files found")


def run(files, mode, skip_verification=False, options=None):
    assert mode in ("check", "generate", "graph")
    options = options or []

    GPS.MDI.save_all(force=True)
    GPS.Locations.remove_category("RecordFlux")

    return "rflx --legacy-errors {skip_verification}{mode} {options} {files}".format(
        skip_verification="--unsafe --no-verification " if skip_verification else "",
        mode=mode,
        files=" ".join(files),
        options=" ".join(options),
    )


def check_all(filenames=None):
    filenames = filenames or get_source_files()
    return run(filenames, mode="check")


def check(filename):
    return check_all([filename])


def output_dir():
    output_dir = GPS.Project.root().get_attribute_as_string("Output_Dir", "RecordFlux")
    assert output_dir, "Output directory not configured"
    create_missing = GPS.Project.root().get_attribute_as_string("Create_Missing_Dirs")
    if create_missing and create_missing.lower() == "true":
        output_path = Path(output_dir)
        if output_path.exists():
            assert output_path.is_dir()
        else:
            output_path.mkdir(parents=True)
    return output_dir


def generate_all(filenames=None):
    filenames = filenames or get_source_files()
    return run(
        filenames,
        mode="generate",
        options=["-d", output_dir(), "--ignore-unsupported-checksum"],
    )


def generate(filename):
    return generate_all([filename])


def graph(filename, unverified=False):
    options = ["-d", output_dir()]
    return run([filename], mode="graph", skip_verification=unverified, options=options)


def get_message_name(locations, name, line, column):
    for message, pos in locations[name].items():
        if line > pos["start"]["line"] and line < pos["end"]["line"]:
            return message
        if line == pos["start"]["line"] and column >= pos["start"]["column"]:
            return message
        if line == pos["end"]["line"] and column <= pos["end"]["column"]:
            return message
    return None


def display_message_graph(filename):
    with (Path(output_dir()) / "locations.json").open() as f:
        locations = json.load(f)
    loc = GPS.EditorBuffer.get().current_view().cursor()
    column = loc.column()
    line = loc.line()
    name = GPS.File(filename).name()
    message_name = get_message_name(locations, name, line, column)
    if not message_name:
        GPS.MDI.dialog(
            f"No message found at {name}:{line}:{column}",
            "Error opening graph",
        )
        return

    scrolled_window = Gtk.ScrolledWindow()
    scrolled_window.set_border_width(0)
    scrolled_window.set_policy(Gtk.PolicyType.ALWAYS, Gtk.PolicyType.ALWAYS)
    scrolled_window.override_background_color(Gtk.StateType.NORMAL, Gdk.RGBA())
    graph = Gtk.Image()
    graph.set_from_file(output_dir() + "/" + message_name + ".svg")
    scrolled_window.add_with_viewport(graph)
    GPS.MDI.add(
        scrolled_window,
        "Message graph",
        message_name,
        group=GPS.MDI.GROUP_GRAPHS,
        position=GPS.MDI.POSITION_AUTOMATIC,
    )
