#!/usr/bin/env python3
import os
import subprocess
import sys
import shutil
import traceback
import platform

# get radiopadre install source directory
# This is where we're installing from. If called with --editable, stuff in this
# directory needs to be symlinked to. Otherwise, assume the directory is temporary, and copy from it
PADRE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# JS9_VERSION = "3.3.1"  ## disabling for now, helper connection issues, see https://github.com/ratt-ru/radiopadre-client/pull/36#issuecomment-809648445
#JS9_VERSION = os.environ.get("RADIOPADRE_JS9_VERSION", "3.2")
JS9_VERSION = os.environ.get("RADIOPADRE_JS9_VERSION", "3.6.1")
JS9_IGNORE_ERRORS = os.environ.get("RADIOPADRE_JS9_IGNORE_ERRORS")

CARTA_VERSION = os.environ.get("RADIOPADRE_CARTA_VERSION", "2.0")
SYSTEM_CARTA = os.environ.get("RADIOPADRE_SYSTEM_CARTA", "True")
if SYSTEM_CARTA.upper() in ("0", "FALSE", ""):
    SYSTEM_CARTA = False

JS9_TGZ  = "js9-{}.tar.gz"   # will be passed to format(JS9_VERSION)
JS9_LINK = "https://github.com/ericmandel/js9/archive/v{0}.tar.gz" # will be passed to format(JS9_VERSION)

JS9_BRANCH = "master"  # or None to install release instead
JS9_REPO = "https://github.com/ericmandel/js9.git"

WETTY_VERSION = "latest" # "2.0.2"
PUPPETEER_VERSION = "latest" # "2.1.0"

CARTA_URL = os.environ.get("RADIOPADRE_CARTA_URL", "https://github.com/CARTAvis/carta-releases/releases/download/v{0}/CARTA-v{0}-ubuntu.tgz")
SOCKET_IO_LINK = "https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"

import argparse
parser = argparse.ArgumentParser(description="Installs radiopadre components inside an existing virtual environment")

parser.add_argument("-e", "--editable", action="store_true",
                    help="install in editable mode (use with pip install -e)")

parser.add_argument("--carta-release", metavar="VERSION", type=str, default=CARTA_VERSION,
                    help=f"Installs specific CARTA release. Default is {CARTA_VERSION}.")

parser.add_argument("--system-carta", action="store_true", dest="system_carta", default=SYSTEM_CARTA,
                    help=f"Use existing install of CARTA if available.")

parser.add_argument("--no-system-carta", action="store_false", dest="system_carta", default=SYSTEM_CARTA,
                    help=f"Ignore existing install of CARTA and always try to install --carta-release version.")

parser.add_argument("--js9-release", metavar="VERSION", type=str, default=JS9_VERSION,
                    help=f"Installs specific JS9 release. Default is {JS9_VERSION}.")

parser.add_argument("--js9-branch", metavar="BRANCH", type=str, default=JS9_BRANCH,
                    help=f"If set, installs JS9 from given branch of {JS9_REPO}.")

parser.add_argument("--js9-dir", metavar="DIR", type=str,
                    help=f"If set, installs JS9 from directory. If not absolute, taken relative to radiopadre source.")

parser.add_argument("--no-js9", action="store_true",
                    help="skips the JS9 installation. FITS functionality will be reduced.")

parser.add_argument("--cfitsio-path", metavar="DIR", type=str, default="",
                    help="path to cfitsio installation. Default is auto-detect.")

parser.add_argument("--inside-container", action="store_true",
                    help=argparse.SUPPRESS)
parser.add_argument("--skip-git-pull", action="store_true",
                    help=argparse.SUPPRESS)

options = parser.parse_args()

if not options.inside_container:
    PADRE_WORKDIR = os.path.expanduser("~/.radiopadre")
else:
    PADRE_WORKDIR = "/.radiopadre"

def message(x, prefix='setup-radiopadre-virtualenv: '):
    print(prefix + x.format(**globals()))

def bye(x, code=1):
    message(x)
    sys.exit(code)

def shell(cmd):
    return subprocess.check_call(cmd.format(**globals()), shell=True)

def which(cmd):
    return subprocess.check_output("which {}".format(cmd), shell=True).decode().strip()

def which_opt(cmd):
    try:
        return subprocess.check_output("which {}".format(cmd), shell=True).decode().strip()
    except subprocess.CalledProcessError as exc:
        return None

def remove_installation(path):
    if os.path.lexists(path):
        if os.path.islink(path) or os.path.isfile(path):
            message(f"Removing existing symlink or file {path}")
            os.unlink(path)
        elif os.path.isdir(path):
            message(f"Removing existing directory {path}")
            shutil.rmtree(path, ignore_errors=True)
        else:
            bye(f"{path} is neither a dir nor a symlink nor a file, don't know how to proceed")

# See https://stackoverflow.com/questions/1871549/determine-if-python-is-running-inside-virtualenv
if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix):
    message("Setting up radiopadre inside virtualenv {sys.prefix}")
    PADRE_VENV = sys.prefix
else:
    bye("Not running in a virtualenv")

# # check for JS9 requirements
# if not options.no_js9:
#     try:
#         nodejs = subprocess.check_output("which nodejs || which node", shell=True)
#     except subprocess.CalledProcessError:
#         if os.path.exists(PADRE_VENV):
#             open(PADRE_VENV + "/js9status", "w").write("nodejs missing")
#         bye("Unable to find nodejs or node -- can't install JS9. Try apt-get install nodejs perhaps, or run with --no-js9")

# Look for virtualenv. Burn it down if it needs a refresh.
complete_cookie = os.path.join(PADRE_VENV, ".radiopadre.install.complete")

# Create workdir if needed
if not os.path.exists(PADRE_WORKDIR):
    message("Creating {PADRE_WORKDIR}")
    os.mkdir(PADRE_WORKDIR)

# get jupyter base path
import sysconfig
site_packages_dir = sysconfig.get_paths()["purelib"]
## the above is the proper way to do the braindead below (sheesh Oleg...)
# notebook_dir = f"{PADRE_VENV}/lib/python3.6/site-packages"

# init radiopadre kernel etc.
message("Creating radiopadre jupyter kernel")
# if options.inside_container:
#     python, jupyter, user = "python", "jupyter", ""
# else:
#python, jupyter, user = f"{PADRE_VENV}/bin/python3", f"{PADRE_VENV}/bin/jupyter"
cmd = f"{PADRE_VENV}/bin/python3 -m ipykernel install --sys-prefix --name radiopadre"
message(f"  {cmd}")
if shell(cmd):
    bye("jupyter kernel failed to install, see log above")

kerneldir = f"{PADRE_VENV}/share/jupyter/kernels/radiopadre"
message("Copying kernel.js and logos to {kerneldir}")
shutil.copy2(f"{PADRE_PATH}/radiopadre/html/radiopadre-kernel.js", f"{kerneldir}/kernel.js")
shutil.copy2(f"{PADRE_PATH}/icons/radiopadre-logo-32x32.png", f"{kerneldir}/logo-32x32.png")
shutil.copy2(f"{PADRE_PATH}/icons/radiopadre-logo-64x64.png", f"{kerneldir}/logo-64x64.png")

notebook_static = f"{site_packages_dir}/notebook/static"
notebook_socket_io = f"{notebook_static}/socket.io.js"

# add link to radiopadre web components in notebook static dir
notebook_padre_www = f"{notebook_static}/radiopadre-www"
padre_www = f"{PADRE_PATH}/radiopadre/html"
remove_installation(notebook_padre_www)
if options.editable:
    message("making link from {notebook_padre_www} to {padre_www}")
    os.symlink(padre_www, notebook_padre_www)
else:
    message("copying {notebook_padre_www} to {padre_www}")
    shutil.copytree(padre_www, notebook_padre_www)

# overwrite icons
shutil.copy2(f"{PADRE_PATH}/icons/radiopadre-logo.ico", f"{notebook_static}/favicon.ico")
shutil.copy2(f"{PADRE_PATH}/icons/radiopadre-logo.ico", f"{notebook_static}/base/images/favicon-notebook.ico")


# find carta
message("CARTA install settings are options.system_carta={options.system_carta}, SYSTEM_CARTA={SYSTEM_CARTA}")

carta_system_version = None
carta_binary = which_opt("carta_backend")
if carta_binary:
    message(f"Found existing CARTA backend ({carta_binary})")
    try:
        carta_system_version = subprocess.check_output([carta_binary, "--version"], shell=False).decode().strip()
        message(f"This is CARTA version {carta_system_version}")
    except subprocess.SubprocessError as exc:
        message(f"Error running {carta_binary} --version: {exc}, ignoring this install")
    CARTA_FRONTEND_PATH = "/usr/share/carta/frontend"
    if os.path.isdir(CARTA_FRONTEND_PATH):
        message(f"Using frontend data {CARTA_FRONTEND_PATH}")
    else:
        carta_system_version = None
        message(f"Frontend data {CARTA_FRONTEND_PATH} not found, ignoring this install")
else:
    carta_binary = which_opt("carta")
    if carta_binary:
        message(f"No CARTA backend, but found {carta_binary}, assuming version 1.x")
        carta_system_version = "1.x"

if carta_system_version:
    if SYSTEM_CARTA:
        CARTA_VERSION = carta_system_version
    else:
        carta_system_version = None
        message(f"RADIOPADRE_SYSTEM_CARTA=0 set, so ignoring this install")

# setup CARTA paths
carta_url = CARTA_URL.format(options.carta_release)
carta_tarball = os.path.basename(carta_url)
carta_base    = os.path.splitext(carta_tarball)[0]
carta_dir     = os.path.join(PADRE_VENV, carta_base)
carta_link    = os.path.join(PADRE_VENV, "carta")
carta_link_appimage = os.path.join(PADRE_VENV, "carta-appimage")

# remove old install
remove_installation(carta_dir)
remove_installation(carta_link)
remove_installation(carta_link_appimage)

if not carta_system_version: 
    # download tarball
    carta_tarball_path = os.path.join(PADRE_WORKDIR, carta_tarball)
    for retry in True, False:
        # try to download if not present
        if os.path.exists(carta_tarball_path):
            message(f"CARTA tarball {carta_tarball_path} is already here")
        else:
            message(f"Trying to download CARTA via {carta_url}")
            subprocess.check_call(f"wget -c {carta_url} -O {carta_tarball_path}", shell=True)
            if not os.path.exists(carta_tarball_path):
                bye(f"{carta_tarball} failed to download")
            # download successfull, will not retry
            retry = False
        try:
            message(f"Unpacking {carta_tarball} in {PADRE_VENV}")
            subprocess.check_call(f"cd {PADRE_VENV}; tar zxf {carta_tarball_path}", shell=True)
            break
        # on error, see if we can retry the download
        except Exception as exc:
            if retry and os.path.exists(carta_tarball_path):
                message("Will retry the download")
                os.unlink(carta_tarball_path)
            else:
                raise
        finally:
            if sys.prefix == "/.radiopadre/venv":
                os.unlink(carta_tarball_path)

    if CARTA_VERSION >= "2":
        carta_appimage = os.path.splitext(os.path.basename(carta_tarball_path))[0]
        carta_appimage = os.path.join(PADRE_VENV, carta_appimage + ".AppImage")
        if os.path.exists(carta_appimage) and os.access(carta_appimage, os.X_OK):
            message(f"Linking {carta_link_appimage} to {carta_appimage}")
            os.symlink(os.path.basename(carta_appimage), carta_link_appimage)
        else:
            message(f"CARTA appimage ({carta_appimage}) not found")
    elif os.path.exists(carta_dir):
        message(f"Linking {carta_link} to {carta_dir}")
        os.symlink(os.path.basename(carta_dir), carta_link)
        message(f"Adjusting ownership and permissions on {carta_dir}")
        # subprocess.check_call(f"chown -R 1000.1000 {carta_dir}", shell=True)
        subprocess.check_call(f"chmod -R a+rX {carta_dir}", shell=True)
    else:
        message(f"WARNING: No CARTA installation ({carta_dir}) found")

# write status
with open(PADRE_VENV + "/carta_version", "wt") as ff:
    ff.write(CARTA_VERSION)

# install nodeenv and node
if not os.path.exists(f"{PADRE_VENV}/bin/npm"):
    message(f"Initializing nodeenv")
    libc_ver = platform.libc_ver()[1]
    message(f"  libc version is {libc_ver}")
    if platform.libc_ver()[1] <= '2.25':
        NODEJS_VERSION = "16.13.2"  
        message(f"  will use nodejs {NODEJS_VERSION} since we are on an older system")
    else:
        NODEJS_VERSION = None
        message("  will use latest nodejs")
    if NODEJS_VERSION is not None:
        subprocess.check_call(f"nodeenv -n {NODEJS_VERSION} -p", shell=True)
    else:
        subprocess.check_call(f"nodeenv -p", shell=True)
else:
    message(f"Looks like nodeenv is already installed in {PADRE_VENV}")
node_version = subprocess.check_output(f"node -v", shell=True).decode()
message(f"Node version is {node_version}")

# install npm deps
shell(f"npm install -g utf-8-validate bufferutil puppeteer@{PUPPETEER_VERSION} yarn")

#sys.exit(0)
# install wetty
shell(f"bash -c 'source {PADRE_VENV}/bin/activate "
       "&& cd {PADRE_VENV}"
       "&& yarn install"
       "&& yarn add --cache-folder yarn.cache --modules-folder node_modules wetty@{WETTY_VERSION}"
       "&& ln -s `yarn bin --cache-folder yarn.cache --modules-folder node_modules wetty` bin/wetty"
       "'")

# install JS9
if not options.no_js9:
    js9status = open(PADRE_VENV + "/js9status", "w")
    js9_www = None
    try:
        # install JS9 inside venv
        # determine whether to use existing directory or repo
        js9_dir = js9_temp = None
        if options.js9_dir:
            js9_dir = options.js9_dir
            if not os.path.isabs(js9_dir):
                js9_dir = os.path.join(PADRE_PATH, js9_dir)
                message(f"Will try to install JS9 from existing directory {js9_dir}")
            if not os.path.isdir(js9_dir):
                raise RuntimeError(f"no such directory: {js9_dir}")
        elif options.js9_branch:
            js9_dir = f"{PADRE_PATH}/js9"
            if os.path.isdir(js9_dir) and os.path.isdir(f"{js9_dir}/.git"):
                message(f"Pulling {options.js9_branch} in {js9_dir}")
                shell("cd {js9_dir} && git fetch origin && git checkout {options.js9_branch} && git pull")
            else:
                import tempfile
                js9_dir = js9_temp = tempfile.mkdtemp(prefix="js9")
                message(f"Cloning {JS9_REPO} branch {options.js9_branch} into {js9_dir}")
                shell("git clone -b {options.js9_branch} {JS9_REPO} {js9_dir}")
        elif options.js9_release:
            JS9_TGZ = JS9_TGZ.format(options.js9_release)
            JS9_LINK = JS9_LINK.format(options.js9_release)
            js9_tarball_path = f"{PADRE_WORKDIR}/{JS9_TGZ}"
            for retry in True, False:
                if os.path.exists(js9_tarball_path):
                    message(f"{js9_tarball_path} is already here, will try to unpack")
                else:
                    message(f"Will try to download JS9 from {JS9_LINK}")
                    shell(f"wget -c {JS9_LINK} -O {js9_tarball_path}")
                # try to unpack
                try:
                    shell(f"cd {PADRE_PATH} && tar zxf {js9_tarball_path}")
                    break
                except Exception as exc:
                    if retry and os.path.exists(js9_tarball_path):
                        message("Will retry the download")
                        os.unlink(js9_tarball_path)
                    else:
                        raise
                finally:
                    if sys.prefix == "/.radiopadre/venv":
                        os.unlink(js9_tarball_path)

            js9_dir = f"{PADRE_PATH}/js9-{options.js9_release}"
            if not os.path.isdir(js9_dir):
                raise RuntimeError("{js9_dir} does not exist. Did the tarball download and unpack?")
        else:
            raise RuntimeError("unknown JS9 install method. Specify one of --js9-release, --js9-branch, --js9-dir")

        # Configure and install
        js9_www = PADRE_VENV + "/js9-www"
        with_cfitsio = f"--with-cfitsio={options.cfitsio_path}" if options.cfitsio_path else "--with-cfitsio"
        if subprocess.call("""cd {js9_dir} && \
                ./configure --prefix={PADRE_VENV} --with-webdir={js9_www} --with-helper=nodejs {with_cfitsio} && \
                make && make install""".format(**globals()), shell=True):
            raise RuntimeError("Failed to configure and/or build JS9 in {js9_dir}, see log above. Fix it, or run with --no-js9.")

        # check for cfitsio
        try:
            output = subprocess.check_output("grep FITSLIB {js9_dir}/config.log".format(**globals()), shell=True)
            if output.strip() != b"FITSLIB='cfitsio'":
                raise subprocess.CalledProcessError("cfitsio",-1,"no cfitsio")
        except subprocess.CalledProcessError:
            raise RuntimeError("JS9 did not find the cfitsio library. Try installing it (apt install libcfitsio-dev), and/or specifying"
                " the path to it with --cfitsio-path, and/or running with --no-js9 if you're really stuck.")

        # add colormap definitions from matplotlib
        message("copying extra colormap definitions to {js9_www}")
        shutil.copy2(f"{PADRE_PATH}/radiopadre/html/js9colormaps.js", js9_www)

        # Make symlink to js9 install dir in notebook dir
        notebook_js9_www = site_packages_dir + "/notebook/static/js9-www"

        remove_installation(notebook_js9_www)
        message("making link from {notebook_js9_www} to {js9_www}")
        os.symlink(js9_www, notebook_js9_www)

        notebook_socket_io = site_packages_dir + "/notebook/static/socket.io.js"
        js9_socket_io = js9_www + "/node_modules/socket.io-client/dist/socket.io.js"

        remove_installation(notebook_socket_io)
        message("making link from {notebook_socket_io} to {js9_socket_io}")
        os.symlink(js9_socket_io, notebook_socket_io)

        # copy config
        message("copying js9prefs.js to {js9_www}")
        shutil.copy2(f"{PADRE_PATH}/js9-config/js9prefs.js", js9_www)
        message("copying js9Prefs.json to {js9_www}")
        shutil.copy2(f"{PADRE_PATH}/js9-config/js9Prefs.json", js9_www)

        if js9_temp:
            message("Removing JS9 source directory {js9_temp}")
            shutil.rmtree(js9_temp)

    except Exception as exc:
        traceback.print_exc()
        js9status.write(str(exc))
        if JS9_IGNORE_ERRORS:
            options.no_js9 = True
        else:
            bye(f"Error installing JS9: {exc}")
    finally:
        js9status.write(os.path.abspath(js9_www))


if options.no_js9:
    # no JS9, still need to download socket.io.js
    remove_installation(notebook_socket_io)
    message("Downloading socket.io.js into {notebook_socket_io}")
    shell(f"wget {SOCKET_IO_LINK} -O {notebook_socket_io}")


open(complete_cookie, "w").write("installed by {__file__}".format(**globals()))

message("Installation successful!")
