#!/usr/bin/env python3
# coding: utf-8

# Copyright (C) 1994-2021 Altair Engineering, Inc.
# For more information, contact Altair at www.altair.com.
#
# This file is part of both the OpenPBS software ("OpenPBS")
# and the PBS Professional ("PBS Pro") software.
#
# Open Source License Information:
#
# OpenPBS is free software. You can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the
# Free Software Foundation, either version 3 of the License, or (at your
# option) any later version.
#
# OpenPBS is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Commercial License Information:
#
# PBS Pro is commercially licensed software that shares a common core with
# the OpenPBS software.  For a copy of the commercial license terms and
# conditions, go to: (http://www.pbspro.com/agreement.html) or contact the
# Altair Legal Department.
#
# Altair's dual-license business model allows companies, individuals, and
# organizations to create proprietary derivative works of OpenPBS and
# distribute them - whether embedded or bundled with other software -
# under a commercial license agreement.
#
# Use of Altair's trademarks, including but not limited to "PBS™",
# "OpenPBS®", "PBS Professional®", and "PBS Pro™" and Altair's logos is
# subject to Altair's trademark licensing policies.


import os
import sys
import socket
import getopt
import tempfile
import logging
import logging.config
import errno

import ptl
import ptl.lib
from ptl.utils.pbs_cliutils import CliUtils
from ptl.utils.pbs_dshutils import DshUtils
from ptl.lib.pbs_testlib import PtlConfig


# trap SIGINT and SIGPIPE
def trap_exceptions(etype, value, tb):
    sys.excepthook = sys.__excepthook__
    if issubclass(etype, KeyboardInterrupt):
        pass
    elif issubclass(etype, IOError) and value.errno == errno.EPIPE:
        pass
    else:
        sys.__excepthook__(etype, value, tb)


sys.excepthook = trap_exceptions

# A basic SWIG interface definition to wrap PBS IFL
swiginter = '''\
%module pbs_ifl

%typemap(out) char ** {
  int len,i;
  len = 0;
  while ($1[len]) len++;
  $result = PyList_New(len);
  for (i = 0; i < len; i++) {
    PyList_SetItem($result,i,PyString_FromString($1[i]));
  }
}

%typemap(in) char ** {
  /* Check if is a list */
  if (PyList_Check($input)) {
    int size = PyList_Size($input);
    int i = 0;
    $1 = (char **) malloc((size+1)*sizeof(char *));
    for (i = 0; i < size; i++) {
      PyObject *o = PyList_GetItem($input,i);
      if (PyString_Check(o))
    $1[i] = PyString_AsString(PyList_GetItem($input,i));
      else {
    PyErr_SetString(PyExc_TypeError,"list must contain strings");
    free($1);
    return NULL;
      }
    }
    $1[i] = 0;
  } else {
    PyErr_SetString(PyExc_TypeError,"not a list");
    return NULL;
  }
}

%typemap(out) struct batch_status * {
    struct batch_status *head_bs, *bs;
    struct attrl *attribs;
    char *resource;
    char *str;
    int i, j;
    int len;
    char buf[4096];
    static char *id = "id";

    head_bs = $1;
    bs = $1;

    for (len=0; bs != NULL; len++)
        bs = bs->next;

    $result = PyList_New(len);

    bs = head_bs;

    for (i=0; i < len; i++) {
        PyObject *dict;
        PyObject *a, *v, *tmpv;

        dict = PyDict_New();
        PyList_SetItem($result, i, dict);

        a = PyString_FromString(id);
        v = PyString_FromString(bs->name);
        PyDict_SetItem(dict, a, v);

        attribs = bs->attribs;
        while (attribs) {
            resource = attribs->resource;
            if (resource != NULL) {
                /* +2 to account for the '.' between name and resource */
                str = malloc(strlen(attribs->name) + strlen(resource) + 2);
                sprintf(str, "%s.%s", attribs->name, attribs->resource);
                a = PyString_FromString(str);
            }
            else {
                a = PyString_FromString(attribs->name);
            }
            tmpv = PyDict_GetItem(dict, a);
            /* if the key already exists, append as comma-separated */
            if (tmpv != NULL) {
                char *s = PyString_AsString(tmpv);
                /* +4 for the quotes, the comma, and a NULL byte */
                str = malloc(strlen(attribs->value) + strlen(s) + 4);
                sprintf(str, "%s,%s", attribs->value, s);
                v = PyString_FromString(str);
            }
            else {
                v = PyString_FromString(attribs->value);
            }
            PyDict_SetItem(dict, a, v);
            attribs = attribs->next;
        }
        bs = bs->next;
    }
}
%{
#include "pbs_ifl.h"
int pbs_py_spawn(int, char *, char **, char **);
%}

%include "pbs_ifl.h"
int pbs_py_spawn(int, char *, char **, char **);
'''


def _remove_file(workdir, filename):
    f = os.path.join(workdir, filename)
    if os.path.isfile(f):
        logging.debug('removing intermediary file ' + filename)
        os.remove(f)


def usage():
    msg = []
    msg += ['Usage: ' + os.path.basename(sys.argv[0]) + ' [OPTION]\n\n']
    msg += ['  Produce Python wrappers for PBS IFL API\n\n']
    msg += ['-c <pbs_conf>: path to pbs.conf\n']
    msg += [
        '-f: force overwrite of _pbs_ifl.so and pbs_ifl.py when present\n']
    msg += ['-h: display usage information\n']
    msg += ['-i <swig.i>: path to swig interface file\n']
    msg += ['-I <python_include>: path to python include directory\n']
    msg += ['-l <level>: logging level\n']
    msg += ['-s <swig>: path to swig binary to use\n']
    msg += ['-t <targethost>: hostname to operate on\n']
    msg += ['-w <workdir>: path to working directory\n']
    msg += ['--log-conf=<file>: logging config file\n']
    msg += ['--version: print version number and exit\n']

    print("".join(msg))


if __name__ == '__main__':

    workdir = tempfile.gettempdir()
    targethost = socket.gethostname()
    config = None
    interface = None
    pythoninc = None
    level = 'INFO'
    force = False
    swigbin = 'swig'
    logconf = None
    complete_options = {}
    duplicate_options = []

    if 'PBS_CONF_FILE' in os.environ:
        config = os.environ['PBS_CONF_FILE']
    else:
        config = '/etc/pbs.conf'

    opts, args = getopt.getopt(sys.argv[1:], "t:i:I:c:w:l:s:hf",
                               ["log-conf=", "version"])

    for o, val in opts:
        if o == '-t':
            targethost = val
        elif o == '-w':
            workdir = CliUtils.expand_abs_path(val)
        elif o == '-i':
            interface = val
        elif o == '-l':
            level = val
        elif o == '-c':
            config = val
        elif o == '-I':
            pythoninc = CliUtils.expand_abs_path(val)
        elif o == '-f':
            force = True
        elif o == '-s':
            swigbin = CliUtils.expand_abs_path(val)
        elif o == '--log-conf':
            logconf = val
        elif o == '--version':
            print(ptl.__version__)
            sys.exit(0)
        elif o == '-h':
            usage()
            sys.exit(0)
        else:
            sys.stderr.write("Unrecognized option\n")
            usage()
            sys.exit(1)
        if o in complete_options:
            duplicate_options.append("option %s -> %s already used.\n"
                                     % (o, val))
        complete_options[o] = val

    if len(duplicate_options) > 0:
        sys.stderr.write("Please use an option only once, exiting.\n")
        for option in duplicate_options:
            sys.stderr.write("    %s" % option)
        sys.exit(1)

    cu = CliUtils()

    if logconf:
        logging.config.fileConfig(logconf)
    else:
        log_lvl = cu.get_logging_level(level)
        logging.basicConfig(level=log_lvl)

    b = cu.check_bin(swigbin)
    if not b:
        logging.error("swig is missing, exiting")
        sys.exit(1)

    b = cu.check_bin("gcc")
    if not b:
        logging.error("gcc is missing, exiting")
        sys.exit(1)

    if pythoninc is None:
        logging.error("Path to Python include directory is mandatory")
        usage()
        sys.exit(1)

    if targethost != socket.gethostname():
        logging.error("This command only works on localhost")
        sys.exit(1)

    PtlConfig()
    du = DshUtils()
    pbs_conf = du.parse_pbs_config(targethost, file=config)

    os.chdir(workdir)

    if interface is None:
        interface = os.path.join(workdir, "pbs_ifl.i")
        f = open(interface, 'w')
        f.write(swiginter)
        f.close()
        srcdir = os.getcwd()
    else:
        srcdir = os.path.dirname(interface)

    if 'PBS_EXEC' in pbs_conf:
        pbsinclude = os.path.join(pbs_conf['PBS_EXEC'], 'include')
        cmd = [swigbin, '-python', '-I' + pbsinclude, interface]
        logging.debug(du.run_cmd(targethost, cmd))
        if srcdir != os.getcwd():
            logging.debug(du.run_copy(targethost,
                                      src=os.path.join(
                                          srcdir, "pbs_ifl_wrap.c"),
                                      dest=workdir))
            logging.debug(du.run_copy(targethost,
                                      src=os.path.join(srcdir, "pbs_ifl.py"),
                                      dest=workdir))
        cmd = ['gcc', '-Wall', '-Wno-unused-variable', '-fPIC', '-shared',
               '-I' + pbsinclude]
        cmd += ['-I' + pythoninc]
        cmd += ['pbs_ifl_wrap.c']
        cmd += ['-L' + os.path.join(pbs_conf['PBS_EXEC'], 'lib')]
        cmd += ['-lpbs']
        cmd += ['-o', '_pbs_ifl.so']
        cmd += ['-lcrypto', '-lssl']
        logging.debug(du.run_cmd(targethost, cmd))

    libdir = os.path.dirname(ptl.lib.__file__)
    if force or not os.path.isfile(libdir + '/_pbs_ifo.so'):
        du.run_copy(targethost,
                    src=os.path.join(workdir, '_pbs_ifl.so'),
                    dest=os.path.join(libdir, '_pbs_ifl.so'), sudo=True)
    if force or not os.path.isfile(os.path.join(libdir, '/pbs_ifl.py')):
        du.run_copy(targethost,
                    src=os.path.join(workdir, 'pbs_ifl.py'),
                    dest=os.path.join(libdir, 'pbs_ifl.py'), sudo=True)

    _remove_file(workdir, "pbs_ifl.py")
    _remove_file(workdir, "_pbs_ifl.so")
    _remove_file(workdir, "pbs_ifl_wrap.c")
    _remove_file(workdir, "pbs_ifl.i")
