#!/usr/bin/env python2
# gnetlist - wrapper script for invoking the gEDA netlister
# Copyright (C) 1998-2010 Ales Hvezda
# Copyright (C) 1998-2010 gEDA Contributors (see ChangeLog for details)
# Copyright (C) 2013-2019 Roland Lutz
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

## \file gnetlist.in
## Wrapper script for invoking the gEDA netlister.
#
# <tt>xorn netlist</tt> doesn't read the old-style Guile configuration
# files \c gafrc and \c gnetlistrc and doesn't support the
# Guile-specific command line options \c -l, \c -m, and \c -c.
# This wrapper script runs the configuration files, detects the
# Guile-specific command line options and translates them into a
# <tt>xorn netlist</tt> invocation and can be used as a drop-in
# replacement for the old <tt>gnetlist</tt> binary in most situations.
#
# It does *not* read the new-style configuration file \c geda.conf,
# and it does *not* support Guile functions as a symbol library.

pyexecdir = '/usr/lib/python2.7/site-packages'
XORN = '/usr/bin/xorn'
gedadatadir = '/usr/share/gEDA'
gedasysconfdir = '/usr/share/gEDA'
backenddir = '/usr/lib/xorn/backends'

import getopt, gettext, os, sys
from gettext import gettext as _
sys.path.insert(0, pyexecdir)
import xorn.command
import xorn.config
import xorn.guile

APPEND, PREPEND = xrange(2)


## Expand environment variables in a string.
#
# This function returns the passed string with environment variables
# expanded.
#
# The invocations of environment variables MUST be in the form \c
# '${variable_name}'; \c '$variable_name' is not valid here.
# Environment variable names can consist solely of letters, digits and
# \c '_'.  It is possible to escape a \c '$' character in the string
# as \c '$$'.
#
# Prints error messages to stderr and leaves the malformed and bad
# variable names in the returned string.

def expand_env_variables(s):
    result = ''
    i = 0

    while True:
        try:
            # look for next variable name
            j = s.index('$', i)
        except ValueError:
            # no variable left
            result += s[i:]
            return result

        result += s[i:j]

        if j + 1 >= len(s):     # '$' is the last character in the string
            result += '$'
            return result
        if s[j + 1] == '$':     # an escaped '$'
            result += s[j + 1]
            i = j + 2
            continue
        if s[j + 1] != '{':     # an isolated '$', put it in output
            result += '$'
            i = j + 1
            continue

        # discard "${"
        i = j + 2

        # look for the end of the variable name
        try:
            j = s.index('}', i)
        except ValueError:
            # problem: no closing '}' to variable
            sys.stderr.write(_("Found malformed environment variable "
                               "in '%s'\n") % s)
            result += s[i - 2:]  # include "${"
            return result

        # test characters of variable name
        bad_characters = [ch for ch in s[i:j]
                          if not ch.isalnum() and ch != '_']
        if bad_characters:
            # illegal character detected in variable name
            sys.stderr.write(_("Found bad character(s) [%s] "
                               "in variable name.\n") % ''.join(bad_characters))
            result += s[i - 2:j + 1]  # include "${" and "}"
            i = j + 1
            continue

        # extract variable name from string and expand it
        try:
            result += os.environ[s[i:j]]
        except KeyError:
            pass
        i = j + 1

## Helper function for checking the type of an API function argument.
#
# When constructing an error message, \a fun and \a i are used to
# indicate the API function and argument index, respectively.
#
# \throws TypeError if \a arg is not an instance of \a t or of a
#                   subclass thereof

def check_argument_type(fun, i, arg, t):
    if not isinstance(arg, t):
        raise TypeError, '"%s" argument %d must be %s, not %s' % (
            fun, i, t.__name__, arg.__class__.__name__)

## Helper function for checking whether an API function argument is
## callable.
#
# When constructing an error message, \a fun and \a i are used to
# indicate the API function and argument index, respectively.
#
# \throws TypeError if \a arg is not callable

def _check_argument_callable(fun, i, arg):
    if not callable(arg):
        raise TypeError, "'%s' argument %d must be callable, " \
                         "but '%s' object is not" % (
            fun, i, arg.__class__.__name__)

# ========================== Configuration options ===========================

# general gafrc options
symbol_library = []
source_library = []

# general gafrc options which are not relevant for netlisting
bitmap_directory = False
bus_ripper_symname = False
attribute_promotion = False
promote_invisible = False
keep_invisible = False
always_promote_attributes = False
make_backup_files = False

# gnetlistrc options
hierarchy_refdes_mangle = True
hierarchy_netname_mangle = True
hierarchy_netattrib_mangle = True
hierarchy_refdes_separator = '/'
hierarchy_netname_separator = '/'
hierarchy_netattrib_separator = '/'
hierarchy_refdes_order = APPEND
hierarchy_netname_order = APPEND
hierarchy_netattrib_order = APPEND

# deprecated gnetlistrc options
prefer_netname_attribute = False
default_net_name = 'unnamed_net'
default_bus_name = 'unnamed_bus'
traverse_hierarchy = True

# -------------------------- RC functions: general ---------------------------

## Add a directory to the Guile load path.
#
# Prepends \a path to the Guile system \c '%load-path', after
# expanding environment variables.
#
# \param [in] path  path to be added
#
# \returns \c True

def rc_scheme_directory(path):
    check_argument_type('scheme-directory', 1, path, basestring)

    # take care of any shell variables
    path = expand_env_variables(path)

    load_path = list(xorn.guile.lookup('%load-path'))
    load_path.insert(0, path)
    xorn.guile.define('%load-path', load_path)

    return True

# \returns \c True

def rc_world_size(width, height, border):
    return True  # not implemented

def rc_print_color_map(map = None):
    if map is None:
        raise NotImplementedError

def rc_gnetlist_version(version):
    check_argument_type('gnetlist-version', 1, version, basestring)
    return False  # this is Xorn, so the versions never match

# ------------------------- RC functions: libraries --------------------------

# \param [in] name  optional descriptive name for library directory
#                   \e (ignored)
#
# \returns \c True on success, \c False otherwise

def rc_component_library(path, name = None):
    check_argument_type('component-library', 1, path, basestring)
    if name is not None:
        check_argument_type('component-library', 2, name, basestring)

    # take care of any shell variables
    path = expand_env_variables(path)

    # invalid path?
    if not os.path.isdir(path):
        sys.stderr.write(
            _("Invalid path [%s] passed to component-library\n") % path)
        return False

    if os.path.isabs(path):
        symbol_library.extend(['--symbol-library', path])
    else:
        symbol_library.extend(['--symbol-library',
                               os.path.join(os.getcwd(), path)])

    return True

# \param [in] prefix  optional prefix for library names \e (ignored)
#
# \returns \c True on success, \c False otherwise

def rc_component_library_search(path, prefix = None):
    check_argument_type('component-library-search', 1, path, basestring)
    if prefix is not None:
        check_argument_type('component-library-search', 2, prefix, basestring)

    # take care of any shell variables
    path = expand_env_variables(path)

    # invalid path?
    if not os.path.isdir(path):
        sys.stderr.write(
            _("Invalid path [%s] passed to component-library-search\n") % path)
        return False

    if os.path.isabs(path):
        symbol_library.extend(['--symbol-library-search', path])
    else:
        symbol_library.extend(['--symbol-library-search',
                               os.path.join(os.getcwd(), path)])

    return True

## Add a library command.
#
# Add a command to the component library.
#
# \param [in] listcmd  command to get a list of symbols
# \param [in] getcmd   command to get a symbol from the library
# \param [in] name     optional descriptive name for component source
#
# \returns \c True

def rc_component_library_command(listcmd, getcmd, name):
    check_argument_type('component-library-command', 1, listcmd, basestring)
    check_argument_type('component-library-command', 2, getcmd, basestring)
    check_argument_type('component-library-command', 3, name, basestring)

    # take care of any shell variables
    # \bug this may be a security risk!
    listcmd = expand_env_variables(listcmd)
    getcmd = expand_env_variables(getcmd)

    symbol_library.extend(['--symbol-library-command',
                           '%s:%s:%s' % (listcmd, getcmd, name)])
    return True

## Add a library function.
#
# Add a set of Guile procedures for listing and generating symbols.
#
# \param [in] listfunc  a procedure which takes no arguments and
#                       returns a list of component names
# \param [in] getfunc   a procedure which takes a component name as an
#                       argument and returns a symbol encoded in a
#                       string in gEDA format, or \c False if the
#                       component name is unknown
# \param [in] name      a descriptive name for this component source
#
# \returns \c True on success, \c False otherwise
#
# \warning This function is not implemented.

def rc_component_library_funcs(listfunc, getfunc, name):
    check_argument_callable('component-library-funcs', 1, listfunc)
    check_argument_callable('component-library-funcs', 2, getfunc)
    check_argument_type('component-library-funcs', 3, name, basestring)

    raise NotImplementedError

# \returns \c True

def rc_reset_component_library():
    del symbol_library[:]
    return True

# \returns \c True on success, \c False otherwise

def rc_source_library(path):
    check_argument_type('source-library', 1, path, basestring)

    # take care of any shell variables
    path = expand_env_variables(path)

    # invalid path?
    if not os.path.isdir(path):
        sys.stderr.write(
            _("Invalid path [%s] passed to source-library\n") % path)
        return False

    if os.path.isabs(path):
        source_library.extend(['--source-library', path])
    else:
        source_library.extend(['--source-library',
                               os.path.join(os.getcwd(), path)])

    return True

# \returns \c True

def rc_reset_source_library():
    del source_library[:]
    return True

# ----------------------------------------------------------------------------

# \returns \c True on success, \c False otherwise

def rc_bitmap_directory(path):
    check_argument_type('bitmap-directory', 1, path, basestring)

    # take care of any shell variables
    path = expand_env_variables(path)

    # invalid path?
    if not os.path.isdir(path):
        sys.stderr.write(
            _("Invalid path [%s] passed to bitmap-directory\n") % path)
        return False

    global bitmap_directory
    bitmap_directory = path

    return True

# \returns \c True

def rc_bus_ripper_symname(symname):
    check_argument_type('bus-ripper-symname', 1, symname, basestring)

    global bus_ripper_symname
    bus_ripper_symname = symname

    return True

def rc_attribute_promotion(mode):
    check_argument_type('attribute-promotion', 1, mode, basestring)
    try:
        global attribute_promotion
        attribute_promotion = { 'enabled': True, 'disabled': False }[mode]
        return True
    except KeyError:
        sys.stderr.write(
            _("Invalid mode [%s] passed to attribute-promotion\n") % mode)
        return False

def rc_promote_invisible(mode):
    check_argument_type('promote-invisible', 1, mode, basestring)
    try:
        global promote_invisible
        promote_invisible = { 'enabled': True, 'disabled': False }[mode]
        return True
    except KeyError:
        sys.stderr.write(
            _("Invalid mode [%s] passed to promote-invisible\n") % mode)
        return False

def rc_keep_invisible(mode):
    check_argument_type('keep-invisible', 1, mode, basestring)
    try:
        global keep_invisible
        keep_invisible = { 'enabled': True, 'disabled': False }[mode]
        return True
    except KeyError:
        sys.stderr.write(
            _("Invalid mode [%s] passed to keep-invisible\n") % mode)
        return False

# \returns \c True

def rc_always_promote_attributes(attrlist):
    global always_promote_attributes

    if isinstance(attrlist, basestring):
        sys.stderr.write(
            _("WARNING: using a string for 'always-promote-attributes' is "
              "deprecated. Use a list of strings instead\n"))

        # convert the space separated strings into a list
        always_promote_attributes = [
            attr for attr in attrlist.split(' ') if attr]
    else:
        check_argument_type('always-promote-attributes', 1, attrlist, tuple)

        for i, attr in enumerate(attrlist):
            if not isinstance(attr, basestring):
                raise TypeError, "'always-promote-attributes' argument 1 " \
                                 "index %d must be basestring, not %s" % (
                    i, attr.__class__.__name__)

        always_promote_attributes = list(attrlist)

    return True

## Enable the creation of backup files when saving.
#
# If enabled then a backup file, of the form \c "example.sch~", is
# created when saving a file.
#
# \param [in] mode  \c "enabled" or \c "disabled"
#
# \returns \c False if \a mode is not a valid mode, \c True if it is

def rc_make_backup_files(mode):
    check_argument_type('make-backup-files', 1, mode, basestring)
    try:
        global make_backup_files
        make_backup_files = { 'enabled': True, 'disabled': False }[mode]
        return True
    except KeyError:
        sys.stderr.write(
            _("Invalid mode [%s] passed to make-backup-files\n") % mode)
        return False

# ----------------------------------------------------------------------------

def rc_hierarchy_uref_mangle(mode):
    check_argument_type('hierarchy-uref-mangle', 1, mode, basestring)
    try:
        global hierarchy_refdes_mangle
        hierarchy_refdes_mangle = { 'enabled': True, 'disabled': False }[mode]
        return True
    except KeyError:
        sys.stderr.write(_("Invalid mode [%s] passed to %s\n")
                           % (mode, 'hierarchy-uref-mangle'))
        return False

def rc_hierarchy_netname_mangle(mode):
    check_argument_type('hierarchy-netname-mangle', 1, mode, basestring)
    try:
        global hierarchy_netname_mangle
        hierarchy_netname_mangle = { 'enabled': True, 'disabled': False }[mode]
        return True
    except KeyError:
        sys.stderr.write(_("Invalid mode [%s] passed to %s\n")
                           % (mode, 'hierarchy-netname-mangle'))
        return False

def rc_hierarchy_netattrib_mangle(mode):
    check_argument_type('hierarchy-netattrib-mangle', 1, mode, basestring)
    try:
        global hierarchy_netattrib_mangle
        hierarchy_netattrib_mangle = {
            'enabled': True, 'disabled': False }[mode]
        return True
    except KeyError:
        sys.stderr.write(_("Invalid mode [%s] passed to %s\n")
                           % (mode, 'hierarchy-netattrib-mangle'))
        return False

def rc_hierarchy_uref_separator(name):
    check_argument_type('hierarchy-uref-separator', 1, name, basestring)

    global hierarchy_refdes_separator
    hierarchy_refdes_separator = name

    return True

def rc_hierarchy_netname_separator(name):
    check_argument_type('hierarchy-netname-separator', 1, name, basestring)

    global hierarchy_netname_separator
    hierarchy_netname_separator = name

    return True

def rc_hierarchy_netattrib_separator(name):
    check_argument_type('hierarchy-netattrib-separator', 1, name, basestring)

    global hierarchy_netattrib_separator
    hierarchy_netattrib_separator = name

    return True

def rc_hierarchy_netattrib_order(mode):
    check_argument_type('hierarchy-netattrib-order', 1, mode, basestring)
    try:
        global hierarchy_netattrib_order
        hierarchy_netattrib_order = {
            'prepend': PREPEND, 'append': APPEND }[mode]
        return True
    except KeyError:
        sys.stderr.write(_("Invalid mode [%s] passed to %s\n")
                           % (mode, 'hierarchy-netattrib-order'))
        return False

def rc_hierarchy_netname_order(mode):
    check_argument_type('hierarchy-netname-order', 1, mode, basestring)
    try:
        global hierarchy_netname_order
        hierarchy_netname_order = {
            'prepend': PREPEND, 'append': APPEND }[mode]
        return True
    except KeyError:
        sys.stderr.write(_("Invalid mode [%s] passed to %s\n")
                           % (mode, 'hierarchy-netname-order'))
        return False

def rc_hierarchy_uref_order(mode):
    check_argument_type('hierarchy-uref-order', 1, mode, basestring)
    try:
        global hierarchy_refdes_order
        hierarchy_refdes_order = { 'prepend': PREPEND, 'append': APPEND }[mode]
        return True
    except KeyError:
        sys.stderr.write(_("Invalid mode [%s] passed to %s\n")
                           % (mode, 'hierarchy-uref-order'))
        return False

# ----------------------------------------------------------------------------

def rc_unnamed_netname(name):
    check_argument_type('unnamed-netname', 1, name, basestring)
    global default_net_name
    default_net_name = name

def rc_unnamed_busname(name):
    check_argument_type('unnamed-busname', 1, name, basestring)
    global default_bus_name
    default_bus_name = name

def rc_hierarchy_traversal(mode):
    check_argument_type('hierarchy-traversal', 1, mode, basestring)
    global traverse_hierarchy
    traverse_hierarchy = mode == 'enabled'

def rc_net_naming_priority(mode):
    check_argument_type('net-naming-priority', 1, mode, basestring)
    global prefer_netname_attribute
    prefer_netname_attribute = mode == 'netname'

# ============================================================================

## Path to gEDA data shared between all users.
#
# If the \c GEDADATA environment variable is set, this value is used;
# otherwise, the configured path is used.

if 'GEDADATA' in os.environ and os.environ['GEDADATA']:
    sys_data_path = os.environ['GEDADATA']
else:
    sys_data_path = gedadatadir

## Path to gEDA configuration shared between all users.
#
# If the \c GEDADATARC environment variable is set, this value is
# used; otherwise, the configured path is used.  Finally fallback to
# using the system data path.

if 'GEDADATARC' in os.environ and os.environ['GEDADATARC']:
    sys_config_path = os.environ['GEDADATARC']
elif gedasysconfdir:
    sys_config_path = gedasysconfdir
else:
    sys_config_path = sys_data_path

## Directory with the gEDA user configuration.
#
# Path to be searched for the current user's gEDA configuration.
# Currently defaults to a directory \c ".gEDA" in the user's home
# directory.

user_config_path = os.path.join(os.environ['HOME'], '.gEDA')

# ----------------------------------------------------------------------------

## Implement the following functions to add support for geda.conf files.

def eda_config_is_loaded(context):
    return False

def eda_config_load(context):
    return False

def eda_config_get_filename(context):
    return False

def eda_config_scm_from_config(context):
    return False

def eda_config_get_system_context():
    return False

def eda_config_get_user_context():
    return False

def eda_config_get_context_for_path(path):
    return False

def eda_config_get_context_for_file(path):
    return False

# ----------------------------------------------------------------------------

## Load an RC file.
#
# Loads and runs the Scheme initialisation file \a name.
#
# \param name     filename of the RC file to load
# \param context  configuration context to use while loading

def rc1_parse_file(name, context):
    # skip missing files, directories etc.
    if not os.path.isfile(name):
        return

    # If the configuration wasn't loaded yet, attempt to load it.
    # Config loading is on a best-effort basis; if we fail, just print
    # a warning.

    if not eda_config_is_loaded(context):
        eda_config_load(context)

    # Normalizing filenames is hard to do right; don't even try it.
    #name = f_normalize_filename(name)

    if '"' in name or '\\' in name:
        sys.stderr.write(
            _("ERROR: Illegal character in RC file name: %s\n") % name)
        sys.exit(1)

    # Attempt to load the RC file, if it hasn't been loaded already.
    try:
        xorn.guile.eval_string(
            '(with-fluids'
            ' ((rc-filename-fluid "%s")'
            '  (rc-config-fluid (eda-config-scm-from-config %s)))'
            ' (primitive-load "%s"))' % (name, '#f', name))
    except xorn.guile.GuileError:
        sys.stderr.write(_("Failed to load RC file [%s]\n") % name)
        sys.exit(1)

    #sys.stderr.write(_("Loaded RC file [%s]\n") % name)

## Return a list of RC file names and associated configuration
## contexts.
#
# Returns a list of pairs <tt>(name, context)</tt> for each Scheme
# initialisation files to process: system, user and local (current
# working directory), first with the default \c "gafrc" basename and
# then with the basename \a rcname, if \a rcname is not \c None;
# additionally, \a rcfile if \a rcfile is not \c None.
#
# \param rcname  RC file basename, or \c None
# \param rcfile  specific RC file path, or \c None

def rc1_files(rcname, rcfile):
    # Load RC files in order.
    result = [
        (os.path.join(sys_config_path, 'system-gafrc'),
         eda_config_get_system_context()),
        (os.path.join(user_config_path, 'gafrc'),
         eda_config_get_user_context()),
        ('gafrc', eda_config_get_context_for_path('gafrc'))]

    # Next application-specific rcname.
    if rcname is not None:
        result += [
            (os.path.join(sys_config_path, 'system-' + rcname),
             eda_config_get_system_context()),
            (os.path.join(user_config_path, rcname),
             eda_config_get_user_context()),
            (rcname, eda_config_get_context_for_path(rcname))]

    # Finally, optional additional RC file.  Specifically use the
    # current working directory's configuration context here, no matter
    # where the rc file is located on disk.
    if rcfile is not None:
        result.append((rcfile, eda_config_get_context_for_file(False)))

    return result

## General RC file parsing function.
#
# Attempt to load and run system, user and local (current working
# directory) Scheme initialisation files, first with the default \c
# "gafrc" basename and then with the basename \a rcname, if \a rcname
# is not \c None.  Additionally, attempt to load and run \a rcfile if
# \a rcfile is not \c None.
#
# \param rcname  RC file basename, or \c None
# \param rcfile  specific RC file path, or \c None

def rc1_parse(rcname, rcfile):
    for name, context in rc1_files(rcname, rcfile):
        rc1_parse_file(name, context)

        # Re-export function component-library-search (this is
        # overwritten by geda.scm)
        xorn.guile.define('component-library-search',
                          rc_component_library_search)

# ----------------------------------------------------------------------------

for name, value in {
        'scheme-directory': rc_scheme_directory,
        'world-size': rc_world_size,
        'print-color-map': rc_print_color_map,
        'gnetlist-version': rc_gnetlist_version,

        'component-library': rc_component_library,
        'component-library-search': rc_component_library_search,
        'component-library-command': rc_component_library_command,
        'component-library-funcs': rc_component_library_funcs,
        'reset-component-library': rc_reset_component_library,
        'source-library': rc_source_library,
        'reset-source-library': rc_reset_source_library,

        'bitmap-directory': rc_bitmap_directory,
        'bus-ripper-symname': rc_bus_ripper_symname,
        'attribute-promotion': rc_attribute_promotion,
        'promote-invisible': rc_promote_invisible,
        'keep-invisible': rc_keep_invisible,
        'always-promote-attributes': rc_always_promote_attributes,
        'make-backup-files': rc_make_backup_files,

        'hierarchy-uref-mangle': rc_hierarchy_uref_mangle,
        'hierarchy-netname-mangle': rc_hierarchy_netname_mangle,
        'hierarchy-netattrib-mangle': rc_hierarchy_netattrib_mangle,
        'hierarchy-uref-separator': rc_hierarchy_uref_separator,
        'hierarchy-netname-separator': rc_hierarchy_netname_separator,
        'hierarchy-netattrib-separator': rc_hierarchy_netattrib_separator,
        'hierarchy-netattrib-order': rc_hierarchy_netattrib_order,
        'hierarchy-netname-order': rc_hierarchy_netname_order,
        'hierarchy-uref-order': rc_hierarchy_uref_order,

        'unnamed-netname': rc_unnamed_netname,
        'unnamed-busname': rc_unnamed_busname,
        'hierarchy-traversal': rc_hierarchy_traversal,
        'net-naming-priority': rc_net_naming_priority,

        'eda-config-is-loaded': eda_config_is_loaded,
        'eda-config-load': eda_config_load,
        'eda-config-get-filename': eda_config_get_filename,
        'eda-config-scm-from-config': eda_config_scm_from_config,
        'eda-config-get-system-context': eda_config_get_system_context,
        'eda-config-get-user-context': eda_config_get_user_context,
        'eda-config-get-context-for-path': eda_config_get_context_for_path,
        'eda-config-get-context-for-file': eda_config_get_context_for_file,
    }.iteritems():
    xorn.guile.define(name, value)

xorn.guile.eval_string('''
(define rc-filename-fluid (make-unbound-fluid))
(define rc-config-fluid (make-unbound-fluid))

;;; Get the filename of the RC file being evaluated.
;;;
;;; \returns the full path to the RC file if the interpreter can
;;;          resolve the filename, otherwise throws an error

(define (rc-filename) (fluid-ref rc-filename-fluid))

;;; Get a configuration context for the current RC file.
;;;
;;; Returns the configuration context applicable to the RC file being
;;; evaluated.  This function is intended to support gEDA transition
;;; from functions in RC files to static configuration files.
;;;
;;; \returns an EdaConfig smob

(define (rc-config) (fluid-ref rc-config-fluid))
''')

# ----------------------------------------------------------------------------

## Get a sorted list of available backends.
#
# Returns a list of available netlister backends by searching for
# files in each of the directories in the current Guile \c %load-path.
# A file is considered to be a netlister backend if its name begins
# with \c "gnet-" and ends with \c ".scm".

def list_backends():
    # Look up the current Guile %load-path
    load_path = xorn.guile.lookup('%load-path')

    backend_names = set()

    # add-to-load-path adds paths twice (once when it is compiled and
    # once when it is run), so we need to filter out duplicate paths.
    visited_dirnames = set()

    for dir_name in load_path:
        if dir_name in visited_dirnames:
            continue
        visited_dirnames.add(dir_name)

        try:
            d_names = os.listdir(dir_name)
        except OSError as e:
            #sys.stderr.write(_("Can't open directory %s: %s\n")
            #                 % (dir_name, e.strerror))
            continue

        for d_name in d_names:
            # Check that filename has the right format to be a backend
            if d_name.startswith('gnet-') and d_name.endswith('.scm'):
                # Remove prefix & suffix.  Add to list of backend names.
                backend_names.add(d_name[5:-4])

    # Sort the list of backends
    return sorted(backend_names)

def main():
    gettext.bindtextdomain(xorn.config.PACKAGE, xorn.config.localedir)
    gettext.textdomain(xorn.config.PACKAGE)

    try:
        options, args = getopt.getopt(
            xorn.command.args, 'c:g:hil:L:m:o:O:p:qvV', [
                'quiet', 'verbose', 'interactive', 'report-gui',
                'list-backends', 'help', 'version'])
    except getopt.GetoptError as e:
        xorn.command.invalid_arguments(e.msg)

    quiet_mode = False
    verbose_mode = False
    output_filename = 'output.net'
    guile_proc = None           # Guile netlist backend to use
    python_backend = None       # Python netlist backend to use
    interactive_mode = False    # enter interactive Scheme REPL after loading
    backend_options = []        # option strings passed to backend

    evaluate_at_startup = []
    extra_load_paths = [os.path.join(sys_data_path, 'scheme')]
    load_before_backend = []
    load_after_backend = []

    report_gui = False
    list_backends_ = False

    for option, value in options:
        if option == '-q' or option == '--quiet':
            quiet_mode = True
        elif option == '-v' or option == '--verbose':
            verbose_mode = True
        elif option == '-o':
            output_filename = value
        elif option == '-L':
            # Argument is a directory to add to the Scheme load path
            extra_load_paths.append(value)
        elif option == '-g':
            guile_proc = value
        elif option == '-p':
            python_backend = value.replace('-', '_')
        elif option == '-O':
            backend_options.append(value)
        elif option == '-l':
            # Argument is filename of a Scheme script to be run before
            # loading gnetlist backend
            load_before_backend.append(value)
        elif option == '-m':
            # Argument is filename of a Scheme script to be run after
            # loading gnetlist backend
            load_after_backend.append(value)
        elif option == '-c':
            # Evaluate Scheme expression at startup
            evaluate_at_startup.append(value)
        elif option == '-i' or option == '--interactive':
            interactive_mode = True
        elif option == '--report-gui':
            report_gui = True
        elif option == '--list-backends':
            list_backends_ = True

        elif option == '-h' or option == '--help':
            sys.stdout.write('''\
Usage: %s [OPTION]... -g BACKEND [--] FILE...
       %s [OPTION]... -i [--] FILE...
Generate a netlist from one or more gEDA schematic files.

  -q                    quiet mode
  -v, --verbose         verbose mode
  -o FILE               filename for netlist data output
  -L DIR                add DIR to Python and Scheme search path
  -p BACKEND            specify Python netlist backend to use
  -g BACKEND            specify Scheme netlist backend to use
  -O STRING             pass an option string to backend
  -l FILE               load Scheme file before loading backend
  -m FILE               load Scheme file after loading backend
  -c EXPR               evaluate Scheme expression at startup
  -i                    enter interactive Python interpreter after loading
      --report-gui      report warnings and errors in GUI dialog
      --list-backends   print a list of available netlist backends
  -h, --help            help; this message
  -V, --version         show version information
  --                    treat all remaining arguments as filenames

Report %s bugs to %s
gEDA/gaf homepage: http://www.geda-project.org/
''' % (xorn.command.program_name, xorn.command.program_name,
       xorn.config.PACKAGE_NAME, xorn.config.PACKAGE_BUGREPORT))
            return

        elif option == '-V' or option == '--version':
            sys.stdout.write('''\
gnetlist - gEDA netlister (%s)
Copyright (C) 1998-2019 gEDA developers

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program 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 General Public License for more details.
''' % xorn.config.PACKAGE_STRING)
            return

    if guile_proc is not None and python_backend is not None:
        sys.stderr.write(
            _("Options `-g BACKEND' and `-p BACKEND' are mutually exclusive\n"))
        sys.exit(1)

    if guile_proc is None and python_backend is None \
            and not interactive_mode and not list_backends_:
        sys.stderr.write(
            _("You gave neither backend to execute nor interactive mode!\n"))
        sys.exit(1)

    # --------------------------------------------------------------------

    # 1. Evaluate expressions passed as `-c' options
    for value in evaluate_at_startup:
        try:
            xorn.guile.eval_string(value)
        except xorn.guile.GuileError:
            sys.stderr.write(_("An error occurred while evaluating a "
                               "`-c' option:\n%s\n") % value)
            sys.exit(1)

    # 2. Add paths passed as `-L' options to the Guile load path.
    for extra_load_path in extra_load_paths:
        if '"' in extra_load_path or '\\' in extra_load_path:
            sys.stderr.write(_("Illegal character in extra load path:\n"
                               "  %s\n") % extra_load_path)
            sys.exit(1)
        xorn.guile.eval_string('(add-to-load-path "%s")' % extra_load_path)

    # 3. Load RC files
    if '"' in sys_data_path or '\\' in sys_data_path:
        sys.stderr.write(_("ERROR: Illegal character in "
                           "system data path: %s\n") % sys_data_path)
        sys.exit(1)

    if '"' in sys_config_path or '\\' in sys_config_path:
        sys.stderr.write(_("ERROR: Illegal character in "
                           "system config path: %s\n") % sys_config_path)
        sys.exit(1)

    xorn.guile.eval_string('''
(define (eval-protected exp)
  (eval exp (interaction-environment)))
    ''')

    xorn.guile.eval_string('''
(define-module (geda core os))
(define-public (%%platform) '(linux))
(define-public (%%sys-data-dirs) '("%s"))
(define-public (%%sys-config-dirs) '("%s"))
(define-public (%%user-data-dir) "%s")
(define-public (%%user-config-dir) "%s")
#f
''' % (sys_data_path, sys_config_path, user_config_path, user_config_path))

    # prevent loading of actual "geda deprecated" module
    xorn.guile.eval_string('(define-module (geda deprecated)) #f')

    xorn.guile.define('use-legacy-frontend', False)
    rc1_parse('gnetlistrc', None)

    # 4. Print a list of available netlist backends if requested.
    if list_backends_:
        import gaf.netlist.backend
        gaf.netlist.backend.load_path.insert(0, backenddir)
        for extra_load_path in extra_load_paths[1:]:
            gaf.netlist.backend.load_path.insert(0, extra_load_path)

        python_backends = set(name.replace('_', '-')
                              for name in gaf.netlist.backend.list_backends())
        guile_backends = set(list_backends())

        sys.stdout.write(_("List of available backends:\n\n"))
        for name in sorted(set.union(guile_backends, python_backends)):
            sys.stdout.write("  [%s%s]\t%s\n" % (
                'P' if name in python_backends else ' ',
                'G' if name in guile_backends else ' ', name))
        sys.stdout.write("\n")
        sys.exit(0)

    # 5. Evaluate the first set of Scheme expressions before we load
    #    any schematic files
    guile_sources = load_before_backend[:]

    # 6. Load basic gnetlist functions
    search_load_path = xorn.guile.lookup('%search-load-path')
    fn = search_load_path('gnetlist.scm')
    if fn is False:
        sys.stderr.write(_(
            "ERROR: Could not find `gnetlist.scm' in load path.\n"))
        sys.exit(1)
    guile_sources.append(fn)

    # Anything up to and including the `-m' options may influence the
    # library paths, but loading some backends (systemc, verilog) at
    # this point causes trouble.  So don't run backend and `-m'
    # options here and hope neither sets any vital paths.
    for guile_source in guile_sources:
        try:
            xorn.guile.load(guile_source)
        except xorn.guile.GuileError:
            sys.stderr.write(_("Error loading \"%s\"\n") % guile_source)
            sys.exit(1)

    if guile_proc is not None:
        # 7. Load backend code
        fn = search_load_path('gnet-%s.scm' % guile_proc)
        if fn is False:
            sys.stderr.write(_(
                "ERROR: Could not find backend `%s' in load path.\n\n"
                "Run `%s --list-backends' for a full list of available "
                  "backends.\n") % (guile_proc, xorn.command.program_name))
            sys.exit(1)
        guile_sources.append(fn)

        # 8. Evaluate second set of Scheme expressions
        guile_sources += load_after_backend
        # `-m' options aren't executed unless `-g' is specified

    # 9. Run post-traverse code
    fn = search_load_path('gnetlist-post.scm')
    if fn is False:
        sys.stderr.write(_(
            "ERROR: Could not find `gnetlist-post.scm' in load path.\n"))
        sys.exit(1)
    guile_sources.append(fn)

    # --------------------------------------------------------------------

    cmd = [XORN, 'netlist'] + symbol_library + source_library

    if prefer_netname_attribute:
        cmd.append('--net-naming-priority=netname-attribute')
    if default_net_name != 'unnamed_net':
        cmd.append('--default-net-name=' + default_net_name)
    if default_bus_name != 'unnamed_bus':
        cmd.append('--default-bus-name=' + default_bus_name)
    if not traverse_hierarchy:
        cmd.append('--dont-traverse-hierarchy')
    if not hierarchy_refdes_mangle:
        cmd.append('--hierarchy-refdes-mangle=disabled')
    if not hierarchy_netname_mangle:
        cmd.append('--hierarchy-netname-mangle=disabled')
    if not hierarchy_netattrib_mangle:
        cmd.append('--hierarchy-netattrib-mangle=disabled')
    if hierarchy_refdes_separator != '/':
        cmd.append(
            '--hierarchy-refdes-separator=' + hierarchy_refdes_separator)
    if hierarchy_refdes_order != APPEND:
        cmd.append('--hierarchy-refdes-order=prepend')
    if hierarchy_netname_separator != '/':
        cmd.append(
            '--hierarchy-netname-separator=' + hierarchy_netname_separator)
    if hierarchy_netname_order != APPEND:
        cmd.append('--hierarchy-netname-order=prepend')

    if python_backend is None:
        cmd += ['-g', 'guile']

        for value in evaluate_at_startup:
            cmd += ['-O', 'eval=' + value]
        for value in extra_load_paths:
            cmd += ['-O', 'add-to-load-path=' + value]
        for value in guile_sources:
            cmd += ['-O', 'load=' + value]

        if guile_proc is not None and not interactive_mode:
            # if both `-i' and `-g' are specified, the backend is
            # loaded but not executed (don't ask me, that's what the
            # old gnetlist code does)
            cmd += ['-O', 'guile-proc=' + guile_proc]

            if guile_proc.startswith('spice'):
                cmd += ['-O', 'use-spice-netnames']

        if verbose_mode:
            cmd += ['-O', 'verbosity=1']
        elif quiet_mode:
            cmd += ['-O', 'verbosity=-1']

        if backend_options:
            cmd += ['-O', '--']
        for backend_option in backend_options:
            cmd += ['-O', backend_option]
    else:
        for extra_load_path in extra_load_paths[1:]:
            cmd += ['-L', extra_load_path]
        cmd += ['-g', python_backend]
        for backend_option in backend_options:
            cmd += ['-O', backend_option]

    if guile_proc is not None and guile_proc.startswith('drc'):
        cmd.append('--ignore-errors')

    if guile_proc == 'pcbfwd' or report_gui:
        cmd.append('--report-gui')

    if not quiet_mode:
        # gnetlist prints schematic names unless `-q' has been specified
        cmd.append('-v')

    if interactive_mode:
        cmd += ['-i']

    if output_filename != '-':
        cmd += ['-o', output_filename]

    cmd += args

    if verbose_mode:
        sys.stderr.write('%s\n' % ' '.join(cmd))

    os.execv(cmd[0], cmd)

if __name__ == '__main__':
    main()
