#!/usr/bin/env python3

import argparse
import ast
import collections.abc
import inspect
import os.path
import sys

ch_lib = "/usr/lib/charliecloud"
sys.path.insert(0, ch_lib)
import charliecloud as ch
import cli
# Note: Any module with a class defining a subcommand must be imported
# *somewhere* so the CLI can find that subcommand. Here’s as good a place as
# any and that’s why lint thinks they’re unused. Beware circular imports.
import build
import gestalt
import modify


## Constants ##

# FIXME: It’s currently easy to get the ch-run path from another script, but
# hard from something in lib. So, we set it here for now.
ch.CH_BIN = os.path.dirname(os.path.abspath(
                 inspect.getframeinfo(inspect.currentframe()).filename))
ch.CH_RUN = ch.CH_BIN + "/ch-run"


## Main ##

def main():

   if (not os.path.exists(ch.CH_RUN)):
      ch.depfails.append(("missing", ch.CH_RUN))

   # Monkey patch problematic characters out of stdout and stderr.
   ch.monkey_write_streams()

   # ch-image(1) flags are defined in the Root_CLI class. Sub-commands are
   # defined throughout the program; look for main_real() methods.
   ap = cli.Root_CLI()  # create parser
   sc = ap.parse()      # parse command line
   ch.init(sc.c)        # initialize all kinds of random stuff incl. logging
   sc.log_parsed()      # print out configuration we found
   ch.profile_start()   # start profiling if needed (stopped in ch.exit())
   sc.main()            # execute subcommand

   ch.exit(0)


## Functions ##

def breakpoint_inject(module_name, line_no):
   # Inject a PDB breakpoint into the module named module_name before the
   # statement on line line_no. See: https://stackoverflow.com/a/41858422

   class PDB_Injector(ast.NodeTransformer):
      def __init__(self, *args, **kwargs):
         self.inject_ct = 0
         return super().__init__(*args, **kwargs)
      def generic_visit(self, parent):
         # Operate on parent of target statement because we need to inject the
         # new code into the parent’s body (i.e., as siblings of the target
         # statement).
         if (    self.inject_ct == 0
             and hasattr(parent, "body")
             and isinstance(parent.body, collections.abc.Sequence)):
            for (i, child) in enumerate(parent.body):
               if (    isinstance(child, ast.stmt)
                   and hasattr(child, "lineno")
                   and child.lineno == line_no):
                  ch.WARNING(  "--break: injecting PDB breakpoint: %s:%d (%s)"
                             % (module_name, line_no, type(child).__name__))
                  parent.body[i:i] = inject_tree.body
                  self.inject_ct += 1
                  break
         super().generic_visit(parent)  # superclass actually visits children
         return parent

   if (module_name not in sys.modules):
      ch.FATAL("--break: no module named %s" % module_name)
   module = sys.modules[module_name]
   src_text = inspect.getsource(module)
   src_path = inspect.getsourcefile(module)
   module_tree = ast.parse(src_text, "%s <re-parsed>" % src_path)
   inject_tree = ast.parse("import pdb; pdb.set_trace()", "Weird Al Yankovic")

   ijor = PDB_Injector()
   ijor.visit(module_tree)  # calls generic_visit() on all nodes
   if (ijor.inject_ct < 1):
      ch.FATAL("--break: no statement found at %s:%d" % (module_name, line_no))
   assert (ijor.inject_ct == 1)

   ast.fix_missing_locations(module_tree)
   exec(compile(module_tree, "%s <re-compiled>" % src_path, "exec"),
        module.__dict__)
   # Set a global in the target module so it can test if it’s been
   # re-executed. This means re-execution is *complete*, so it will not be set
   # in module-level code run during re-execution, but if the original
   # execution continues *after* re-execution completes (this happens for
   # __main__), it *will* be set in that code.
   module.__dict__["breakpoint_reexecuted"] = "%s:%d" % (module_name, line_no)


## Bootstrap ##

# This code is more complicated than the standard boilerplace (i.e., “if
# (__name__ == "__main__"): main()”) for two reasons:
#
#   1. The mechanism for fatal errors is to raise ch.Fatal_Error. We catch
#      this to re-print warnings and print the error message before exiting.
#      (We used to priont an error message and then sys.exit(1), but this
#      approach lets us do things like rollback and fixes ordering problems
#      such as #1486.)
#
#   2. There is a big mess of hairy code to let us set PDB breakpoints in this
#      file (i.e., module __main__) with --break. See PR #1837.

if (__name__ == "__main__"):
   try:
      # We can’t set these two module globals that support --break normally
      # (i.e., module-level code at the top of this file) because this module
      # might be executed twice, and thus any value we set would be
      # overwritten by the default when the module is re-executed.
      if ("breakpoint_considered" not in globals()):
         global breakpoint_considered
         breakpoint_considered = True
         # A few lines of bespoke CLI parsing so that we can inject
         # breakpoints into the CLI parsing code itself.
         for (opt, arg) in zip(sys.argv[1:], sys.argv[2:] + [None]):
            (opt, _, arg_eq) = opt.partition("=")
            if (opt == "--break"):
               if (not sys.stdin.isatty()):
                  ch.FATAL("--break: standard input must be a terminal")
               if (arg_eq != ""):
                  arg = arg_eq
               try:
                  (module_name, line_no) = arg.split(":")
                  line_no = int(line_no)
               except ValueError:
                  ch.FATAL("--break: can’t parse MODULE:LIST: %s" % arg)
               breakpoint_inject(module_name, line_no)
      # If we injected into __main__, we already ran main() when re-executing
      # this module inside breakpoint_inject().
      if ("breakpoint_reexecuted" not in globals()):
         main()
         ch.exit(0)
   except ch.Fatal_Error as x:
      ch.ERROR(*x.args, **x.kwargs)
      ch.exit(1)
