# Implementation of “ch-image modify”.

import os
import sys
import tempfile
import uuid

import charliecloud as ch
import build_cache as bu
import cli
import filesystem as fs
import image as im
import irtree

lark = im.lark


class Modify_CLI(cli.Build_CLI):
   name = "modify"
   doc = "Build new image with shell commands."
   __slots__ = ("dst_image",     # destination image object
                "main_dynamic",  # main method corresponding to selected mode
                "src_image", )   # source image object
   flag_groups = {
      None: {
         "c":
            dict(metavar="CMD", dest="commands",
                 action="append", default=list(),
                 h="run CMD as though specified by RUN; can be repeated"),
         ("i", "interactive"):
            dict(action="store_true",
                 h="use interactive mode, even if stdin is not a TTY"),
         ("S", "shell"):
            dict(metavar="shell", default="/bin/sh",
                 h="use SHELL instead of the default /bin/sh"),
         "SRC":
            dict(dest="src_name",
                 h="image to modify"),
         "DST":
            dict(dest="dst_name",
                 h="name of resulting modified image"),
         "SCRIPT":
            dict(nargs="?",
                 h="(host) path of shell script to run inside image"),
      }
   }
   flag_groups.update(cli.Build_CLI.flag_groups)

   def irtree_from_commands(self):
      """Return a parse tree corresponding to a set of “ch-image modify”
         commands, as though the commands had been specified in a Dockerfile.
         Note that because “ch-image modify” simply executes one or more
         commands inside a container, the only Dockerfile instructions we need
         to consider are “FROM” and “RUN”. E.g. for the command line

            $ ch-image modify -c 'echo foo' -c 'echo bar' -- foo foo2

         this function produces the following parse tree:

            start
               dockerfile
                  from_
                     image_ref
                        IMAGE_REF foo
                  run
                     run_shell
                        LINE_CHUNK echo foo
                  run
                     run_shell
                        LINE_CHUNK echo bar"""
      # Children of dockerfile tree
      st = list()
      # Metadata attribute. We use this attribute in the “_pretty” method for
      # our “Tree” class. Constructing a tree without specifying a “Meta”
      # instance that has been given a “line” value will result in the
      # attribute not being present, which causes an error when we try to
      # access that attribute. Here we give the attribute a debug value of -1
      # to avoid said errors.
      meta = lark.tree.Meta()
      meta.line = -1
      meta.column = -1
      st.append(im.Tree(lark.Token('RULE', 'from_'),
                        [im.Tree(lark.Token('RULE', 'image_ref'),
                                 [lark.Token('IMAGE_REF', str(self.src_image))],
                                 meta)],
                        meta))
      if (self.c.shell is not None):
         st.append(im.Tree(lark.Token('RULE', 'shell'),
                          [lark.Token('STRING_QUOTED', '"%s"' % self.c.shell),
                           lark.Token('STRING_QUOTED', '"-c"')], meta))
      for cmd in self.c.commands:
         st.append(im.Tree(lark.Token('RULE', 'run'),
                           [im.Tree(lark.Token('RULE', 'run_shell'),
                                    [lark.Token('LINE_CHUNK', cmd)], meta)],
                           meta))
      return im.Tree(lark.Token('RULE', 'start'),
                     [im.Tree(lark.Token('RULE','dockerfile'),
                              st, meta)], meta)

   def irtree_from_script(self):
      """Temporary(?) analog of “modify_tree_make” for the non-interactive
         version of “modify” using a script. For the command line:

            $ ch-image modify foo foo2 /path/to/script

         this function produces the following parse tree

            start
               dockerfile
                  from_
                     image_ref
                        IMAGE_REF foo
                  copy
                     copy_shell
                        WORD /path/to/script WORD /ch/script.sh
                  run
                     run_shell
                        LINE_CHUNK /bin/sh /ch/script.sh"""
      # Children of dockerfile tree
      st = []
      # Metadata attribute. We use this attribute in the “_pretty” method for
      # our “Tree” class. Constructing a tree without specifying a “Meta”
      # instance that has been given a “line” value will result in the
      # attribute not being present, which causes an error when we try to
      # access that attribute. Here we give the attribute a debug value of -1
      # to avoid said errors.
      meta = lark.tree.Meta()
      meta.line = -1
      meta.column = -1
      st.append(im.Tree(lark.Token('RULE', 'from_'),
                        [im.Tree(lark.Token('RULE', 'image_ref'),
                                 [lark.Token('IMAGE_REF', str(self.src_image))],
                                  meta)],
                        meta))
      st.append(im.Tree(lark.Token('RULE', 'copy'),
                        [im.Tree(lark.Token('RULE', 'copy_shell'),
                           [lark.Token('WORD', self.c.script),
                            lark.Token('WORD', '/ch/script.sh')], meta)],
                        meta))
      # FIXME: Add error handling if “cli.shell” doesn’t exist (issue #1913).
      st.append(
         im.Tree(lark.Token('RULE', 'run'),
                 [im.Tree(lark.Token('RULE', 'run_shell'),
                          [lark.Token('LINE_CHUNK',
                                      '%s /ch/script.sh' % self.c.shell)],
                           meta)], meta))
      return im.Tree(lark.Token('RULE', 'start'),
                     [im.Tree(lark.Token('RULE','dockerfile'), st, meta)], meta)

   def main_commands(self):
      self.main_irtree(self.irtree_from_commands())

   def main_interactive(self):
      # Generate “fake” SID for build cache. We do this because we can’t
      # compute an SID, but we still want to make sure that it’s unique enough
      # that we’re unlikely to run into a collision.
      fake_sid = uuid.uuid4()
      # Set up the images.
      self.dst_image.unpack_clear()
      self.dst_image.copy_unpacked(self.src_image)
      bu.cache.worktree_adopt(self.dst_image, self.src_image.ref.for_path)
      bu.cache.ready(self.dst_image)
      bu.cache.branch_nocheckout(self.src_image.ref, self.dst_image.ref)
      # Run an interactive shell.
      cp = ch.cmd_simple(  [ch.CH_BIN + "/ch-run", "--unsafe", "-w"]
                         + sum([["-b", i] for i in self.c.bind], [])
                         + [str(self.dst_image.ref), "--", self.c.shell])
      xt = ch.Run_Exit(cp.returncode)
      if (xt == ch.Run_Exit.ERR_CMD):
         ch.FATAL("can’t run shell: %s" % self.c.shell)
      elif (xt != ch.Run_Exit.OK):
         ch.WARNING("interactive session exited with %d; committing anyway"
                    % cp.returncode)
      # Clean up and commit.
      ch.VERBOSE("using SID %s" % fake_sid)
      if (self.dst_image.metadata["history"] == []):
         self.dst_image.metadata["history"].append(
            { "empty_layer": False,
              "command":     "ch-image import"})
      self.dst_image.metadata_save()
      bu.cache.commit(self.dst_image.unpack_path,
                      fake_sid, "MODIFY interactive", [])

   def main_irtree(self, tree):
      ch.VERBOSE(tree.pretty()[:-1])  # rm trailing newline
      irtree.parse_tree_traverse(tree, self.c)

   def main_real(self):
      self.main_dynamic()

   def main_script(self):
      self.main_irtree(self.irtree_from_script())

   def main_stdin(self):
      # The basic approach here is that we write stdin to a file and then
      # pretend we got that as SCRIPT argument. FIXME: Perhaps better to just
      # feed it to the shell’s stdin directly but this works.
      text = ch.ossafe("can’t read standard input", sys.stdin.read)
      if (len(text) == 0):
         ch.FATAL("expected a script on stdin but it was empty")
      fp = tempfile.NamedTemporaryFile(delete=False)
      p = fs.Path(fp.name)
      p.file_write(text)
      ch.close_(fp)
      self.c.script = p
      self.main_script()
      p.unlink()

   def validate_main(self):
      # Source and destination image objects.
      self.src_image = im.Image(im.Reference(self.c.src_name))
      self.dst_image = im.Image(im.Reference(self.c.dst_name))
      if (not self.src_image.unpack_exist_p):
         ch.FATAL("base image not found: %s" % self.src_image.ref)
      super().validate_main()

   def validate_parse(self):
      # “modify” doesn’t really have a context directory. Use the filesystem
      # root so we can access necessary files (e.g., the modify script).
      self.c.context = "/"
      # Can’t modify an image into itself.
      if (self.c.src_name == self.c.dst_name):
         ch.FATAL("destination must be different from source image")
      # Figure out the mode.
      if (len(self.c.commands) > 0):
         if (self.c.interactive):
            ch.FATAL("-c is incompatible with -i")
         if (self.c.script):
            ch.FATAL("SCRIPT is incompatible with -c")
         self.main_dynamic = self.main_commands
      elif (self.c.script):
         if (self.c.interactive):
            ch.FATAL("SCRIPT is incompatible with -i")
         self.main_dynamic = self.main_script
      elif (sys.stdin.isatty() or self.c.interactive):
         self.main_dynamic = self.main_interactive
      else:
         self.main_dynamic = self.main_stdin
      # Done.
      ch.VERBOSE("modify shell: %s" % self.c.shell)
      ch.VERBOSE("modify mode: %s" % self.main_dynamic.__name__)
      super().validate_parse()
