/*****************************************************************************\
*                                                                             *
*  Name   : write_build_config                                                *
*  Author : Chris Koeritz                                                     *
*                                                                             *
*******************************************************************************
* Copyright (c) 1995-$now By Author.  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 is online at:      *
*     http://www.fsf.org/copyleft/gpl.html                                    *
* Please send any updates to: fred@gruntose.com                               *
\*****************************************************************************/

#include "write_build_config.h"

#include <basis/function.h>
#include <basis/portable.h>
#include <basis/set.cpp>
#include <basis/version_record.h>
#include <data_struct/string_table.h>
#include <opsystem/byte_filer.h>
#include <loggers/console_logger.h>
#include <data_struct/static_memory_gremlin.h>
#include <opsystem/version_ini.h>
#include <textual/tokenizer.h>

#include <stdio.h>

const int MAX_LINE_SIZE = 2048;
  //!< we should never see an ini line longer than this.

const int MAX_HEADER_FILE = 128 * KILOBYTE;
  //!< an excessively long allowance for the maximum generated header size.

const char *DEFINITIONS_STATEMENT = "DEFINITIONS";
  //!< the tag we see in the build.ini for directly compatible macros.

const char *EXPORT_STATEMENT = "export ";
  //!< a tag we see on variables to be inherited by subshells

// make conditionals that we will eat.
const char *IFEQ_STATEMENT = "ifeq";
const char *IFNEQ_STATEMENT = "ifneq";
const char *ENDIF_STATEMENT = "endif";

#undef LOG
#define LOG(to_print) CLASS_EMERGENCY_LOG(program_wide_logger(), to_print)

write_build_config::write_build_config()
: application_shell(static_class_name()),
  _end_matter(new istring),
  _ver(new version),
  _nesting(0)
{}

write_build_config::~write_build_config()
{
  WHACK(_end_matter);
  WHACK(_ver);
}

const string_set &write_build_config::exclusions() 
{
  static string_set _hidden;
  static bool _initted = false;
  if (!_initted) {
    _hidden += "DEBUG";
    _hidden += "OPTIMIZE";
    _hidden += "STRICT_WARNINGS";
    _hidden += "NO_SETUP";
  }
  return _hidden;
}

// adds some more material to dump at the end of the file.
#define ADD_COMMENT_RETURN(sym, val) { \
  *_end_matter += istring("  ") + sym + " = " + val + "\n"; \
  return common::OKAY; \
}

outcome write_build_config::output_macro(const istring &symbol,
    const istring &value, istring &accumulator)
{
  // drop any excluded items to avoid interfering with devstu solution.
  if (exclusions().member(symbol))
    ADD_COMMENT_RETURN(symbol, value);
  // drop any malformed symbols or values.
  if (symbol.contains("\"") || value.contains("\""))
    ADD_COMMENT_RETURN(symbol, value);
  accumulator += "  #ifndef ";
  accumulator += symbol;
  accumulator += "\n";
  accumulator += "    #define ";
  accumulator += symbol;
  accumulator += " \"";
  accumulator += value;
  accumulator += "\"\n";
  accumulator += "  #endif\n";
  return common::OKAY;
}

bool write_build_config::process_version_parts(const istring &symbol,
    const istring &value)
{
  if (symbol == "major")
    { _ver->set_component(version::MAJOR, value); return true; }
  if (symbol == "minor")
    { _ver->set_component(version::MINOR, value); return true; }
  if (symbol == "revision")
    { _ver->set_component(version::REVISION, value); return true; }
  if (symbol == "build")
    { _ver->set_component(version::BUILD, value); return true; }
  return false;
}

bool write_build_config::check_nesting(const istring &to_check)
{
  if (to_check.compare(IFEQ_STATEMENT, 0, 0, int(strlen(IFEQ_STATEMENT)))
      || to_check.compare(IFNEQ_STATEMENT, 0, 0,
          int(strlen(IFNEQ_STATEMENT)))) {
    _nesting++;
    return true;
  }
  if (to_check.compare(ENDIF_STATEMENT, 0, 0, int(strlen(ENDIF_STATEMENT)))) {
    _nesting--;
    return true;
  }
  return false;
}

outcome write_build_config::output_decorated_macro(const istring &symbol_in,
    const istring &value, istring &cfg_accumulator, istring &ver_accumulator)
{
  // make sure we catch any conditionals.
  if (check_nesting(symbol_in))
    ADD_COMMENT_RETURN(symbol_in, value);
  // toss out any exclusions.
  if (exclusions().member(symbol_in))
    ADD_COMMENT_RETURN(symbol_in, value);
  if (symbol_in.contains("\"") || value.contains("\""))
    ADD_COMMENT_RETURN(symbol_in, value);
  if (symbol_in[0] == '[')
    ADD_COMMENT_RETURN(symbol_in, value);
  if (_nesting)
    ADD_COMMENT_RETURN(symbol_in, value);
  // switch the output stream based on whether its a version component or not.
  istring *the_accumulator = &cfg_accumulator;
  if (process_version_parts(symbol_in, value)) {
    the_accumulator = &ver_accumulator;
  }
  // add a special tag so that we won't be colliding with other names.
  istring symbol = istring("__build_") + symbol_in;
  return output_macro(symbol, value, *the_accumulator);
}

outcome write_build_config::output_definition_macro
    (const istring &embedded_value, istring &accumulator)
{
  FUNCDEF("output_definition_macro");
//LOG(istring("into output def with: ") + embedded_value);
  tokenizer t;
  t.parse(embedded_value);
  if (!t.symbols())
    ADD_COMMENT_RETURN("bad definition", embedded_value);
  if (exclusions().member(t.table().name(0)))
    ADD_COMMENT_RETURN(t.table().name(0), t.table()[0]);
  if (_nesting)
    ADD_COMMENT_RETURN(t.table().name(0), t.table()[0]);
  return output_macro(t.table().name(0), t.table()[0], accumulator);
}

bool write_build_config::write_output_file(const istring &filename,
    const istring &new_contents)
{
  FUNCDEF("write_output_file");
  // now read the soon-to-be output file so we can check its current state.
  bool write_header = true;
  byte_filer check_header(filename, "rb");
  if (check_header.good()) {
    byte_array file_contents;
    int read = check_header.read(file_contents, MAX_HEADER_FILE);
if (read < 1) LOG("why is existing header contentless?");
    if (read > 0) {
      istring found(istring::UNTERMINATED, (char *)file_contents.observe(),
          read);
//LOG(istring("got existing content:\n-----\n") + found + "\n-----\n");
//LOG(istring("new_content has:\n-----\n") + new_contents + "\n-----\n");
      if (found == new_contents) {
        write_header = false;
      }
    }
  }
  // writing only occurs when we know that the build configurations have
  // changed.  if the file is the same, we definitely don't want to write
  // it because pretty much all the other files depend on it.
  if (write_header) {
    // we actually want to blast out a new file.
    byte_filer build_header(filename, "wb");
    if (!build_header.good())
      non_continuable_error(static_class_name(), func, istring("failed to create "
          "build header file in ") + build_header.filename());
    build_header.write(new_contents);
    LOG(istring(static_class_name()) + ": wrote config to "
        + build_header.filename());
  } else {
    // nothing has changed.
//    LOG(istring(static_class_name()) + ": config already up to date in "
//        + filename);
  }
  return true;
}

int write_build_config::execute()
{
  FUNCDEF("execute");
  SET_DEFAULT_CONSOLE_LOGGER;  // override the file_logger from app_shell.

  // find our build ini file.
  istring repodir = portable::env_string("REPOSITORY_DIR");

  // the below code should never be needed for a properly configured build.
#ifdef __WIN32__
  if (!repodir) repodir = "l:";
#else  // unix and other locations.
  if (!repodir)
    repodir = portable::env_string("HOME") + "/hoople";
#endif

  istring fname = repodir + "/build.ini";
    // where we seek out our build settings.

  // these are very specific paths, but they really are where we expect to
  // see the headers.
  istring cfg_header_filename = repodir + "/source/lib_src/library/"
      "__build_configuration.h";
  istring ver_header_filename = repodir + "/source/lib_src/library/"
      "__build_version.h";

  // open the ini file for reading.
  byte_filer ini(fname, "r");
  if (!ini.good())
    non_continuable_error(static_class_name(), func, istring("failed to open "
        "build.ini file for reading at ") + ini.filename());

  // now we build strings that represents the output files we want to create.
  istring cfg_accumulator;
  istring ver_accumulator;

  // odd indentation below comes from the strings being c++ code that will be
  // written to a file.
  cfg_accumulator += "\
#ifndef BUILD_SYSTEM_CONFIGURATION\n\
#define BUILD_SYSTEM_CONFIGURATION\n\n\
  // This file provides all of the code flags which were used when this\n\
  // build was generated.  Some of the items in build.ini have been stripped\n\
  // out because they are not used.\n\n";
  ver_accumulator += "\
#ifndef BUILD_VERSION_CONFIGURATION\n\
#define BUILD_VERSION_CONFIGURATION\n\n\
  // This file provides the version macros for this particular build.\n\n";

  // iterate through the entries we read in earlier and generate our header.
  istring symbol, value;
  istring buffer;
  while (!ini.eof()) {
    int chars = ini.getline(buffer, MAX_LINE_SIZE);
    if (!chars) continue;  // hmmm.
    
    tokenizer t;
    t.parse(buffer);
    if (!t.symbols()) continue;  // not so good.

    // pull out the first pair we found and try to parse it.
    symbol = t.table().name(0);
    value = t.table()[0];
    symbol.strip_spaces(istring::FROM_BOTH_SIDES);

    // clean out + characters that can come from += declarations.
    while ( (symbol[symbol.end()] == '+') || (symbol[symbol.end()] == ':') ) {
      symbol.zap(symbol.end(), symbol.end());
      symbol.strip_spaces(istring::FROM_END);
    }

    if (symbol[0] == '#') continue;  // toss out any comments.

    if (!symbol) continue;  // seems like that one didn't work out so well.

    if (symbol.compare(EXPORT_STATEMENT, 0, 0, int(strlen(EXPORT_STATEMENT)))) {
      // clean out export statements in front of our variables.
      symbol.zap(0, int(strlen(EXPORT_STATEMENT) - 1));
    }

    // check for a make-style macro definition.
    if (symbol.compare(DEFINITIONS_STATEMENT, 0, 0,
        int(strlen(DEFINITIONS_STATEMENT)))) {
      // found a macro definition.  write that up after pulling the real
      // contents out.
      output_definition_macro(value, cfg_accumulator);
    } else {
      // this one is hopefully a very tasty specialized macro.  we will
      // show it with added text to make it unique.
      output_decorated_macro(symbol, value, cfg_accumulator, ver_accumulator);
    }
  }

  // write some calculated macros now.
  ver_accumulator += "\n";
  ver_accumulator += "  // calculated macros are dropped in here.\n\n";

  // we write our version in a couple forms.  hopefully we accumulated it.

  // this one is the same as the file version currently (below), but may go to
  // a different version at some point.
  ver_accumulator += "  #define __build_SYSTEM_VERSION \"";
  ver_accumulator += _ver->flex_text_form();
  ver_accumulator += "\"\n\n";

  // we drop in the version as numbers also, since the version RC wants that.
  ver_accumulator += "  #define __build_FILE_VERSION_COMMAS ";
  ver_accumulator += _ver->flex_text_form(version::COMMAS);
  ver_accumulator += "\n";
  // another form of the file version for dotted notation.
  ver_accumulator += "  #define __build_FILE_VERSION \"";
  ver_accumulator += _ver->flex_text_form();
  ver_accumulator += "\"\n";

  // product version is just the first two parts.
  _ver->set_component(version::REVISION, "0");
  _ver->set_component(version::BUILD, "0");
  // product version as a list of numbers.
  ver_accumulator += "  #define __build_PRODUCT_VERSION_COMMAS ";
  ver_accumulator += _ver->flex_text_form(version::COMMAS);
  ver_accumulator += "\n";
  // another form of the product version for use as a string.
  ver_accumulator += "  #define __build_PRODUCT_VERSION \"";
  ver_accumulator += _ver->flex_text_form(version::DOTS, version::MINOR);
  ver_accumulator += "\"\n";

  // write a blob of comments at the end with what we didn't use.
  cfg_accumulator += "\n";
  cfg_accumulator += "/*\n";
  cfg_accumulator += "These settings were not used:\n";
  cfg_accumulator += *_end_matter;
  cfg_accumulator += "*/\n";
  cfg_accumulator += "\n";
  cfg_accumulator += "#endif /* outer guard */\n\n";

  // finish up the version file also.
  ver_accumulator += "\n";
  ver_accumulator += "#endif /* outer guard */\n\n";

  if (!write_output_file(cfg_header_filename, cfg_accumulator)) {
    LOG(istring("failed writing output file ") + cfg_header_filename);
  }
  if (!write_output_file(ver_header_filename, ver_accumulator)) {
    LOG(istring("failed writing output file ") + ver_header_filename);
  }

  return 0;
}

HOOPLE_MAIN(write_build_config, )

#ifdef __BUILD_STATIC_APPLICATION__
  // static dependencies found by buildor_gen_deps.sh:
  #include <basis/array.cpp>
  #include <basis/byte_array.cpp>
  #include <basis/callstack_tracker.cpp>
  #include <basis/chaos.cpp>
  #include <basis/convert_utf.cpp>
  #include <basis/definitions.cpp>
  #include <basis/earth_time.cpp>
  #include <basis/guards.cpp>
  #include <basis/istring.cpp>
  #include <basis/log_base.cpp>
  #include <basis/memory_checker.cpp>
  #include <basis/mutex.cpp>
  #include <basis/object_base.cpp>
  #include <basis/outcome.cpp>
  #include <basis/packable.cpp>
  #include <basis/portable.cpp>
  #include <basis/sequence.cpp>
  #include <basis/set.cpp>
  #include <basis/utility.cpp>
  #include <basis/version_checker.cpp>
  #include <basis/version_record.cpp>
  #include <data_struct/amorph.cpp>
  #include <data_struct/bit_vector.cpp>
  #include <data_struct/byte_hasher.cpp>
  #include <data_struct/configurator.cpp>
  #include <data_struct/hash_table.cpp>
  #include <data_struct/pointer_hash.cpp>
  #include <data_struct/stack.cpp>
  #include <data_struct/static_memory_gremlin.cpp>
  #include <data_struct/string_hash.cpp>
  #include <data_struct/string_hasher.cpp>
  #include <data_struct/string_table.cpp>
  #include <data_struct/symbol_table.cpp>
  #include <data_struct/table_configurator.cpp>
  #include <loggers/console_logger.cpp>
  #include <loggers/file_logger.cpp>
  #include <loggers/locked_logger.cpp>
  #include <loggers/null_logger.cpp>
  #include <loggers/program_wide_logger.cpp>
  #include <opsystem/application_base.cpp>
  #include <opsystem/application_shell.cpp>
  #include <opsystem/byte_filer.cpp>
  #include <opsystem/command_line.cpp>
  #include <opsystem/critical_events.cpp>
  #include <opsystem/directory.cpp>
  #include <opsystem/filename.cpp>
  #include <opsystem/ini_config.cpp>
  #include <opsystem/ini_parser.cpp>
  #include <opsystem/path_configuration.cpp>
  #include <opsystem/rendezvous.cpp>
  #include <opsystem/version_ini.cpp>
  #include <textual/byte_format.cpp>
  #include <textual/parser_bits.cpp>
  #include <textual/string_manipulation.cpp>
  #include <textual/tokenizer.cpp>
#endif // __BUILD_STATIC_APPLICATION__

