import sys
import logging
import datetime
import textwrap
from itertools import chain
import click
from .. import __version__, PROGRAM, session
from ..env import show_tracebacks
from ..utilities import networkx_layouts, graphviz_layouts, add_linenos, ngettext
from ..utilities.logging import FinesseStreamHandler
from ..script.exceptions import KatScriptError
LOGGER = logging.getLogger(PROGRAM)
[docs]def set_verbosity(ctx, option, value):
    """Callback to set verbosity for root CLI command."""
    # If neither -v nor -q is set, value is 0.
    if not value:
        return
    if option.name == "verbose":
        setter = session.louder
    elif option.name == "quiet":
        setter = session.quieter
    else:
        raise ValueError("Unsupported option.")
    # If -v or -q was specified N times, we want to get increasingly louder/quieter by
    # N times.
    for _ in range(value):
        setter() 
[docs]def set_log_display_level(ctx, option, value):
    """Callback to set log verbosity for root CLI command."""
    if not value:
        return
    state = ctx.ensure_object(KatState)
    state.log_display_level = value 
[docs]def set_debug(ctx, option, value):
    if value is None:
        return
    state = ctx.ensure_object(KatState)
    state.debug = value 
[docs]def set_log_excludes(ctx, option, value):
    if value is None:
        return
    state = ctx.ensure_object(KatState)
    for exclude in value:
        state.exclude(exclude) 
[docs]def print_banner(ctx, option, value):
    """Show the Finesse banner and exit."""
    if not value or ctx.resilient_parsing:  # Keep this check as-is!
        return
    state = ctx.ensure_object(KatState)
    state.print_banner(exit_=True) 
[docs]def parse_path(state, path, legacy=False, **error_kwargs):
    from finesse.script import parse_file, parse_legacy_file
    try:
        if legacy:
            return parse_legacy_file(path)
        else:
            return parse_file(path)
    except Exception as error:
        state.print_error(error, "PARSING ERROR:", **error_kwargs) 
[docs]def plot_graph(state, *args, **kwargs):
    from finesse.plotting.graph import plot_graph as finesse_plot_graph
    finesse_plot_graph(*args, **kwargs) 
[docs]def list_graph_layouts(ctx, option, value):
    """Callback to list available graph layouts."""
    from ..env import has_pygraphviz
    if not value:
        return
    state = ctx.ensure_object(KatState)
    state.print("Available layouts:", fg="green")
    nx_layouts = networkx_layouts()
    gv_layouts = graphviz_layouts()
    layouts = sorted(chain(nx_layouts, gv_layouts))
    gv_suffix = click.style("*", fg="yellow")
    for layout in layouts:
        suffix = gv_suffix if layout in gv_layouts else ""
        state.print(f"- {layout}{suffix}")
    state.print()
    if has_pygraphviz():
        state.print(
            "*This layout can also be plotted directly in graphviz using the "
            "--graphviz flag",
            fg="yellow",
        )
    else:
        state.print("Install pygraphviz/graphviz for more layouts", fg="yellow")
    sys.exit() 
def _fancy_format_kat_error(exception):
    """Format each marked error in red."""
    assert isinstance(exception, KatScriptError)
    error = f"{exception.rubric()}\n"
    errorlines = []
    linenos = []
    errorlinenos = set()
    for lineno, linechunks in exception.chunkify().items():
        line = ""
        for bounds, is_error in linechunks:
            chunktxt = exception.container.script(bounds)
            if is_error:
                errorlinenos.add(lineno)
                if len(chunktxt) == chunktxt.count(" "):
                    # Error chunk is just spaces. Replace with interpunct.
                    chunktxt = click.style("·" * len(chunktxt), fg="red")
                elif chunktxt == "\n":
                    # Error is a newline. Replace with carriage return symbol. Also add
                    # the newline anyway without colouring it red.
                    chunktxt = click.style("↵", fg="red") + chunktxt
                line += click.style(chunktxt, fg="red")
            else:
                line += chunktxt
        # Get rid of trailing newline.
        try:
            line = line.splitlines()[0]
        except IndexError:
            # Line is empty.
            pass
        linenos.append(lineno)
        errorlines.append(line)
    # Add line numbers.
    numberedlines = add_linenos(linenos, errorlines)
    # Add message for missing lines.
    finallines = []
    lastlineno = None
    for lineno, line in zip(linenos, numberedlines):
        if lastlineno:
            diff = lineno - lastlineno
            if diff > 1:
                missingmsg = ngettext(diff - 1, "%d missing line", "%d missing lines")
                finallines.append(f"   *** {missingmsg} ***")
        prefix = "-->" if lineno in errorlinenos else "   "
        finallines.append(prefix + line)
        lastlineno = lineno
    syntax = ""
    if exception.syntax is not None:
        syntax = f"\n\nSyntax: {click.style(exception.syntax, fg='green')}"
    message = error + "\n".join(finallines) + syntax
    return message
input_file_argument = click.argument("input_file", type=click.File("r"))
output_file_argument = click.argument("output_file", type=click.File("w"), default="-")
plot_option = click.option(
    "--plot/--no-plot",
    default=True,
    show_default=True,
    help="Display results as figure (if possible).",
)
trace_option = click.option(
    "--trace/--no-trace",
    default=False,
    show_default=True,
    help="Displays the results of a beam trace of the model.",
)
graphviz_option = click.option(
    "--graphviz",
    is_flag=True,
    default=False,
    show_default=True,
    help="Generate layout and display using Graphviz.",
)
list_graph_layouts_option = click.option(
    "--list-graph-layouts",
    callback=list_graph_layouts,
    is_flag=True,
    default=False,
    expose_value=False,
    is_eager=True,
    help="Show available graph layouts and exit.",
)
graph_layout_argument = click.option(
    "--layout",
    type=str,
    default="neato",
    help="Graph layout algorithm to use.",
)
network_type_argument = click.option(
    "-t",
    "--type",
    "network_type",
    type=click.Choice(("full", "components", "optical")),
    default="full",
    help="Network to plot.",
)
legacy_option = click.option(
    "--legacy",
    is_flag=True,
    default=False,
    show_default=True,
    help="Specify that the input file uses Finesse 2 syntax.",
)
# The current default verbosity is already maximum, so this currently has no effect.
verbose_option = click.option(
    "-v",
    "--verbose",
    count=True,
    callback=set_verbosity,
    expose_value=False,
    is_eager=True,
    help="Increase verbosity of log output (can be specified multiple times).",
)
quiet_option = click.option(
    "-q",
    "--quiet",
    count=True,
    callback=set_verbosity,
    expose_value=False,
    help="Decrease verbosity of log output (can be specified multiple times).",
)
fancy_error_option = click.option(
    "--fancy-errors/--no-fancy-errors",
    is_flag=True,
    default=True,
    callback=set_fancy_error_formatting,
    expose_value=False,
    show_default=True,
    help=(
        "Highlight script error locations in red rather than marking them on the "
        "following line."
    ),
)
log_display_level_option = click.option(
    "--log-level",
    type=click.Choice(("warning", "info", "debug")),
    default="warning",
    show_default=True,
    callback=set_log_display_level,
    expose_value=False,
    help="Set minimum log severity level to display.",
)
log_exclude_option = click.option(
    "--log-exclude",
    multiple=True,
    callback=set_log_excludes,
    expose_value=False,
    help="Ignore log records from a particular logger (wildcards allowed).",
)
debug_option = click.option(
    "--debug",
    is_flag=True,
    default=False,
    callback=set_debug,
    expose_value=False,
    show_default=True,
    help="Enable debug mode (print tracebacks).",
)
[docs]class KatState:
    """Shared state for all CLI subcommands.
    This object gets built by Click when the CLI is called and encapsulates global
    settings for use by individual commands/groups.
    """
    LOG_DISPLAY_DEFAULT_LEVEL = logging.WARNING
[docs]    def __init__(self):
        # Fancy error flag. This is NOT the default value for the CLI - it is set on by
        # default via the @fancy_error_option decorators.
        self.fancy_errors = False
        # Set up the Finesse logger.
        self._log_handler = FinesseStreamHandler()
        self._log_handler.setFormatter(ClickLogColorFormatter())
        self._log_handler.setStream(click.get_text_stream("stderr"))
        LOGGER.addHandler(self._log_handler)
        # Set *default* log display level.
        # NOTE: the user's requested log display level is set by
        # :func:`set_log_display_level`.
        self.log_display_level = self.LOG_DISPLAY_DEFAULT_LEVEL 
    @property
    def log_display_level(self):
        """Log verbosity on stdout."""
        return LOGGER.getEffectiveLevel()
    @log_display_level.setter
    def log_display_level(self, log_display_level):
        try:
            log_display_level = log_display_level.upper()
        except AttributeError:
            # Probably an int.
            pass
        LOGGER.setLevel(log_display_level)
    @property
    def isverbose(self):
        """Verbose output enabled.
        Returns True if the verbosity is enough for info messages to be displayed.
        """
        return session.verbose_for("info")
    @property
    def debug(self):
        return show_tracebacks()
    @debug.setter
    def debug(self, value):
        show_tracebacks(value)
[docs]    def exclude(self, pattern):
        """Exclude records from loggers with names matching the specified pattern."""
        self._log_handler.exclude(pattern) 
[docs]    def print(self, text="", indent=0, error=False, exit_=False, exit_code=0, **kwargs):
        text = str(text)
        if indent > 0:
            text = textwrap.indent(text, " " * 4)
        click.secho(text, err=error, **kwargs)
        if exit_:
            self.exit(exit_code) 
[docs]    def print_error(
        self, exception_or_msg, title=None, exit_=True, exit_code=1, **kwargs
    ):
        if title:
            self.print(title, error=True, fg="red", bold=True)
        if self.debug and isinstance(exception_or_msg, BaseException):
            # Print the full traceback.
            from traceback import format_exception
            # Only print the traceback part of the exception, because the message is
            # printed below. FIXME: the `etype` is not used here, and the second
            # parameter set to None results in "NoneType: None" being printed by
            # `TracebackException` after the traceback...
            etype, _, tb = sys.exc_info()
            trace = "".join(format_exception(etype, None, tb))
            self.print(trace, indent=1)
        if self.fancy_errors and isinstance(exception_or_msg, KatScriptError):
            msg = _fancy_format_kat_error(exception_or_msg)
            fg = None
        else:
            # Format the whole error as red.
            msg = str(exception_or_msg)
            fg = "red"
        self.print(msg, error=True, fg=fg, **kwargs)
        if exit_:
            self.exit(exit_code) 
[docs]    def print_banner(self, kat_file=None, **kwargs):
        """Print the Finesse banner."""
        if not self.isverbose:
            return
        input_file = f"Input file: {kat_file.name}" if kat_file else ""
        timenow = datetime.datetime.now().strftime("%c").strip()
        timestr = f"{timenow:>60}"
        banner = rf"""
            ------------------------------------------------------------------------
                                 FINESSE {__version__}
                    o_.-=.       Frequency domain INterferomEter Simulation SoftwarE
                    (\'".\|                         http://www.gwoptics.org/finesse/
                    .>' (_--.
                _=/d   ,^\       {input_file}
                ~~ \)-'   '
                / |
                '  '    {timestr}
            ------------------------------------------------------------------------
            """
        self.print(textwrap.dedent(banner).strip(), **kwargs) 
[docs]    def exit(self, code=0):
        """Stop execution."""
        sys.exit(code)