/*
 * Copyright (c) 2003-2009, John Wiegley.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * - Neither the name of New Artisans LLC nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <system.hh>

#include "global.h"
#if defined(HAVE_BOOST_PYTHON)
#include "pyinterp.h"
#else
#include "session.h"
#endif
#include "item.h"
#include "journal.h"
#include "pool.h"

namespace ledger {

static bool args_only = false;

global_scope_t::global_scope_t(char ** envp)
{
  TRACE_CTOR(global_scope_t, "");

#if defined(HAVE_BOOST_PYTHON)
  if (! python_session.get()) {
    python_session.reset(new ledger::python_interpreter_t);
    session_ptr = python_session;
  }
#else
  session_ptr.reset(new session_t);
#endif

  set_session_context(session_ptr.get());

  // Create the report object, which maintains state relating to each
  // command invocation.  Because we're running from main(), the
  // distinction between session and report doesn't really matter, but if
  // a GUI were calling into Ledger it would have one session object per
  // open document, with a separate report_t object for each report it
  // generated.
  report_stack.push_front(new report_t(session()));

  // Read the user's options, in the following order:
  //
  //  1. environment variables (LEDGER_<option>)
  //  2. initialization file (~/.ledgerrc)
  //  3. command-line (--option or -o)
  //
  // Before processing command-line options, we must notify the session object
  // that such options are beginning, since options like -f cause a complete
  // override of files found anywhere else.
  if (! args_only) {
    session().set_flush_on_next_data_file(true);
    read_environment_settings(envp);
    session().set_flush_on_next_data_file(true);
    read_init();
  } else {
    session().HANDLER(price_db_).off();
  }
}

global_scope_t::~global_scope_t()
{
  TRACE_DTOR(global_scope_t);

  // If memory verification is being performed (which can be very slow),
  // clean up everything by closing the session and deleting the session
  // object, and then shutting down the memory tracing subsystem.
  // Otherwise, let it all leak because we're about to exit anyway.
  IF_VERIFY() set_session_context(NULL);

#if defined(HAVE_BOOST_PYTHON)
  python_session.reset();
#endif
}

void global_scope_t::read_init()
{
  if (HANDLED(init_file_)) {
    path init_file(HANDLER(init_file_).str());
    if (exists(init_file)) {
      TRACE_START(init, 1, "Read initialization file");

      ifstream init(init_file);

      if (session().read_journal(init_file, NULL, &report()) > 0 ||
	  session().journal->auto_xacts.size() > 0 ||
	  session().journal->period_xacts.size() > 0) {
	throw_(parse_error, _("Transactions found in initialization file '%1'")
	       << init_file);
      }

      TRACE_FINISH(init, 1);
    }
  }
}

char * global_scope_t::prompt_string()
{
  static char prompt[32];
  std::size_t i;
  for (i = 0; i < report_stack.size(); i++)
    prompt[i] = ']';
  prompt[i++] = ' ';
  prompt[i]   = '\0';
  return prompt;
}

void global_scope_t::report_error(const std::exception& err)
{
  std::cout.flush();		// first display anything that was pending

  if (caught_signal == NONE_CAUGHT) {
    // Display any pending error context information
    string context = error_context();
    if (! context.empty())
      std::cerr << context << std::endl;

    std::cerr << _("Error: ") << err.what() << std::endl;
  } else {
    caught_signal = NONE_CAUGHT;
  }
}

void global_scope_t::execute_command(strings_list args, bool at_repl)
{
  session().set_flush_on_next_data_file(true);

  // Process the command verb, arguments and options
  if (at_repl) {
    args = read_command_arguments(report(), args);
    if (args.empty())
      return;
  }

  strings_list::iterator arg  = args.begin();
  string		 verb = *arg++;

  // Look for a precommand first, which is defined as any defined function
  // whose name starts with "ledger_precmd_".  The difference between a
  // precommand and a regular command is that precommands ignore the journal
  // data file completely, nor is the user's init file read.
  //
  // Here are some examples of pre-commands:
  //
  //   parse STRING       ; show how a value expression is parsed
  //   eval STRING        ; simply evaluate a value expression
  //   format STRING      ; show how a format string is parsed
  //
  // If such a command is found, create the output stream for the result and
  // then invoke the command.

  function_t   command;
  bool	       is_precommand = false;
  bind_scope_t bound_scope(*this, report());

  if (bool(command = look_for_precommand(bound_scope, verb)))
    is_precommand = true;

  // If it is not a pre-command, then parse the user's ledger data at this
  // time if not done alreday (i.e., if not at a REPL).  Then patch up the
  // report options based on the command verb.

  if (! is_precommand) {
    if (! at_repl)
      session().read_journal_files();

    normalize_report_options(verb);

    if (! bool(command = look_for_command(bound_scope, verb)))
      throw_(std::logic_error, _("Unrecognized command '%1'") << verb);
  }

  // Create the output stream (it might be a file, the console or a PAGER
  // subprocess) and invoke the report command.  The output stream is closed
  // by the caller of this function.

  report().output_stream
    .initialize(report().HANDLED(output_) ?
		optional<path>(path(report().HANDLER(output_).str())) :
		optional<path>(),
		report().HANDLED(pager_) ?
		optional<path>(path(report().HANDLER(pager_).str())) :
		optional<path>());

  // Now that the output stream is initialized, report the options that will
  // participate in this report, if the user specified --options

  if (HANDLED(options))
    report_options(report(), report().output_stream);

  // Create an argument scope containing the report command's arguments, and
  // then invoke the command.  The bound scope causes lookups to happen
  // first in the global scope, and then in the report scope.

  call_scope_t command_args(bound_scope);
  for (strings_list::iterator i = arg; i != args.end(); i++)
    command_args.push_back(string_value(*i));

  INFO_START(command, "Finished executing command");
  command(command_args);
  INFO_FINISH(command);
}

int global_scope_t::execute_command_wrapper(strings_list args, bool at_repl)
{
  int status = 1;

  try {
    if (at_repl) push_report();
    execute_command(args, at_repl);
    if (at_repl) pop_report();

    // If we've reached this point, everything succeeded fine.  Ledger uses
    // exceptions to notify of error conditions, so if you're using gdb,
    // just type "catch throw" to find the source point of any error.
    status = 0;
  }
  catch (const std::exception& err) {
    if (at_repl) pop_report();
    report_error(err);
  }

  return status;
}

void global_scope_t::report_options(report_t& report, std::ostream& out)
{
  out << "<=============================================================================>"
      << std::endl;
  out << "[Global scope options]" << std::endl;

  HANDLER(args_only).report(out);
  HANDLER(debug_).report(out);
  HANDLER(init_file_).report(out);
  HANDLER(script_).report(out);
  HANDLER(trace_).report(out);
  HANDLER(verbose).report(out);
  HANDLER(verify).report(out);

  out << std::endl << "[Session scope options]" << std::endl;
  report.session.report_options(out);

  out << std::endl << "[Report scope options]" << std::endl;
  report.report_options(out);
  out << "<=============================================================================>"
      << std::endl;
}

option_t<global_scope_t> * global_scope_t::lookup_option(const char * p)
{
  switch (*p) {
  case 'a':
    OPT(args_only);
    break;
  case 'd':
    OPT(debug_);
    break;
  case 'f':
    OPT(full_help);
    break;
  case 'h':
    OPT_(help);
    else OPT(help_calc);
    else OPT(help_comm);
    else OPT(help_disp);
    break;
  case 'i':
    OPT(init_file_);
    break;
  case 'o':
    OPT(options);
    break;
  case 's':
    OPT(script_);
    break;
  case 't':
    OPT(trace_);
    break;
  case 'v':
    OPT_(verbose);
    else OPT(verify);
    else OPT(version);
    break;
  }
  return NULL;
}

expr_t::ptr_op_t global_scope_t::lookup(const string& name)
{
  const char * p = name.c_str();
  switch (*p) {
  case 'o':
    if (WANT_OPT()) { p += OPT_PREFIX_LEN;
      if (option_t<global_scope_t> * handler = lookup_option(p))
	return MAKE_OPT_HANDLER(global_scope_t, handler);
    }
    break;

  case 'p':
    if (WANT_PRECMD()) { const char * q = p + PRECMD_PREFIX_LEN;
      switch (*q) {
      case 'p':
	if (is_eq(q, "push"))
	  return MAKE_FUNCTOR(global_scope_t::push_command);
	else if (is_eq(q, "pop"))
	  return MAKE_FUNCTOR(global_scope_t::pop_command);
	break;
      }
    }
    break;
  }

  // If you're wondering how symbols from report() will be found, it's
  // because of the bind_scope_t object in execute_command() below.
  return NULL;
}

void global_scope_t::read_environment_settings(char * envp[])
{
  TRACE_START(environment, 1, "Processed environment variables");

  process_environment(const_cast<const char **>(envp), "LEDGER_", report());

#if 1
  // These are here for backwards compatability, but are deprecated.

  if (const char * p = std::getenv("LEDGER")) {
    if (! std::getenv("LEDGER_FILE"))
      process_option("environ", "file", report(), p, "LEDGER");
  }
  if (const char * p = std::getenv("LEDGER_INIT")) {
    if (! std::getenv("LEDGER_INIT_FILE"))
      process_option("environ", "init-file", report(), p, "LEDGER_INIT");
  }
  if (const char * p = std::getenv("PRICE_HIST")) {
    if (! std::getenv("LEDGER_PRICEDB"))
      process_option("environ", "price-db", report(), p, "PRICE_HIST");
  }
  if (const char * p = std::getenv("PRICE_EXP"))
    process_option("environ", "price-exp", report(), p, "PRICE_EXP");
#endif

  TRACE_FINISH(environment, 1);
}

strings_list
global_scope_t::read_command_arguments(scope_t& scope, strings_list args)
{
  TRACE_START(arguments, 1, "Processed command-line arguments");

  strings_list remaining = process_arguments(args, scope);

  TRACE_FINISH(arguments, 1);

  return remaining;
}

void global_scope_t::normalize_session_options()
{
  INFO("Initialization file is " << HANDLER(init_file_).str());
  INFO("Price database is " << session().HANDLER(price_db_).str());

  foreach (const path& pathname, session().HANDLER(file_).data_files)
    INFO("Journal file is " << pathname.string());
}

function_t global_scope_t::look_for_precommand(scope_t&	     scope,
					       const string& verb)
{
  if (expr_t::ptr_op_t def = scope.lookup(string(PRECMD_PREFIX) + verb))
    return def->as_function();
  else
    return function_t();
}

function_t global_scope_t::look_for_command(scope_t&	  scope,
					    const string& verb)
{
  if (expr_t::ptr_op_t def = scope.lookup(string(CMD_PREFIX) + verb))
    return def->as_function();
  else
    return function_t();
}

void global_scope_t::normalize_report_options(const string& verb)
{
  // Patch up some of the reporting options based on what kind of
  // command it was.

  report_t& rep(report());

#ifdef HAVE_ISATTY
  if (! rep.HANDLED(force_color)) {
    if (! rep.HANDLED(no_color) && isatty(STDOUT_FILENO))
      rep.HANDLER(color).on_only(string("?normalize"));
    if (rep.HANDLED(color) && ! isatty(STDOUT_FILENO))
      rep.HANDLER(color).off();
  }
  if (! rep.HANDLED(force_pager)) {
    if (rep.HANDLED(pager_) && ! isatty(STDOUT_FILENO))
      rep.HANDLER(pager_).off();
  }
#endif

  // jww (2009-02-09): These globals are a hack, but hard to avoid.
  item_t::use_effective_date = (rep.HANDLED(effective) &&
				! rep.HANDLED(actual_dates));

  rep.session.commodity_pool->keep_base	 = rep.HANDLED(base);
  rep.session.commodity_pool->get_quotes = rep.session.HANDLED(download);

  if (rep.session.HANDLED(price_exp_))
    rep.session.commodity_pool->quote_leeway =
      rep.session.HANDLER(price_exp_).value.as_long();

  if (rep.session.HANDLED(price_db_))
    rep.session.commodity_pool->price_db = rep.session.HANDLER(price_db_).str();
  else
    rep.session.commodity_pool->price_db = none;

  if (rep.HANDLED(date_format_))
    set_date_format(rep.HANDLER(date_format_).str().c_str());
  if (rep.HANDLED(datetime_format_))
    set_datetime_format(rep.HANDLER(datetime_format_).str().c_str());
  if (rep.HANDLED(start_of_week_)) {
    if (optional<date_time::weekdays> weekday =
	string_to_day_of_week(rep.HANDLER(start_of_week_).str()))
      start_of_week = *weekday;
  }

  // jww (2008-08-14): This code really needs to be rationalized away for 3.0.
  // I might be able to do it with command objects, like register_t, which
  // each know how to adjust the report based on its current option settings.
  if (verb == "print" || verb == "xact" || verb == "dump") {
    rep.HANDLER(related).on_only(string("?normalize"));
    rep.HANDLER(related_all).on_only(string("?normalize"));
  }
  else if (verb == "equity") {
    rep.HANDLER(equity).on_only(string("?normalize"));
  }
  else if (rep.HANDLED(related)) {
    if (verb[0] == 'r') {
      rep.HANDLER(invert).on_only(string("?normalize"));
    } else {
      rep.HANDLER(subtotal).on_only(string("?normalize"));
      rep.HANDLER(related_all).on_only(string("?normalize"));
    }
  }

  if (verb == "print")
    rep.HANDLER(limit_).on(string("?normalize"), "actual");

  if (! rep.HANDLED(empty))
    rep.HANDLER(display_).on(string("?normalize"), "amount|(!post&total)");

  if (verb[0] != 'b' && verb[0] != 'r')
    rep.HANDLER(base).on_only(string("?normalize"));

  // If a time period was specified with -p, check whether it also gave a
  // begin and/or end to the report period (though these can be overridden
  // using -b or -e).  Then, if no _duration_ was specified (such as monthly),
  // then ignore the period since the begin/end are the only interesting
  // details.
  if (rep.HANDLED(period_)) {
    if (! rep.HANDLED(sort_all_))
      rep.HANDLER(sort_xacts_).on_only(string("?normalize"));

    date_interval_t interval(rep.HANDLER(period_).str());

    if (! rep.HANDLED(begin_) && interval.start) {
      string predicate =
	"date>=[" + to_iso_extended_string(*interval.start) + "]";
      rep.HANDLER(limit_).on(string("?normalize"), predicate);
    }
    if (! rep.HANDLED(end_) && interval.end) {
      string predicate =
	"date<[" + to_iso_extended_string(*interval.end) + "]";
      rep.HANDLER(limit_).on(string("?normalize"), predicate);
    }

    if (! interval.duration)
      rep.HANDLER(period_).off();
  }

  // If -j or -J were specified, set the appropriate format string now so as
  // to avoid option ordering issues were we to have done it during the
  // initial parsing of the options.
  if (rep.HANDLED(amount_data)) {
    rep.HANDLER(format_)
      .on_with(string("?normalize"), rep.HANDLER(plot_amount_format_).value);
  }
  else if (rep.HANDLED(total_data)) {
    rep.HANDLER(format_)
      .on_with(string("?normalize"), rep.HANDLER(plot_total_format_).value);
  }

  // If the --exchange (-X) option was used, parse out any final price
  // settings that may be there.
  if (rep.HANDLED(exchange_) &&
      rep.HANDLER(exchange_).str().find('=') != string::npos) {
    value_t(0L).exchange_commodities(rep.HANDLER(exchange_).str(), true,
				     rep.terminus);
  }

  long cols = 0;
  if (rep.HANDLED(columns_))
    cols = rep.HANDLER(columns_).value.to_long();
  else if (const char * columns = std::getenv("COLUMNS"))
    cols = lexical_cast<long>(columns);

  if (cols > 0) {
    DEBUG("auto.columns", "cols = " << cols);

    if (! rep.HANDLER(date_width_).specified)
      rep.HANDLER(date_width_)
	.on_with(none, format_date(CURRENT_DATE(), FMT_PRINTED).length());

    long date_width    = rep.HANDLER(date_width_).value.to_long();
    long payee_width   = (rep.HANDLER(payee_width_).specified ?
			  rep.HANDLER(payee_width_).value.to_long() :
			  int(double(cols) * 0.263157));
    long account_width = (rep.HANDLER(account_width_).specified ?
			  rep.HANDLER(account_width_).value.to_long() :
			  int(double(cols) * 0.302631));
    long amount_width  = (rep.HANDLER(amount_width_).specified ?
			  rep.HANDLER(amount_width_).value.to_long() :
			  int(double(cols) * 0.157894));
    long total_width   = (rep.HANDLER(total_width_).specified ?
			  rep.HANDLER(total_width_).value.to_long() :
			  amount_width);

    DEBUG("auto.columns", "date_width	 = " << date_width);
    DEBUG("auto.columns", "payee_width	 = " << payee_width);
    DEBUG("auto.columns", "account_width = " << account_width);
    DEBUG("auto.columns", "amount_width	 = " << amount_width);
    DEBUG("auto.columns", "total_width	 = " << total_width);

    if (! rep.HANDLER(date_width_).specified &&
	! rep.HANDLER(payee_width_).specified &&
	! rep.HANDLER(account_width_).specified &&
	! rep.HANDLER(amount_width_).specified &&
	! rep.HANDLER(total_width_).specified) {
      long total = (4 /* the spaces between */ + date_width + payee_width +
		    account_width + amount_width + total_width);
      if (total > cols) {
	DEBUG("auto.columns", "adjusting account down");
	account_width -= total - cols;
	DEBUG("auto.columns", "account_width now = " << account_width);
      }
    }

    if (! rep.HANDLER(date_width_).specified)
      rep.HANDLER(date_width_).on_with(string("?normalize"), date_width);
    if (! rep.HANDLER(payee_width_).specified)
      rep.HANDLER(payee_width_).on_with(string("?normalize"), payee_width);
    if (! rep.HANDLER(account_width_).specified)
      rep.HANDLER(account_width_).on_with(string("?normalize"), account_width);
    if (! rep.HANDLER(amount_width_).specified)
      rep.HANDLER(amount_width_).on_with(string("?normalize"), amount_width);
    if (! rep.HANDLER(total_width_).specified)
      rep.HANDLER(total_width_).on_with(string("?normalize"), total_width);
  }
}

void global_scope_t::visit_man_page() const
{
  int pid = fork();
  if (pid < 0) {
    throw std::logic_error(_("Failed to fork child process"));
  }
  else if (pid == 0) {	// child
    execlp("man", "man", "1", "ledger", NULL);

    // We should never, ever reach here
    perror("execlp: man");
    exit(1);
  }

  int status = -1;
  wait(&status);
  exit(0);			// parent
}

void handle_debug_options(int argc, char * argv[])
{
  for (int i = 1; i < argc; i++) {
    if (argv[i][0] == '-') {
      if (std::strcmp(argv[i], "--args-only") == 0) {
	args_only = true;
      }
      else if (std::strcmp(argv[i], "--verify") == 0) {
#if defined(VERIFY_ON)
	verify_enabled = true; // global in utils.h
#endif
      }
      else if (std::strcmp(argv[i], "--verbose") == 0 ||
	       std::strcmp(argv[i], "-v") == 0) {
#if defined(LOGGING_ON)
	_log_level = LOG_INFO; // global in utils.h
#endif
      }
      else if (i + 1 < argc && std::strcmp(argv[i], "--debug") == 0) {
#if defined(DEBUG_ON)
	_log_level    = LOG_DEBUG; // global in utils.h
	_log_category = argv[i + 1]; // global in utils.h
	i++;
#endif
      }
      else if (i + 1 < argc && std::strcmp(argv[i], "--trace") == 0) {
#if defined(TRACING_ON)
	_log_level   = LOG_TRACE; // global in utils.h
	try {
	  // global in utils.h
	  _trace_level = boost::lexical_cast<uint8_t>(argv[i + 1]);
	}
	catch (const boost::bad_lexical_cast& e) {
	  throw std::logic_error(_("Argument to --trace must be an integer"));
	}
	i++;
#endif
      }
    }
  }
}

} // namespace ledger