#! /usr/bin/python3
# vim: et ts=4 sw=4

# Copyright © 2010-2012 Piotr Ożarowski <piotr@debian.org>
# Copyright © 2010 Canonical Ltd
# Copyright © 2025 Akexander Sulfrian <alexander@sulfrian.net>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import logging
import optparse
import os
import sys
from os import environ 
from os.path import exists, join, splitext
from subprocess import PIPE, Popen
from enaml.core.enaml_compiler import COMPILER_VERSION
from enaml.core.import_hooks import CACHEDIR
sys.path.insert(1, '/usr/share/python3/')
from debpython.version import SUPPORTED, debsorted, vrepr, \
    get_requested_versions, getver
from debpython import files as dpf, PUBLIC_DIR_RE
from debpython.interpreter import Interpreter
from debpython.option import Option

# initialize script
logging.basicConfig(format='%(levelname).1s: %(module)s:%(lineno)d: '
                           '%(message)s')
log = logging.getLogger(__name__)
STDINS = {}
WORKERS = {}


def filter_files(files, compile_versions):
    """Generate (file, versions_to_compile) pairs."""
    valid_versions = set(compile_versions)  # all by default

    for fpath in files:
        public_dir = PUBLIC_DIR_RE.match(fpath)
        if public_dir and len(public_dir.group(1)) != 1:
            yield fpath, set([getver(public_dir.group(1))])
        else:
            yield fpath, valid_versions


### COMPILE ####################################################
def enaml_compile(version, workers):
    if not isinstance(version, str):
        version = vrepr(version)
    cmd = "/usr/bin/python%s -m enaml.compile -" % version
    process = Popen(cmd, bufsize=0, shell=True,
                    stdin=PIPE, close_fds=True)
    workers[version] = process  # keep the reference for .communicate()
    stdin = process.stdin
    while True:
        filename = (yield)
        stdin.write(filename.encode('utf-8') + b'\n')


def compile(files, versions, force):
    global STDINS, WORKERS
    # start Python interpreters that will handle byte compilation
    for version in versions:
        if version not in STDINS:
            coroutine = enaml_compile(version, WORKERS)
            next(coroutine)
            STDINS[version] = coroutine

    interpreter = Interpreter('python')

    # byte compile files
    skip_dirs = set()
    for fn, versions_to_compile in filter_files(files, versions):
        for version in versions_to_compile:
            if not force:
                magic_tag = 'enaml-%s-cv%s' % (
                    interpreter.magic_tag(version), COMPILER_VERSION)
                root, tail = os.path.split(fn)
                fnroot = splitext(tail)[0]
                cfn = '%s.%s%s%s' % (fnroot, magic_tag, os.path.extsep, 'enamlc')
                cpath = join(root, CACHEDIR, cfn)

                if exists(cpath):
                    continue
            pipe = STDINS[version]
            pipe.send(fn)


################################################################
def main():
    usage = '%prog [-V VERSION_RANGE] DIR_OR_FILE\n' +\
        '       %prog -p PACKAGE'
    parser = optparse.OptionParser(usage, option_class=Option)
    parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
                      help='turn verbose mode on')
    parser.add_option('-q', '--quiet', action='store_false', dest='verbose',
                      default=False, help='be quiet')
    parser.add_option('-f', '--force', action='store_true', dest='force',
                      default=False,
                      help="Force the rebuild of the byte-code files, even "
                      "if the files already exists")
    parser.add_option('-p', '--package',
                      help="Specify Debian package name whose files should "
                      "be bytecompiled")
    parser.add_option('-V', type='version_range', dest='vrange',
                      help="Force Enaml source files from private module "
                      "directories to be bytecompiled with the Python 3 "
                      "versions matching the given range, regardless of the "
                      "default Python 3 version in the system.  "
                      "If there are no other options, bytecompile all Enaml "
                      "source files from public module directories for the "
                      "installed Python 3 versions that match the given "
                      "range.  "
                      "VERSION_RANGE examples: '3.1' (version 3.1 only),  "
                      "'3.1-' (version 3.1 or newer),  '3.1-3.3' (version "
                      "3.1 or 3.2),  '-4.0' (all supported 3.X versions)")

    (options, args) = parser.parse_args()

    if options.verbose or environ.get('ENAMLCOMPILE_DEBUG') == '1':
        log.setLevel(logging.DEBUG)
        log.debug('argv: %s', sys.argv)
        log.debug('options: %s', options)
        log.debug('args: %s', args)
    else:
        log.setLevel(logging.WARN)

    if options.vrange and options.vrange[0] == options.vrange[1] and\
            options.vrange != (None, None) and\
            exists("/usr/bin/python%d.%d" % options.vrange[0]):
        # specific version requested, use it even if it's not in SUPPORTED
        versions = {options.vrange[0]}
    else:
        versions = get_requested_versions(options.vrange, available=True)
    if not versions:
        log.error('Requested versions are not installed')
        exit(3)

    if options.package and args:  # package's private directories
        # get requested Python version
        compile_versions = debsorted(versions)[:1]
        log.debug('compile versions: %s', versions)

        pkg_files = tuple(dpf.from_package(options.package,
                                           extensions=('.enaml',)))
        for item in args:
            if not exists(item):
                log.warning('No such file or directory: %s', item)
            else:
                log.debug('byte compiling %s using Python %s',
                          item, compile_versions)
                files = dpf.filter_directory(pkg_files, item)
                compile(files, compile_versions, options.force)
    elif options.package:  # package's public modules
        # no need to limit versions here, it's either pyr mode or version is
        # hardcoded in path / via -V option
        files = dpf.from_package(options.package, extensions=('.enaml',))
        files = dpf.filter_public(files, versions)
        compile(files, versions, options.force)
    elif args:  # other directories/files
        for item in args:
            files = dpf.from_directory(item, extensions=('.enaml',))
            compile(files, versions, options.force)
    else:
        parser.print_usage()
        exit(1)

    # wait for all processes to finish
    rv = 0
    for process in WORKERS.values():
        process.communicate()
        if process.returncode not in (None, 0):
            rv = process.returncode
    exit(rv)

if __name__ == '__main__':
    main()
