#!/usr/bin/env python
#
# Copyright (C) 2011-2014 ABINIT Group (Yann Pouillon)
#
# This file is part of the ABINIT software package. For license information,
# please see the COPYING file in the top-level directory of the ABINIT source
# distribution.
#

from __future__ import print_function

try:
    from configparser import ConfigParser,NoOptionError
except ImportError:
    from ConfigParser import ConfigParser,NoOptionError
from time import gmtime,strftime

import subprocess
import os
import re
import sys

class MyConfigParser(ConfigParser):

  def optionxform(self,option):
    return str(option)

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

#
# Subroutines
#

# Macro file template
def macro_template(name,stamp):

  return """# Generated by %s on %s

#
# ABINIT fallback support for the "configure" script
#

#
# IMPORTANT NOTE
#
# This file has been automatically generated by the %s
# script. If you try to edit it, your changes will systematically be
# overwritten.
#
@FALLBACKS@



# AFB_SETUP_PACKAGES()
# --------------------
#
# Initializes all fallbacks in the correct order.
#
AC_DEFUN([AFB_SETUP_PACKAGES],[@FBK_CALLS@
  afb_pkg_list="@FBK_LIST@"
  AC_SUBST(afb_pkg_list)
]) # AFB_SETUP_PACKAGES
""" % (name,stamp,name)



# Snippet for environment variables
def snippet_envvars(pkg_langs,lang_specs,group="fallbacks"):

  # Init
  ret = ""
  if ( group == "fallbacks" ):
    suffix = "_@PKG_NICK@"
    docstr = " of @pkg_nick@"
  else:
    suffix = ""
    docstr = ""

  # Create environment variables
  for lang in lang_specs["languages"][group]:
    for stage in lang_specs[lang]["envs"]:
      if ( (lang in pkg_langs) and lang_specs[lang][stage] ):
        for item in lang_specs[lang][stage]:
          ret += "\n  AC_ARG_VAR([%s%s],\n" % (item,suffix)
          ret += "    [%s%s])\n" % (lang_specs["descriptions"][item],docstr)
          ret += "  AC_SUBST(%s%s)\n" % (item,suffix)
          if ( group == "fallbacks" ):
            ret += "  if test -z \"${%s%s}\"; then\n" % (item,suffix)
            ret += "    %s%s=\"${%s}\"\n" % (item,suffix,item)
            ret += "    afb_@pkg_nick@_%s_custom=\"no\"\n" % item.lower()
            ret += "  else\n"
            ret += "    afb_@pkg_nick@_custom=\"yes\"\n"
            ret += "    afb_@pkg_nick@_%s_custom=\"yes\"\n" % item.lower()
            ret += "  fi"
      else:
        if ( lang_specs[lang][stage] ):
          for item in lang_specs[lang][stage]:
            ret += "\n  unset %s%s" % (item,suffix)

  return ret



# Individual macro template
def macro_fallback(nick,name,desc,libs,bins,md5s,urls,deps=None,
  dep_ranks=None,include=None,tricks="[${afb_fc_vendor}],[${afb_fc_version}]",
  is_preq=False,env_vars="",chk_api=None):

  # Create template
  ret = """


# _AFB_SETUP_@PKG_NICK@()
# -------------%s
#
# Sets all variables needed to handle the @PKG_NICK@ fallback.
#
AC_DEFUN([_AFB_SETUP_@PKG_NICK@],[
  dnl Initial setup
  afb_@pkg_nick@_bins=""
  afb_@pkg_nick@_incs=""
  afb_@pkg_nick@_libs=""
  afb_@pkg_nick@_custom="no"
  afb_@pkg_nick@_tricks="unknown"
  afb_@pkg_nick@_ok="unknown"
  afb_@pkg_nick@_tarball="${abinit_tardir}/@PKG_NAME@.tar.gz"
  tmp_tarball_ok="no"@SET_VARS@

  dnl Define variables needed to build the package
  @pkg_nick@_pkg_name="@PKG_NAME@"
  AC_SUBST(@pkg_nick@_pkg_name)
  @pkg_nick@_pkg_inst="@PKG_INST@"
  AC_SUBST(@pkg_nick@_pkg_inst)
  @pkg_nick@_pkg_string="@PKG_DESC@"
  AC_SUBST(@pkg_nick@_pkg_string)

  dnl Define options associated to the package
  AC_ARG_ENABLE([@pkg_opt@],
    AC_HELP_STRING([--disable-@pkg_opt@],
      [Disable @PKG_DESC@ support]))
  AC_SUBST(enable_@pkg_nick@)@WITH_OPTS@

  dnl Make sure all required options are set
  if test "${enable_@pkg_nick@}" = ""; then
    enable_@pkg_nick@="yes"
  fi

  dnl Define and set environment variables for the package@ENV_VARS@

  dnl Check whether to activate fallback
  AC_MSG_CHECKING([whether to enable the @PKG_NICK@ fallback])
  AC_MSG_RESULT([${enable_@pkg_nick@}])
  if test "${enable_@pkg_nick@}" = "yes"; then@CHK_DEPS@

    dnl Adjust build flags to the actual situation@BINS_SPECS@

    dnl Get the package
    for dl_url in LOCAL @PKG_URLS@; do
      if test "${tmp_tarball_ok}" = "no"; then
        if test "${dl_url}" != "LOCAL"; then
          rm -f "${afb_@pkg_nick@_tarball}"
          AC_MSG_NOTICE([downloading @PKG_NICK@ - this may take a while])
          ${afb_downloader} ${afb_dlopts} \\
            "${afb_@pkg_nick@_tarball}" \\
            "${dl_url}"
        fi
        if test -s "${afb_@pkg_nick@_tarball}"; then
          AFB_CHECK_MD5SUM([${afb_@pkg_nick@_tarball}],[@MD5SUM@])
          case "${afb_md5_ok}" in
            yes)
              tmp_tarball_ok="yes"
            ;;
            unknown)
              tmp_tarball_ok="yes"
              AC_MSG_WARN([could not check integrity of @PKG_NAME@ tarball])
              ;;
            no)
              AC_MSG_WARN([@PKG_NAME@ tarball MD5 check failed])
              ;;
          esac
        fi
      fi
    done

    dnl Enable fallback support only if the download was successful
    if test "${tmp_tarball_ok}" = "yes"; then
      afb_@pkg_nick@_ok="yes"@PKG_SPECS@
      AFB_TRICKS_@PKG_NICK@(@TRICKS@)
    else
      afb_@pkg_nick@_ok="no"
      AC_MSG_ERROR([could not download @PKG_NICK@ fallback tarball
    Solution: disable support for @PKG_NICK@ or download the tarball
    manually to ${abinit_tardir}])
    fi

    dnl Warn user about broken packages
    if test "${afb_@pkg_nick@_ok}" != "yes"; then
      AC_MSG_WARN([@PKG_NICK@ is broken, which will prevent the build of all
                      its possible dependencies])
    fi

@PKG_XFER@  fi

  dnl Inform Automake
  AM_CONDITIONAL([DO_BUILD_@PKG_NICK@],[test "${enable_@pkg_nick@}" = "yes" -a "${afb_@pkg_nick@_ok}" = "yes"])
  AM_CONDITIONAL([DO_TEST_@PKG_NICK@],[test "${enable_@pkg_nick@}" = "yes" -a "${afb_@pkg_nick@_ok}" = "yes"])

  dnl Substitutions
  AC_SUBST(afb_@pkg_nick@_ok)
  AC_SUBST(afb_@pkg_nick@_tarball)
@PKG_SUBST@]) # _AFB_SETUP_@PKG_NICK@
""" % ("-"*len(nick))

  # Fill-in set-up sequence
  set_vars = ""
  if ( libs is None ):
    set_vars += "\n  unset with_%s_libs" % pkg

  # Set include specs
  incs_specs = ""
  if ( include is None ):
    incs_test = ""
    incs_path = ""
    set_vars += "\n  unset with_%s_incs" % pkg
  else:
    incs_test = "\"${with_@pkg_nick@_incs}\" = \"\" -o "
    incs_path = "-I\$(prefix)/include"

  # Check dependencies
  chk_deps = ""
  if ( not deps is None ):
    chk_deps += "\n\n    dnl Check for dependencies"
    for dep in dep_ranks:
      chk_deps += "\n    if test \"${afb_%s_ok}\" = \"no\"; then\n" % dep
      chk_deps += "      AC_MSG_ERROR([@PKG_NICK@ depends on broken %s\n" % dep.upper()
      chk_deps += "        Solution: disable @PKG_NICK@ or fix %s])\n" % dep.upper()
      chk_deps += "    fi"
      incs_specs += "  CPPFLAGS_@PKG_NICK@=\"${CPPFLAGS_@PKG_NICK@} ${afb_%s_incs}\"\n\n" % dep

  # Set package specs
  tmp_libs = ""
  pkg_version = re.sub(pkg+"-", "", pkg_name, flags=re.IGNORECASE)
  if ( pkg_version == pkg_name ):
    pkg_version = re.sub(".*-([0-9])", "\\1", pkg_name, flags=re.IGNORECASE)
  pkg_inst = "%s/%s" % (pkg, pkg_version)
  pkg_specs = ""
  pkg_subst = ""
  if ( is_preq ):
    if ( bins ):
      pkg_specs += "\n      afb_@pkg_nick@_bins=\"\\$(prefix)/%s/bin\"" % pkg_inst
      pkg_subst += "  AC_SUBST(afb_@pkg_nick@_bins)\n"
    if ( include ):
      pkg_specs += "\n      afb_@pkg_nick@_incs=\"-I\\$(prefix)/%s/include\"" % pkg_inst
      pkg_subst += "  AC_SUBST(afb_@pkg_nick@_incs)\n"
    if ( libs ):
      for lib in libs:
        if ( re.match("lib.*\.a",lib) ):
          tmp_libs = " -l%s" % (re.sub(r"lib(.*)\.a",r"\1",lib)) + tmp_libs
      pkg_specs += "\n      afb_@pkg_nick@_libs=\"-L\\$(prefix)/%s/lib%s\"" % (pkg_inst,tmp_libs)
      pkg_subst += "  AC_SUBST(afb_@pkg_nick@_libs)\n"

  # Set bin specs
  bins_specs = ""
  if ( bins is None ):
    set_vars += "\n  unset with_%s_bins" % pkg
  elif ( not deps is None ):
    for dep in dep_ranks:
      tmp_libs = ""
      for lib in deps[dep]:
        if ( re.match("lib.*\.a",lib) ):
          tmp_libs = " -l%s" % (re.sub(r"lib(.*)\.a",r"\1",lib)) + tmp_libs
      tmp_libs = "-L\\$(prefix)/lib" + tmp_libs
      bins_specs += "\n    LIBS_@PKG_NICK@=\"${afb_%s_libs} ${LIBS_@PKG_NICK@}\"" % dep
  if ( bins_specs == "" ):
    bins_specs = "\n    dnl Note: nothing to do for @pkg_nick@"

  # Set URL specs
  urls_specs = '"' + '" "'.join(urls) + '"'

  # Define --with-* options
  with_opts = ""
  with_tcon = ""
  with_tdef = ""
  pkg_xfer = ""
  chk_args = ""
  if ( is_preq ):
    if ( chk_api ):
      chk_args = "(%d,%d,%d)" % (chk_api["maj"],chk_api["min"],chk_api["max"])
    if ( bins ):
      with_opts += "\n    AC_ARG_WITH([@pkg_opt@-bins],\n"
      with_opts += "    AC_HELP_STRING([--with-@pkg_opt@-bins],\n"
      with_opts += "      [Path to external %s executables]))\n" % nick
      with_opts += "  AC_SUBST(with_@pkg_nick@_bins)\n"
      with_tcon += " -a \\\n            \"${with_@pkg_nick@_bins}\" = \"\""
      with_tdef += " -o \\\n            \"${with_@pkg_nick@_bins}\" != \"\""
      pkg_xfer += "    afb_@pkg_nick@_bins=\"${with_@pkg_nick@_bins}\"\n"
    if ( include ):
      with_opts += "  AC_ARG_WITH([@pkg_opt@-incs],\n"
      with_opts += "    AC_HELP_STRING([--with-@pkg_opt@-incs],\n"
      with_opts += "      [Include flags for an external %s]))\n" % nick
      with_opts += "  AC_SUBST(with_@pkg_nick@_incs)\n"
      with_tcon += " -a \\\n            \"${with_@pkg_nick@_incs}\" = \"\""
      with_tdef += " -o \\\n            \"${with_@pkg_nick@_incs}\" != \"\""
      pkg_xfer += "    afb_@pkg_nick@_incs=\"${with_@pkg_nick@_incs}\"\n"
    if ( libs ):
      with_opts += "  AC_ARG_WITH([@pkg_opt@-libs],\n"
      with_opts += "    AC_HELP_STRING([--with-@pkg_opt@-libs],\n"
      with_opts += "      [Library flags for an external %s]))\n" % nick
      with_opts += "  AC_SUBST(with_@pkg_nick@_libs)\n"
      with_tcon += " -a \\\n            \"${with_@pkg_nick@_libs}\" = \"\""
      with_tdef += " -o \\\n            \"${with_@pkg_nick@_libs}\" != \"\""
      pkg_xfer += "    afb_@pkg_nick@_libs=\"${with_@pkg_nick@_libs}\"\n"
    with_opts += "  if test \"${enable_@pkg_nick@}\" = \"\"; then\n"
    with_opts += "    if test \"0\" = \"1\""
    with_opts += with_tdef + "; then\n"
    with_opts += "      enable_@pkg_nick@=\"no\"\n"
    with_opts += "    fi\n"
    with_opts += "  fi\n"
    with_opts += "  if test \"${enable_@pkg_nick@}\" = \"yes\"; then\n"
    with_opts += "    if test \"1\" = \"1\""
    with_opts += with_tcon + "; then\n"
    with_opts += "      AC_MSG_NOTICE([options for @pkg_nick@ are consistent])\n"
    with_opts += "    else\n"
    with_opts += "      AC_MSG_ERROR([inconsistent options for @pkg_nick@!\n"
    with_opts += "        use --enable-@pkg_nick@ or --with-@pkg_nick@-*, not both])\n"
    with_opts += "    fi\n"
    with_opts += "  fi"
  if ( pkg_xfer != "" ):
    pkg_xfer = "  else\n\n    dnl Check whether the external package works\n%s    AFB_CHECK_@PKG_NICK@@CHK_ARGS@\n    afb_@pkg_nick@_ok=\"${afb_@pkg_nick@_ext_ok}\"\n" % pkg_xfer

  # Transform template (the order is important)
  ret = re.sub("@SET_VARS@",set_vars,ret)
  ret = re.sub("@INCS_TEST@",incs_test,ret)
  ret = re.sub("@INCS_PATH@",incs_path,ret)
  ret = re.sub("@PKG_SPECS@",pkg_specs,ret)
  ret = re.sub("@PKG_XFER@",pkg_xfer,ret)
  ret = re.sub("@CHK_ARGS@",chk_args,ret)
  ret = re.sub("@PKG_SUBST@",pkg_subst,ret)
  ret = re.sub("@ENV_VARS@",env_vars,ret)
  ret = re.sub("@BINS_SPECS@",bins_specs,ret)
  ret = re.sub("@WITH_OPTS@",with_opts,ret)
  ret = re.sub("@CHK_DEPS@",chk_deps,ret)
  ret = re.sub("@MD5SUM@",md5s,ret)
  ret = re.sub("@TRICKS@",tricks,ret)
  ret = re.sub("@PKG_NAME@",name,ret)
  ret = re.sub("@PKG_INST@",pkg_inst,ret)
  ret = re.sub("@PKG_DESC@",desc,ret)
  ret = re.sub("@PKG_NICK@",nick.upper(),ret)
  ret = re.sub("@PKG_URLS@",urls_specs,ret)
  ret = re.sub("@pkg_nick@",nick,ret)
  ret = re.sub("@pkg_opt@",re.sub("_","-",nick),ret)

  return ret



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

#
# Main program
#

# Initial setup
my_name    = "make-macros-fallbacks"
my_configs = ["config/specs/fallbacks.conf","config/specs/languages.conf"]
my_output  = "config/m4/auto-fallbacks.m4"

# Check if we are in the top of the ABINIT source tree
if ( not os.path.exists("configure.ac") or
     not os.path.exists("config/specs/fallbacks.conf") ):
  print("%s: You must be in the top of an ABINIT source tree." % my_name)
  print("%s: Aborting now." % my_name)
  sys.exit(1)

# Read config open(s)
for cnf_file in my_configs:
  if ( os.path.exists(cnf_file) ):
    if ( re.search("\.cf$",cnf_file) ):
      exec(compile(open(cnf_file).read(), cnf_file, 'exec'))
  else:
    print("%s: Could not find config file (%s)." % (my_name,cnf_file))
    print("%s: Aborting now." % my_name)
    sys.exit(2)

# What time is it?
now = strftime("%Y/%m/%d %H:%M:%S +0000",gmtime())

# Init
cnf = MyConfigParser()
cnf.read(my_configs[0])
abinit_fallbacks = cnf.sections()
abinit_fallbacks.sort()
m4_all = macro_template(my_name,now)
m4_fbk = ""
m4_ord = ""

# Define environment variables
lcf = MyConfigParser()
lcf.read(my_configs[1])
lng_specs = {}
lng_specs["languages"] = {}
lng_specs["descriptions"] = {}
for lang in lcf.sections():
  if ( lang == "languages" ):
    lng_specs["languages"]["fallbacks"] = lcf.get("languages","fallbacks").split()
    lng_specs["languages"]["global"] = lcf.get("languages","fallbacks").split()
  elif (lang == "descriptions" ):
    for item in lcf.options(lang):
      lng_specs["descriptions"][item] = lcf.get(lang,item)
  else:
    lng_specs[lang] = {"envs":lcf.get(lang,"envs").split()}
    for stage in lng_specs[lang]["envs"]:
      if ( lcf.has_option(lang,stage) ):
        lng_specs[lang][stage] = lcf.get(lang,stage).split()
      else:
        lng_specs[lang][stage] = None

# Sort fallbacks by dependencies
is_preq = {}
all_deps = {}
pkg_num = {}
i = 0
for pkg in abinit_fallbacks:
  is_preq[pkg] = False
  pkg_num[pkg] = i
  i += 1
for i in range(len(abinit_fallbacks)):
  for pkg in abinit_fallbacks:
    if ( cnf.has_option(pkg,"depends") ):
      all_deps[pkg] = cnf.get(pkg,"depends").split()
      pkg_deps = all_deps[pkg]
      for dep in pkg_deps:
        is_preq[dep] = True
        if ( pkg_num[dep] >= pkg_num[pkg] ):
          pkg_num[pkg] = pkg_num[dep] + 1
    else:
      all_deps[pkg] = None
pkg_ranks = list(pkg_num.values())
pkg_ranks = list(set(pkg_ranks))
pkg_ranks.sort()

# Create package list in the correct order
for rnk in pkg_ranks:
  for pkg in pkg_num:
    if ( pkg_num[pkg] == rnk ):
      m4_ord += "\n  _AFB_SETUP_%s" % pkg.upper()
m4_ord += "\n"

m4_all = re.sub("@FBK_LIST@"," ".join(abinit_fallbacks),m4_all)
m4_all = re.sub("@FBK_CALLS@",m4_ord,m4_all)
m4_cfg = []

# Process fallbacks individually
for pkg in abinit_fallbacks:

  # Extract mandatory package information
  pkg_name = cnf.get(pkg,"name")
  pkg_desc = cnf.get(pkg,"description")
  pkg_md5s = cnf.get(pkg,"md5sum")
  pkg_urls = cnf.get(pkg,"urls").split()
  pkg_lngs = ["CONFIG"] + cnf.get(pkg,"languages").split() + ["LINK"]
  pkg_chka = cnf.get(pkg,"check_api")

  # Extract optional package information
  try:
    pkg_bins = cnf.get(pkg,"binaries").split()
  except NoOptionError:
    pkg_bins = None
  try:
    pkg_hdrs = cnf.get(pkg,"headers").split()
  except NoOptionError:
    pkg_hdrs = None
  try:
    pkg_libs = cnf.get(pkg,"libraries").split()
  except NoOptionError:
    pkg_libs = None
  try:
    pkg_mods = cnf.get(pkg,"modules").split()
  except NoOptionError:
    pkg_mods = None
  try:
    pkg_deps = cnf.get(pkg,"depends").split()
  except NoOptionError:
    pkg_deps = None

  # Expand dependencies
  if ( pkg_deps is None ):
    pkg_dep_libs = None
  else:
    pkg_dep_libs = {}
    for dep in pkg_deps:
      pkg_dep_libs[dep] = cnf.get(dep,"libraries").split()

  # Check whether the fallback exports C headers or modules
  if ( (pkg_hdrs is None) and (pkg_mods is None) ):
    pkg_incs = None
  else:
    pkg_incs = "yes"

  # Define environment variables
  pkg_vars = snippet_envvars(pkg_lngs,lng_specs)

  # Check whether we want to check the package's API
  chk_api = None
  if ( pkg_chka == "yes" ):
    api_version = pkg_name.split("-")[1].split(".")[0:2]
    chk_api = {}
    chk_api["maj"] = int(api_version[0])
    chk_api["min"] = int(api_version[1])
    chk_api["max"] = int(api_version[1])

  # Generate macro
  m4_fbk += macro_fallback(pkg,pkg_name,pkg_desc,pkg_libs,pkg_bins,
    pkg_md5s,pkg_urls,pkg_dep_libs,pkg_deps,pkg_incs,is_preq=is_preq[pkg],
    env_vars=pkg_vars,chk_api=chk_api)

# Substitute global template and write down macro file
m4_all = re.sub("@FALLBACKS@", m4_fbk, m4_all)
open(my_output,"w").write(m4_all)
