#!/usr/bin/env python3
import argparse,atexit,os,re,shutil,sys

# We run this code from the source directory, and we want to use the latest
# version of our libraries.
SRCDIR=os.path.dirname(os.path.realpath(sys.argv[0]))
sys.path.insert(0,os.path.join(SRCDIR,'pylib'))
from install import *

__author__='Jeff Clough, @jeffclough@mastodon.social'
__version__='0.1.1-2022-11-27'

# Regardless of how this code terminates, put the PWD back like we found it.
atexit.register(os.chdir,os.getcwd())
# But for now, use this program's directory as the default.
os.chdir(SRCDIR)

ap=argparse.ArgumentParser(
  #formatter_class=argparse.RawDescriptionHelpFormatter,
  description=f"""\
Install the "handy" scripts and modules to the given (with --dir) or default ({options.tdir}) base directory. It will be created if necessary, along with its subdirectories.

The --verbosity (-v) flags are any of quiet, ops, deps, time, debug, and full. The "quiet" flag turns all the others off. "full" turns them all on. Flags can be combined with + or - characters to add or remove those flags. The first flag listed is assumed to be added unless preceded by -. There are no spaces in --verbosity's value. E.g. "./install -v ops+time ..." or "./install -v full-debug".
"""
)
ap.add_argument('--dir',dest='tdir',action='store',default=options.tdir,help="Set the base of the target directory structure. Subdirectories like bin, doc, etc, include, lib, man, and sbin will be created here if and when needed. (default: %(default)s)")
ap.add_argument('--vdir',dest='vdir',action='store',default=options.vdir,help="Set the directory where Python virtual environments are to be created. (default: %(default)s)")
ap.add_argument('--python',metavar='COMMAND',action='store',default=options.python,help="The python command to run when installing virtual environments. (default: %(default)s)")
ap.add_argument('--force',action='store_true',help="Rather than comparing the timestamps of source and target files, copy the former to the latter unconditionally.")
ap.add_argument('--dryrun','-n',action='store_true',help="Go through all the motions, but don't do any work.")
ap.add_argument('--test',action='store_true',help="Run internal tests, report the results, and terminate.")
ap.add_argument('-v','--verbosity',action='store',default=options.verbFlags(),help="""Verbosity flags. See description text above for details. (default: %(default)s)""")
ap.add_argument('--debugger',action='store_true',help="Engage pdb within this script once we get through the setup. This is not for the uninitiated. See https://docs.python.org/3/library/pdb.html#debugger-commands for a command summary.")
ap.add_argument('target',action='store',nargs='*',help="This is one of more files to be installed.")
opt=ap.parse_args()

# Parse our verbosity flags first.
options.verb=parse_verbosity(opt.verbosity)
dc.enable(options.verb & V.DEBUG)

# Update our Options instance (options) with our command line option values.
options.dryrun=opt.dryrun
options.force=opt.force
options.tdir=Path(options.tdir).expandAll()
options.vdir=Path(options.vdir).expandAll()
options.python=opt.python

dc(f"options: {options}")

if opt.debugger:
  import pdb
  pdb.set_trace()

### Testing ###

if opt.test:
  import doctest

  def test_mode_conversion():
    """
    >>> stat_mode(0o700)==stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR
    True
    >>> stat_mode(0o070)==stat.S_IRGRP|stat.S_IWGRP|stat.S_IXGRP
    True
    >>> stat_mode(0o007)==stat.S_IROTH|stat.S_IWOTH|stat.S_IXOTH
    True
    >>> shell_mode(stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)==0o700
    True
    >>> shell_mode(stat.S_IRGRP|stat.S_IWGRP|stat.S_IXGRP)==0o070
    True
    >>> shell_mode(stat.S_IROTH|stat.S_IWOTH|stat.S_IXOTH)==0o007
    True
    >>> dir_mode(0o640)==0o750
    True
    >>> dir_mode(0o400)==0o500
    True
    """

    pass

  failed,total=doctest.testmod()
  if failed:
    print(f"Failed {failed} of {total} tests.")
    sys.exit(1)
  print(f"Passed all {total} tests!")
  sys.exit(0)

# TODO: Process command line targets. Default to "all".

# Make sure we have someplace to install these files.
target_dir=Folder(options.tdir)()
bin_dir=Folder(options.tdir/'bin')()
Folder(options.tdir/'etc')()
Folder(options.tdir/'include')()
Folder(options.tdir/'lib')()
lib_dir=Folder(options.tdir/'lib'/'python')()
venv_dir=Folder(options.vdir)()

pylib=Folder(options.sdir/'pylib') # No need to create this in the filesystem.

# Copy our library modules first because some of our scripts are really just
# symlinks to them. These Python modules are located in our pylib directory
# and must be copied to ~/my/lib/python.
pylibs=[
  'CSV.py',
  'RE.py',
  'abbrev.py',
  'ansi.py',
  'conf.py',
  'daemon.py',
  'date_parser.py',
  'debug.py',
  'dirwalker.py',
  'english.py',
  'exiftool.py',
  'grep.py',
  'handy.py',
  'install.py',
# 'latlong.py',
  'loggy.py',
  'parsing.py',
  'path.py',
  'png.py',
  'prime.py',
  'semver.py',
  'shc.py',
  'stardate.py',
  'table.py',
  'tree.py',
  'versioning.py',
]

for f in pylibs:
  File(lib_dir/f).copy(pylib/f,follow=True)()

# Set up symlinks to Python modules that can also be run as commands.
command_links=[
  'timeout',
]
for cmd in command_links:
  mod=cmd+'.py'
  File(lib_dir/mod).copy(pylib/mod).link(bin_dir/cmd,force=options.force)()

# Do something very similar with phonetics.py.
File(lib_dir/'phonetics.py').copy(pylib/'phonetics.py').link(
  *[bin_dir/x for x in ('nato','police','raf','us','wu')],
  force=options.force
)()


# If pip installed, set up a "pip" command. Otherwise, pip is None.
pip=shutil.which('pip3')
if pip:
  pip=Command(pip)

# Determine whether our Python supports SSL.
res=Command(f"python3 -c 'import ssl'",stderr=DEVNULL,quiet=True)()
dc(f"res.result={res.result!r}")
supports_ssl=res.result==0
dc(f"supports_ssl={supports_ssl!r}")

# Use pip to install pyparsing if it's not already there.
pyparsing=lib_dir/'pyparsing'
#dc(f"{pyparsing!r}.isDir() returns {pyparsing.isDir()}.")
if supports_ssl:
  if not pyparsing.isDir():
    Command(f"pip3 install -t {lib_dir} pyparsing")()
else:
  print("WARNING: Skipping pyparsing because pip won't run without SSL support in Python.")

#TODO: Figure out why forcing links works for "base" below, but not for
# "timeout" and "phonetics.py" above.

# Copy our "base" script and set up its symlinks.
File(bin_dir/'base').copy('base').link(
  'base16',
  'base32',
  'base64',
  'decode16',
  'decode32',
  'decode64',
  'encode16',
  'encode32',
  'encode64',
  link_dir=bin_dir,
  force=options.force
)()

# Copy our Python and shell scripts. These are located in our source directory.
scripts=[
  'ad-userAccountControl',
  'ansi',
  'args',
  'ascii',
  'backup-volume',
  'certmon',
  'chronorename',
  'columnate',
  'csv',
  'datecycle',
  'dump',
  'factors',
  'fib',
  'freq',
  'ind',
  'ip2host',
  'json',
  'json2csv',
  'keeplast',
  'ldif',
  'mark',
  'mazer',
  'mix',
  'names',
  'not',
  'now',
  'numlines',
  'patch-cal',
  'pg',
  'ph',
  'portname',
  'pretty-json',
  'prime',
  'progress',
  'pwgen',
  'pygrep',
  'randword',
  're',
  'reduce',
  'secdel',
  'slice',
  'strftime',
  'strptime',
  'timeshift',
  'title-case',
  'tread',
  'ts',
  'wrap',
]

# Copy our scripts.
for f in scripts:
  File(bin_dir/f).copy(f)()

class VirtualEnv(Target):

  def __init__(self,cmd,venv_dir=None):
    """Prepare to create a Python virtual environment for command cmd,
    which depends on the pip-installable packages named in the "deps"
    arguments (if any)."""

    if venv_dir is None:
      venv_dir=options.vdir

    super().__init__(venv_dir/cmd/cmd)
    self.cmd=cmd
    self.requirements=[]

  def requires(self,*args):
    """The venv requires these packages. We'll call pip to install them."""

    self.requirements.extend(args)
    return self

  def __call__(self,*args):
    """Create our virtual environment if needed, and set up our command
    to run from there."""

    # Create and initialize our Python virtual environment for this command.
    # TODO Don't create a venv that already exists.
    if not self.target.isDir():
      script=f"""\
#!/bin/bash
cd {venv_dir}
{options.python} -m venv {cmd}
"""
      if self.requirements:
        modules=' '.join(self.requirements)
        script+=f"""\
cd {cmd}
source ./bin/activate
pip install {modules}"""
      res=ShellScript(script,stdout=PIPE,stderr=PIPE)()
      self.stdout=res.stdout
      self.stderr=res.stderr
      self.result=res.result
      self.script_filename=res.cmd

    # Copy "runner" to our new venv directory, and symlink cmd to it from our bin directory.
    runner=File(venv_dir/self.cmd/'runner').copy('runner')
    runner.link(cmd,link_dir=options.bdir,force=True)
    runner()

    # Create our symlinks to runner from our bin_dir.
    self.target=runner
    
    return self

# Create and set up our Python virtual environments.
v=VirtualEnv(venv_dir/'j2p')
dc(f"v={v!r}")
v.requires('javalang','six')
dc(f"v={v!r}")
v.link('j2p',link_dir=bin_dir,force=True)
dc(f"v={v!r}")
v()
VirtualEnv(venv_dir/'qr').requires('PyQRCode').link(bin_dir/'qr')()

dc(f"platform.system={platform.system}")
if platform.system=='Darwin':
  # Get a list of the non-Alex English voices built into the "say" command.
  re_say_parser=re.compile(r'([-a-zA-Z]+)\s+([_a-zA-Z]+)')
  say=Command("say -r250",stdout=PIPE,stderr=PIPE,quiet=True)
  say("-v?")
  voices=[
    bin_dir/m.group(1).lower() for m in
      [re_say_parser.match(l) for l in say.stdout.split('\n')]
    if m and m.group(2).startswith('en_') and m.group(1).lower()!='alex'
  ]
  dc(f"Found voices: {voices}")
  # Install our "alex" script and set up its symlinks.
  File(bin_dir/'alex').copy('alex').link(*voices)()

# We exit with a return code of 1 if any exceptions occurred.
sys.exit(int(bool(Target.exceptions)))
