/*
 * Copyright (c) 2003-2023, 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 "amount.h"
#include "commodity.h"
#include "annotate.h"
#include "pool.h"
#include "history.h"
#include "quotes.h"

namespace ledger {

shared_ptr<commodity_pool_t> commodity_pool_t::current_pool;

commodity_pool_t::commodity_pool_t()
  : default_commodity(NULL), keep_base(false),
    quote_leeway(86400), get_quotes(false),
    get_commodity_quote(commodity_quote_from_script)
{
  null_commodity = create("");
  null_commodity->add_flags(COMMODITY_BUILTIN | COMMODITY_NOMARKET);
  TRACE_CTOR(commodity_pool_t, "");
}

commodity_t * commodity_pool_t::create(const string& symbol)
{
  shared_ptr<commodity_t::base_t>
    base_commodity(new commodity_t::base_t(symbol));
  shared_ptr<commodity_t> commodity(new commodity_t(this, base_commodity));

  DEBUG("pool.commodities", "Creating base commodity " << symbol);

  // Create the "qualified symbol" version of this commodity's symbol
  if (commodity_t::symbol_needs_quotes(symbol)) {
    commodity->qualified_symbol = "\"";
    *commodity->qualified_symbol += symbol;
    *commodity->qualified_symbol += "\"";
  }

  DEBUG("pool.commodities", "Creating commodity '" << symbol << "'");

#if DEBUG_ON
  std::pair<commodities_map::iterator, bool> result =
#endif
    commodities.insert(commodities_map::value_type(symbol, commodity));
#if DEBUG_ON
  assert(result.second);
#endif

  commodity_price_history.add_commodity(*commodity.get());

  return commodity.get();
}

commodity_t * commodity_pool_t::find(const string& symbol)
{
  DEBUG("pool.commodities", "Find commodity " << symbol);

  commodities_map::const_iterator i = commodities.find(symbol);
  if (i != commodities.end())
    return (*i).second.get();
  return NULL;
}

commodity_t * commodity_pool_t::find_or_create(const string& symbol)
{
  DEBUG("pool.commodities", "Find-or-create commodity " << symbol);
  if (commodity_t * commodity = find(symbol))
    return commodity;
  return create(symbol);
}

commodity_t * commodity_pool_t::alias(const string& name, commodity_t& referent)
{
  commodities_map::const_iterator i = commodities.find(referent.base_symbol());
  assert(i != commodities.end());

  std::pair<commodities_map::iterator, bool> result
    = commodities.insert(commodities_map::value_type(name, (*i).second));
  assert(result.second);

  return (*result.first).second.get();
}

commodity_t *
commodity_pool_t::create(const string& symbol, const annotation_t& details)
{
  DEBUG("pool.commodities", "commodity_pool_t::create[ann] "
        << "symbol " << symbol << std::endl << details);

  if (details)
    return create(*find_or_create(symbol), details);
  else
    return create(symbol);
}

commodity_t *
commodity_pool_t::find(const string& symbol, const annotation_t& details)
{
  DEBUG("pool.commodities", "commodity_pool_t::find[ann] "
        << "symbol " << symbol << std::endl << details);

  annotated_commodities_map::const_iterator i =
    annotated_commodities.find
    (annotated_commodities_map::key_type(symbol, details));
  if (i != annotated_commodities.end()) {
    DEBUG("pool.commodities", "commodity_pool_t::find[ann] found "
          << "symbol " << (*i).second->base_symbol() << std::endl
          << as_annotated_commodity(*(*i).second.get()).details);
    return (*i).second.get();
  } else {
    return NULL;
  }
}

commodity_t *
commodity_pool_t::find_or_create(const string& symbol,
                                 const annotation_t& details)
{
  DEBUG("pool.commodities", "commodity_pool_t::find_or_create[ann] "
        << "symbol " << symbol << std::endl << details);

  if (details) {
    if (commodity_t * ann_comm = find(symbol, details)) {
      assert(ann_comm->annotated && as_annotated_commodity(*ann_comm).details);
      return ann_comm;
    } else {
      return create(symbol, details);
    }
  } else {
    return find_or_create(symbol);
  }
}

commodity_t *
commodity_pool_t::find_or_create(commodity_t& comm, const annotation_t& details)
{
  DEBUG("pool.commodities", "commodity_pool_t::find_or_create[ann:comm] "
        << "symbol " << comm.base_symbol() << std::endl << details);

  if (details) {
    if (commodity_t * ann_comm = find(comm.base_symbol(), details)) {
      assert(ann_comm->annotated && as_annotated_commodity(*ann_comm).details);
      return ann_comm;
    } else {
      return create(comm, details);
    }
  } else {
    return &comm;
  }
}

annotated_commodity_t *
commodity_pool_t::create(commodity_t&        comm,
                         const annotation_t& details)
{
  DEBUG("pool.commodities", "commodity_pool_t::create[ann:comm] "
        << "symbol " << comm.base_symbol() << std::endl << details);

  assert(comm);
  assert(! comm.has_annotation());
  assert(details);

  shared_ptr<annotated_commodity_t>
    commodity(new annotated_commodity_t(&comm, details));

  comm.add_flags(COMMODITY_SAW_ANNOTATED);
  if (details.price) {
    if (details.has_flags(ANNOTATION_PRICE_FIXATED))
      comm.add_flags(COMMODITY_SAW_ANN_PRICE_FIXATED);
    else
      comm.add_flags(COMMODITY_SAW_ANN_PRICE_FLOAT);
  }

  DEBUG("pool.commodities", "Creating annotated commodity "
        << "symbol " << commodity->base_symbol()
        << std::endl << details);

#if DEBUG_ON
  std::pair<annotated_commodities_map::iterator, bool> result =
#endif
    annotated_commodities.insert(annotated_commodities_map::value_type
                                 (annotated_commodities_map::key_type
                                  (comm.base_symbol(), details), commodity));
#if DEBUG_ON
  assert(result.second);
#endif

  return commodity.get();
}

void commodity_pool_t::exchange(commodity_t&      commodity,
                                const amount_t&   per_unit_cost,
                                const datetime_t& moment)
{
  DEBUG("commodity.prices.add", "exchanging commodity " << commodity
        << " at per unit cost " << per_unit_cost << " on " << moment);

  commodity_t& base_commodity
    (commodity.annotated ?
     as_annotated_commodity(commodity).referent() : commodity);

  base_commodity.add_price(moment, per_unit_cost);
}

cost_breakdown_t
commodity_pool_t::exchange(const amount_t&             amount,
                           const amount_t&             cost,
                           const bool                  is_per_unit,
                           const bool                  add_price,
                           const optional<datetime_t>& moment,
                           const optional<string>&     tag)
{
  DEBUG("commodity.prices.add", "exchange: " << amount << " for " << cost);
  DEBUG("commodity.prices.add", "exchange: is-per-unit   = " << is_per_unit);
#if DEBUG_ON
  if (moment)
    DEBUG("commodity.prices.add", "exchange: moment        = " << *moment);
  if (tag)
    DEBUG("commodity.prices.add", "exchange: tag           = " << *tag);
#endif

  commodity_t& commodity(amount.commodity());

  annotation_t * current_annotation = NULL;
  if (commodity.annotated)
    current_annotation = &as_annotated_commodity(commodity).details;

  amount_t per_unit_cost =
    (is_per_unit || amount.is_zero()) ? cost.abs() : (cost / amount).abs();

  if (! cost.has_commodity())
    per_unit_cost.clear_commodity();

  if (cost.has_annotation())
    per_unit_cost = per_unit_cost.strip_annotations(keep_details_t());

  DEBUG("commodity.prices.add", "exchange: per-unit-cost = " << per_unit_cost);

  // Do not record commodity exchanges where amount's commodity has a
  // fixated price, since this does not establish a market value for the
  // base commodity.
  if (add_price &&
      ! per_unit_cost.is_realzero() &&
      (current_annotation == NULL ||
       ! (current_annotation->price &&
          current_annotation->has_flags(ANNOTATION_PRICE_FIXATED))) &&
      commodity.referent() != per_unit_cost.commodity().referent()) {
    exchange(commodity, per_unit_cost, moment ? *moment : CURRENT_TIME());
  }

  cost_breakdown_t breakdown;
  breakdown.final_cost = ! is_per_unit ? cost : cost * amount.abs();

  DEBUG("commodity.prices.add",
        "exchange: final-cost    = " << breakdown.final_cost);

  if (current_annotation && current_annotation->price)
    breakdown.basis_cost
      = (*current_annotation->price * amount).unrounded();
  else
    breakdown.basis_cost = breakdown.final_cost;

  DEBUG("commodity.prices.add",
        "exchange: basis-cost    = " << breakdown.basis_cost);

  annotation_t annotation(per_unit_cost, moment ?
                          moment->date() : optional<date_t>(), tag);

  annotation.add_flags(ANNOTATION_PRICE_CALCULATED);
  if (current_annotation &&
      current_annotation->has_flags(ANNOTATION_PRICE_FIXATED))
    annotation.add_flags(ANNOTATION_PRICE_FIXATED);
  if (moment)
    annotation.add_flags(ANNOTATION_DATE_CALCULATED);
  if (tag)
    annotation.add_flags(ANNOTATION_TAG_CALCULATED);

  breakdown.amount = amount_t(amount, annotation);

  DEBUG("commodity.prices.add",
        "exchange: amount        = " << breakdown.amount);

  return breakdown;
}

optional<std::pair<commodity_t *, price_point_t> >
commodity_pool_t::parse_price_directive
  (char * line, bool do_not_add_price, bool no_date)
{
  char * date_field_ptr = line;
  char * time_field_ptr = next_element(date_field_ptr);
  if (! time_field_ptr) return none;
  string date_field = date_field_ptr;

  char *     symbol_and_price;
  datetime_t datetime;
  string     symbol;

  if (! no_date && std::isdigit(time_field_ptr[0])) {
    symbol_and_price = next_element(time_field_ptr);
    if (! symbol_and_price) return none;

    datetime = parse_datetime(date_field + " " + time_field_ptr);
  }
  else if (! no_date && std::isdigit(date_field_ptr[0])) {
    symbol_and_price = time_field_ptr;
    datetime = datetime_t(parse_date(date_field));
  }
  else {
    symbol = date_field_ptr;
    symbol_and_price = time_field_ptr;
    datetime = CURRENT_TIME();
  }

  if (symbol.empty())
    commodity_t::parse_symbol(symbol_and_price, symbol);

  price_point_t point;
  point.when = datetime;
  point.price.parse(symbol_and_price, PARSE_NO_MIGRATE);
  VERIFY(point.price.valid());

  DEBUG("commodity.download", "Looking up symbol: " << symbol);
  if (commodity_t * commodity = find_or_create(symbol)) {
    DEBUG("commodity.download", "Adding price for " << symbol << ": "
          << point.when << " " << point.price);
    if (! do_not_add_price)
      commodity->add_price(point.when, point.price, true);
    commodity->add_flags(COMMODITY_KNOWN);
    return std::pair<commodity_t *, price_point_t>(commodity, point);
  }

  return none;
}

commodity_t *
commodity_pool_t::parse_price_expression(const std::string&          str,
                                         const bool                  add_prices,
                                         const optional<datetime_t>& moment)
{
  scoped_array<char> buf(new char[str.length() + 1]);

  std::strcpy(buf.get(), str.c_str());

  char * price = std::strchr(buf.get(), '=');
  if (price)
    *price++ = '\0';

  if (commodity_t * commodity = find_or_create(trim_ws(buf.get()))) {
    if (price && add_prices) {
      for (char * p = std::strtok(price, ";");
           p;
           p = std::strtok(NULL, ";")) {
        commodity->add_price(moment ? *moment : CURRENT_TIME(), amount_t(p));
      }
    }
    return commodity;
  }
  return NULL;
}

} // namespace ledger
