// ---------------------------------------------------------------------
//
// Copyright (C) 2007 - 2018 by the deal.II authors
//
// This file is part of the deal.II library.
//
// The deal.II library is free software; you can use it, redistribute
// it, and/or modify it under the terms of the GNU Lesser General
// Public License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// The full text of the license can be found in the file LICENSE at
// the top level of the deal.II distribution.
//
// ---------------------------------------------------------------------


// This is the program that we use to generate explicit instantiations for a
// variety of template arguments. It takes two kinds of input files. The first
// is given as arguments on the command line and contains entries of the
// following form:
// --------------------
// REAL_SCALARS    := { double; float; long double }
// COMPLEX_SCALARS := { std::complex<double>;
//                      std::complex<float>;
//                      std::complex<long double> }
// VECTORS := { Vector<double>; Vector<float>; Vector<long double> }
// --------------------
//
// The input file is typically located in share/deal.II/template-arguments in
// the build directory and it is built from config/template-arguments.in to
// contain the list of vectors etc. that make sense for the current
// configuration. For example, the list of VECTORS is going to contain PETSc
// vectors if so configured.
//
// The second input is read from the command line and consists of a sequence
// of statements of the following form:
// --------------------
// for (u,v:VECTORS; z:SCALARS) { f(u, z, const v &); }
// --------------------
// Here, everything between {...} will be copied as many times as there are
// combinations of arguments u,v in the list of substitutions given by
// VECTORS. For each copy, the arguments u,v will be replaced by one of these
// combinations.

// Author: Wolfgang Bangerth, 2007


#include <algorithm>
#include <iostream>
#include <fstream>
#include <map>
#include <list>
#include <cstdlib>
#include <string>

// a map from the keys in the expansion lists to the list itself. For
// instance, the example above will lead to the entry
//      expansion_lists[REAL_SCALARS] = (double, float, long double)
// in this map, among others
std::map<std::string, std::list<std::string> >  expansion_lists;



/* ======================== auxiliary functions ================= */

// Return whether or not one string starts with a given prefix
bool
has_prefix(const std::string &base, const std::string &prefix)
{
  if (prefix.size() > base.size())
    return false;
  else
    return std::equal(prefix.begin(), prefix.end(), base.begin());
}


// replace all occurrences of 'pattern' by 'substitute' in 'in', and
// return the result
std::string
replace_all (const std::string &in,
             const std::string &pattern,
             const std::string &substitute)
{
  std::string x = in;
  while (x.find(pattern) != std::string::npos)
    x.replace (x.find(pattern),
               pattern.size(),
               substitute);
  return x;
}


// extract from the start of #in the part of the string that ends with one
// of the characters in #delim_list. The extracted part is deleted from #in
// and returned. We skip characters in delim_list if they are preceded by a
// backslash
std::string
get_substring_with_delim (std::string       &in,
                          const std::string &delim_list)
{
  std::string x;
  while (in.size() != 0)
    {
      // stop copying to the result if the current character is a
      // delimiter, but only if the previous character was not a backslash
      if ((delim_list.find (in[0]) != std::string::npos)
          &&
          !((x.size() > 0)
            &&
            (x[x.size()-1] == '\\')))
        break;

      x += in[0];
      in.erase (0, 1);
    }

  return x;
}


// delete all whitespace at the beginning of the given argument
void
skip_space (std::string &in)
{
  while ((in.size() != 0)
         &&
         ((in[0] == ' ') || (in[0] == '\t') || (in[0] == '\n')))
    in.erase (0, 1);
}


std::string remove_comments (std::string line)
{
  const std::string::size_type double_slash_comment = line.find ("//");
  if (double_slash_comment != std::string::npos)
    line.erase (double_slash_comment, std::string::npos);

  const std::string::size_type slash_star_comment_begin = line.find ("/*");
  if (slash_star_comment_begin != std::string::npos)
    {
      const std::string::size_type slash_star_comment_end = line.find ("*/");
      if (slash_star_comment_end == std::string::npos)
        {
          std::cerr << "The program can currently only handle /* block */"
                    << "comments that start and end within the same line."
                    << std::endl;
          std::exit (1);
        }
      line.erase (slash_star_comment_begin,
                  slash_star_comment_end - slash_star_comment_begin + 2);
    }

  return line;
}


// read the whole file specified by the stream given as argument into a string
// for simpler parsing, and return it
std::string read_whole_file (std::istream &in)
{
  std::string whole_file;
  while (in)
    {
      std::string line;
      getline (in, line);

      whole_file += remove_comments (line);
      whole_file += '\n';
    }
  // substitute tabs by spaces, multiple spaces by single ones
  for (unsigned int i=0; i<whole_file.size(); ++i)
    if (whole_file[i] == '\t')
      whole_file[i] = ' ';
  while (whole_file.find("  ") != std::string::npos)
    whole_file.replace (whole_file.find("  "), 2, " ");

  return whole_file;
}



// split a given string assumed to consist of a list of substrings
// delimited by a particular character into its components
std::list<std::string>
split_string_list (const std::string &s,
                   const char         delimiter)
{
  std::string tmp = s;
  std::list<std::string> split_list;

  // split the input list
  while (tmp.length() != 0)
    {
      std::string name;
      name = tmp;

      if (name.find(delimiter) != std::string::npos)
        {
          name.erase (name.find(delimiter), std::string::npos);
          tmp.erase (0, tmp.find(delimiter)+1);
        }
      else
        tmp = "";

      skip_space (name);

      while ((name.size() != 0) && (name[name.length()-1] == ' '
                                    || name[name.length()-1] == '\n'))
        name.erase (name.length()-1, 1);

      split_list.push_back (name);
    }

  return split_list;
}



// return the given list but without empty entries
std::list<std::string>
delete_empty_entries (const std::list<std::string> &list)
{
  std::list<std::string> return_list;
  for (std::list<std::string>::const_iterator i = list.begin();
       i != list.end(); ++i)
    if (*i != "")
      return_list.push_back (*i);

  return return_list;
}



// determine whether a given substring at position #pos and length #length
// in the string #text is a real token, i.e. not just part of another word
bool is_real_token (const std::string &text,
                    const std::string::size_type pos,
                    const std::string::size_type length)
{
  static const std::string token_chars ("abcdefghijklmnopqrstuvwxyz"
                                        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                                        "0123456789"
                                        "_");
  if ((pos != 0) && (token_chars.find (text[pos-1]) != std::string::npos))
    return false;

  if ((pos+length < text.size()) &&
      (token_chars.find (text[pos+length]) != std::string::npos))
    return false;

  return true;
}


// substitute all occurrences of #token in #text by #substitute. because a
// replacement token could be a templated class like std::complex<double>
// and because the token to the substituted may be a template argument
// itself, we surround the substitution by a space which shouldn't matter
// in C++
std::string substitute_tokens (const std::string &text,
                               const std::string &token,
                               const std::string &substitute)
{
  std::string x_text = text;
  std::string::size_type pos = 0;
  while ((pos = x_text.find(token, pos)) != std::string::npos)
    {
      if (is_real_token (x_text, pos, token.size()))
        {
          x_text.replace (pos, token.size(),
                          std::string(" ")+substitute+std::string(" "));
          pos += substitute.size()+2;
        }
      else
        ++pos;
    }

  return x_text;
}



/* ======================== the main functions ================= */


// read and parse the expansion lists like
//   REAL_SCALARS    := { double; float; long double }
// as specified at the top of the file and store them in the global
// expansion_lists variable
void read_expansion_lists (const std::string &filename)
{
  std::ifstream in (filename.c_str());

  if (! in)
    {
      std::cerr << "Instantiation list file can not be read!"
                << std::endl;
      std::exit (1);
    }

  // read the entire file into a string for simpler processing. replace
  // end-of-line characters by spaces
  std::string whole_file = read_whole_file (in);

  skip_space (whole_file);

  // now process entries of the form
  //   NAME := { class1; class2; ...}.
  while (whole_file.size() != 0)
    {
      const std::string
      name = get_substring_with_delim (whole_file, " :");

      skip_space (whole_file);
      if (!has_prefix(whole_file, ":="))
        {
          std::cerr << "Invalid entry <" << name << '>' << std::endl;
          std::exit (1);
        }
      whole_file.erase (0, 2);
      skip_space (whole_file);
      if (whole_file[0] != '{')
        {
          std::cerr << "Invalid entry <" << name << '>' << std::endl;
          std::exit (1);
        }
      whole_file.erase (0, 1);
      skip_space (whole_file);

      std::string
      expansion = get_substring_with_delim (whole_file, "}");

      if (whole_file[0] != '}')
        {
          std::cerr << "Invalid entry <" << name << '>' << std::endl;
          std::exit (1);
        }
      whole_file.erase (0, 1);
      skip_space (whole_file);

      // assign but remove empty entries; this may happen if an expansion
      // list ends in a semicolon (then we get an empty entry at the end),
      // or if there are multiple semicolons after each other (this may
      // happen if, for example, we have "Vector<double>; TRILINOS_VECTOR;"
      // and if TRILINOS_VECTOR is an empty expansion after running
      // ./configure)
      expansion_lists[name]
        = delete_empty_entries (split_string_list (expansion, ';'));
    }
}



// produce all combinations of substitutions of the tokens given in the
// #substitutions list in #text and output it to std::cout
void substitute (const std::string &text,
                 const std::list<std::pair<std::string, std::string> > &substitutions)
{
  // do things recursively: if the list of substitutions has a single
  // entry, then process all of them. otherwise, process the first in the
  // list and call the function recursively with the rest of the
  // substitutions
  if (substitutions.size() > 1)
    {
      // do the first substitution, then call function recursively
      const std::string name    = substitutions.front().first,
                        pattern = substitutions.front().second;

      if (expansion_lists.find(pattern)==expansion_lists.end())
        {
          std::cerr << "could not find pattern '" << pattern << "'" << std::endl;
          std::exit (1);
        }


      const std::list<std::pair<std::string, std::string> >
      rest_of_substitutions (++substitutions.begin(),
                             substitutions.end());

      for (std::list<std::string>::const_iterator
           expansion = expansion_lists[pattern].begin();
           expansion != expansion_lists[pattern].end();
           ++expansion)
        {
          std::string new_text
            = substitute_tokens (text, name, *expansion);

          substitute (new_text, rest_of_substitutions);
        }
    }
  else if (substitutions.size() == 1)
    {
      // do the substitutions
      const std::string name    = substitutions.front().first,
                        pattern = substitutions.front().second;

      if (expansion_lists.find(pattern)==expansion_lists.end())
        {
          std::cerr << "could not find pattern '" << pattern << "'"<< std::endl;
          std::exit (1);
        }

      for (std::list<std::string>::const_iterator
           expansion = expansion_lists[pattern].begin();
           expansion != expansion_lists[pattern].end();
           ++expansion)
        {
          // surround each block in the for loop with an if-def hack
          // that allows us to split instantiation files into several
          // chunks to be used in different .cc files (to reduce
          // compiler memory usage).
          // Just define SPLIT_INSTANTIATIONS_COUNT to a positive number (number of sections)
          // to split the definitions into and SPLIT_INSTANTIATIONS_INDEX as a number
          // between 0 and SPLIT_INSTANTIATIONS_COUNT-1 to get the instantiations of that
          // particular chunk.
          static unsigned int counter = 0;
          std::cout << "#if (SPLIT_INSTANTIATIONS_CHECK("
                    << counter++
                    << "))" << std::endl;
          std::cout << substitute_tokens (text, name, *expansion)
                    << std::endl;
          std::cout << "#endif" << std::endl;
        }

    }
  else
    {
      std::cout << text
                << std::endl;
    }

}



// process the list of instantiations given in the form
//   for (u,v:VECTORS; z:SCALARS) { f(u, z, const v &); }
void process_instantiations ()
{
  std::string whole_file = read_whole_file (std::cin);

  // process entries of the form
  //   for (X:Y; A:B) { INST }
  while (whole_file.size() != 0)
    {
      skip_space (whole_file);
      if (!has_prefix(whole_file, "for"))
        {
          std::cerr << "Invalid instantiation list: missing 'for'" << std::endl;
          std::exit (1);
        }
      whole_file.erase (0, 3);
      skip_space (whole_file);
      if (whole_file[0] != '(')
        {
          std::cerr << "Invalid instantiation list: missing '('" << std::endl;
          std::exit (1);
        }
      whole_file.erase (0, 1);
      skip_space (whole_file);

      const std::list<std::string>
      substitutions_list
        = split_string_list (get_substring_with_delim (whole_file,
                                                       ")"),
                             ';');
      if (whole_file[0] != ')')
        {
          std::cerr << "Invalid instantiation list: missing ')'" << std::endl;
          std::exit (1);
        }
      whole_file.erase (0, 1);
      skip_space (whole_file);

      // process the header
      std::list<std::pair<std::string, std::string> >
      substitutions;

      for (std::list<std::string>::const_iterator
           s = substitutions_list.begin();
           s != substitutions_list.end(); ++s)
        {
          const std::list<std::string>
          names_and_type = split_string_list (*s, ':');
          if (names_and_type.size() != 2)
            {
              std::cerr << "Invalid instantiation header: '"<< *s << "'" << std::endl;
              std::exit (1);
            }

          const std::list<std::string>
          names = split_string_list (names_and_type.front(), ',');

          for (std::list<std::string>::const_iterator
               x = names.begin(); x != names.end(); ++x)
            substitutions.emplace_back (*x, names_and_type.back());
        }

      // now read the part in {...}
      skip_space (whole_file);
      if (whole_file[0] != '{')
        {
          std::cerr << "Invalid substitution text" << std::endl;
          std::exit (1);
        }
      whole_file.erase (0, 1);
      skip_space (whole_file);
      const std::string text_to_substitute
        = get_substring_with_delim (whole_file, "}");
      whole_file.erase (0,1);
      skip_space (whole_file);

      // now produce the substitutions. first replace all occurrences of
      // "\{" by "{"
      substitute (replace_all(replace_all(text_to_substitute, "\\{", "{"),
                              "\\}", "}"),
                  substitutions);
    }
}



int main (int argc, char **argv)
{
  if (argc < 2)
    {
      std::cerr << "Usage: " << std::endl
                << "  expand_instantiations class_list_files < in_file > out_file"
                << std::endl;
      std::exit (1);
    }

  for (int i=1; i<argc; ++i)
    read_expansion_lists (argv[i]);

  // write header:
  std::cout << "// This file is automatically generated from corresponding .inst.in, do not edit."
            << std::endl << std::endl;

  // Make sure SPLIT_INSTANTIATIONS_* is working correctly if the user doesn't
  // use it. The defaults will not split the instantiations. This logic is
  // somewhat tricky to get right for two reasons: 1. icc 14 will not allow an
  // expressition like "#if !defined(B) || (A % B == C)" 2. we have .cc files
  // where more than one .inst is included and splitting is only required in
  // one of them. So we need to handle the case where _COUNT is undefined but
  // _INDEX is defined, which might be needed later.
  std::cout << "#ifdef SPLIT_INSTANTIATIONS_COUNT" << std::endl
            << "  #define SPLIT_INSTANTIATIONS_CHECK(C) (((C) % SPLIT_INSTANTIATIONS_COUNT) == SPLIT_INSTANTIATIONS_INDEX)" << std::endl
            << "#else" << std::endl
            << "  #define SPLIT_INSTANTIATIONS_CHECK(C) (1)" << std::endl
            << "#endif" << std::endl << std::endl;

  process_instantiations ();

  // undefine the macro to avoid issues when more than one .inst file is
  // included in a single .cc
  std::cout << std::endl
            << "#undef SPLIT_INSTANTIATIONS_CHECK" << std::endl;
}
