/*
 * 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 "item.h"

namespace ledger {

bool item_t::use_aux_date = false;

bool item_t::has_tag(const string& tag, bool) const
{
  DEBUG("item.meta", "Checking if item has tag: " << tag);
  if (! metadata) {
    DEBUG("item.meta", "Item has no metadata at all");
    return false;
  }
  string_map::const_iterator i = metadata->find(tag);
#if DEBUG_ON
  if (SHOW_DEBUG("item.meta")) {
    if (i == metadata->end())
      DEBUG("item.meta", "Item does not have this tag");
    else
      DEBUG("item.meta", "Item has the tag!");
  }
#endif
  return i != metadata->end();
}

bool item_t::has_tag(const mask_t& tag_mask,
                     const optional<mask_t>& value_mask, bool) const
{
  if (metadata) {
    foreach (const string_map::value_type& data, *metadata) {
      if (tag_mask.match(data.first)) {
        if (! value_mask)
          return true;
        else if (data.second.first)
          return value_mask->match(data.second.first->to_string());
      }
    }
  }
  return false;
}

optional<value_t> item_t::get_tag(const string& tag, bool) const
{
  DEBUG("item.meta", "Getting item tag: " << tag);
  if (metadata) {
    DEBUG("item.meta", "Item has metadata");
    string_map::const_iterator i = metadata->find(tag);
    if (i != metadata->end()) {
      DEBUG("item.meta", "Found the item!");
      return (*i).second.first;
    }
  }
  return none;
}

optional<value_t> item_t::get_tag(const mask_t& tag_mask,
                                  const optional<mask_t>& value_mask,
                                  bool) const
{
  if (metadata) {
    foreach (const string_map::value_type& data, *metadata) {
      if (tag_mask.match(data.first) &&
          (! value_mask ||
           (data.second.first &&
            value_mask->match(data.second.first->to_string())))) {
        return data.second.first;
      }
    }
  }
  return none;
}

namespace {
  struct CaseInsensitiveKeyCompare
    : public std::binary_function<string, string, bool>
  {
    bool operator()(const string& s1, const string& s2) const {
      return boost::algorithm::ilexicographical_compare(s1, s2);
    }
  };
}

item_t::string_map::iterator
item_t::set_tag(const string&            tag,
                const optional<value_t>& value,
                const bool               overwrite_existing)
{
  assert(! tag.empty());

  if (! metadata)
    metadata = string_map(CaseInsensitiveKeyCompare());

  DEBUG("item.meta", "Setting tag '" << tag << "' to value '"
        << (value ? *value : string_value("<none>")) << "'");

  optional<value_t> data = value;
  if (data && (data->is_null() ||
               (data->is_string() && data->as_string().empty())))
    data = none;

  string_map::iterator i = metadata->find(tag);
  if (i == metadata->end()) {
    std::pair<string_map::iterator, bool> result
      = metadata->insert(string_map::value_type(tag, tag_data_t(data, false)));
    assert(result.second);
    return result.first;
  } else {
    if (overwrite_existing)
      (*i).second = tag_data_t(data, false);
    return i;
  }
}

void item_t::parse_tags(const char * p,
                        scope_t&     scope,
                        bool         overwrite_existing)
{
  if (! std::strchr(p, ':')) {
    if (const char * b = std::strchr(p, '[')) {
      if (*(b + 1) != '\0' &&
          (std::isdigit(*(b + 1)) || *(b + 1) == '=')) {
        if (const char * e = std::strchr(b, ']')) {
          char buf[256];
          std::strncpy(buf, b + 1, static_cast<std::size_t>(e - b - 1));
          buf[e - b - 1] = '\0';

          if (char * pp = std::strchr(buf, '=')) {
            *pp++ = '\0';
            _date_aux = parse_date(pp);
          }
          if (buf[0])
            _date = parse_date(buf);
        }
      }
    }
    return;
  }

  scoped_array<char> buf(new char[std::strlen(p) + 1]);

  std::strcpy(buf.get(), p);

  string tag;
  bool   by_value = false;
  bool   first = true;
  for (char * q = std::strtok(buf.get(), " \t");
       q;
       q = std::strtok(NULL, " \t")) {
    const string::size_type len = std::strlen(q);
    if (len < 2) continue;
    if (q[0] == ':' && q[len - 1] == ':') { // a series of tags
      for (char * r = std::strtok(q + 1, ":");
           r;
           r = std::strtok(NULL, ":")) {
        string_map::iterator i = set_tag(r, none, overwrite_existing);
        (*i).second.second = true;
      }
    }
    else if (first && q[len - 1] == ':') { // a metadata setting
      std::size_t index = 1;
      if (q[len - 2] == ':') {
        by_value = true;
        index    = 2;
      }
      tag = string(q, len - index);

      string_map::iterator i;
      string field(p + (q - buf.get()) + len);
      trim(field);
      if (by_value) {
        bind_scope_t bound_scope(scope, *this);
        i = set_tag(tag, expr_t(field).calc(bound_scope), overwrite_existing);
      } else {
        i = set_tag(tag, string_value(field), overwrite_existing);
      }
      (*i).second.second = true;
      break;
    }
    first = false;
  }
}

void item_t::append_note(const char * p,
                         scope_t&     scope,
                         bool         overwrite_existing)
{
  if (note) {
    *note += '\n';
    *note += p;
  } else {
    note = p;
  }

  parse_tags(p, scope, overwrite_existing);
}

namespace {
  value_t get_status(item_t& item) {
    return long(item.state());
  }
  value_t get_uncleared(item_t& item) {
    return item.state() == item_t::UNCLEARED;
  }
  value_t get_cleared(item_t& item) {
    return item.state() == item_t::CLEARED;
  }
  value_t get_pending(item_t& item) {
    return item.state() == item_t::PENDING;
  }

  value_t get_actual(item_t& item) {
    return ! item.has_flags(ITEM_GENERATED | ITEM_TEMP);
  }

  value_t get_date(item_t& item) {
    return item.date();
  }
  value_t get_primary_date(item_t& item) {
    return item.primary_date();
  }
  value_t get_aux_date(item_t& item) {
    if (optional<date_t> aux_date = item.aux_date())
      return *aux_date;
    return NULL_VALUE;
  }
  value_t get_note(item_t& item) {
    return item.note ? string_value(*item.note) : NULL_VALUE;
  }

  value_t has_tag(call_scope_t& args) {
    item_t& item(find_scope<item_t>(args));

    if (args.size() == 1) {
      if (args[0].is_string())
        return item.has_tag(args.get<string>(0));
      else if (args[0].is_mask())
        return item.has_tag(args.get<mask_t>(0));
      else
        throw_(std::runtime_error,
               _f("Expected string or mask for argument 1, but received %1%")
               % args[0].label());
    }
    else if (args.size() == 2) {
      if (args[0].is_mask() && args[1].is_mask())
        return item.has_tag(args.get<mask_t>(0), args.get<mask_t>(1));
      else
        throw_(std::runtime_error,
               _f("Expected masks for arguments 1 and 2, but received %1% and %2%")
               % args[0].label() % args[1].label());
    }
    else if (args.size() == 0) {
      throw_(std::runtime_error, _("Too few arguments to function"));
    }
    else {
      throw_(std::runtime_error, _("Too many arguments to function"));
    }
    return false;
  }

  value_t get_tag(call_scope_t& args)
  {
    item_t& item(find_scope<item_t>(args));
    optional<value_t> val;

    if (args.size() == 1) {
      if (args[0].is_string())
        val = item.get_tag(args.get<string>(0));
      else if (args[0].is_mask())
        val = item.get_tag(args.get<mask_t>(0));
      else
        throw_(std::runtime_error,
               _f("Expected string or mask for argument 1, but received %1%")
               % args[0].label());
    }
    else if (args.size() == 2) {
      if (args[0].is_mask() && args[1].is_mask())
        val = item.get_tag(args.get<mask_t>(0), args.get<mask_t>(1));
      else
        throw_(std::runtime_error,
               _f("Expected masks for arguments 1 and 2, but received %1% and %2%")
               % args[0].label() % args[1].label());
    }
    else if (args.size() == 0) {
      throw_(std::runtime_error, _("Too few arguments to function"));
    }
    else {
      throw_(std::runtime_error, _("Too many arguments to function"));
    }

    return val ? *val : NULL_VALUE;
  }

  value_t get_pathname(item_t& item) {
    if (item.pos)
      return string_value(item.pos->pathname.string());
    else
      return NULL_VALUE;
  }

  value_t get_filebase(item_t& item) {
    if (item.pos)
      return string_value(item.pos->pathname.filename().string());
    else
      return NULL_VALUE;
  }

  value_t get_filepath(item_t& item) {
    if (item.pos)
      return string_value(item.pos->pathname.parent_path().string());
    else
      return NULL_VALUE;
  }


  value_t get_beg_pos(item_t& item) {
    return item.pos ? long(item.pos->beg_pos) : 0L;
  }

  value_t get_beg_line(item_t& item) {
    return item.pos ? long(item.pos->beg_line) : 0L;
  }

  value_t get_end_pos(item_t& item) {
    return item.pos ? long(item.pos->end_pos) : 0L;
  }

  value_t get_end_line(item_t& item) {
    return item.pos ? long(item.pos->end_line) : 0L;
  }

  value_t get_seq(item_t& item) {
    return long(item.seq());
  }
  value_t get_id(item_t& item) {
    return string_value(item.id());
  }

  value_t get_addr(item_t& item) {
    return long(reinterpret_cast<intptr_t>(&item));
  }

  value_t get_depth(item_t&) {
    return 0L;
  }

  value_t ignore(item_t&) {
    return false;
  }

  template <value_t (*Func)(item_t&)>
  value_t get_wrapper(call_scope_t& scope) {
    return (*Func)(find_scope<item_t>(scope));
  }
}

value_t get_comment(item_t& item)
{
  if (! item.note) {
    return string_value("");
  } else {
    std::ostringstream buf;
    if (item.note->length() > 15)
      buf << "\n    ;";
    else
      buf << "  ;";

    bool need_separator = false;
    for (const char * p = item.note->c_str(); *p; p++) {
      if (*p == '\n') {
        need_separator = true;
      } else {
        if (need_separator) {
          buf << "\n    ;";
          need_separator = false;
        }
        buf << *p;
      }
    }
    return string_value(buf.str());
  }
}

void item_t::define(const symbol_t::kind_t, const string& name,
                    expr_t::ptr_op_t def)
{
  bind_scope_t bound_scope(*scope_t::default_scope, *this);
  set_tag(name, def->calc(bound_scope));
}

expr_t::ptr_op_t item_t::lookup(const symbol_t::kind_t kind,
                                const string& name)
{
  if (kind != symbol_t::FUNCTION)
    return NULL;

  switch (name[0]) {
  case 'a':
    if (name == "actual")
      return WRAP_FUNCTOR(get_wrapper<&get_actual>);
    else if (name == "actual_date")
      return WRAP_FUNCTOR(get_wrapper<&get_primary_date>);
    else if (name == "addr")
      return WRAP_FUNCTOR(get_wrapper<&get_addr>);
    else if (name == "aux_date")
      return WRAP_FUNCTOR(get_wrapper<&get_aux_date>);
    break;

  case 'b':
    if (name == "beg_line")
      return WRAP_FUNCTOR(get_wrapper<&get_beg_line>);
    else if (name == "beg_pos")
      return WRAP_FUNCTOR(get_wrapper<&get_beg_pos>);
    break;

  case 'c':
    if (name == "cleared")
      return WRAP_FUNCTOR(get_wrapper<&get_cleared>);
    else if (name == "comment")
      return WRAP_FUNCTOR(get_wrapper<&get_comment>);
    break;

  case 'd':
    if (name[1] == '\0' || name == "date")
      return WRAP_FUNCTOR(get_wrapper<&get_date>);
    else if (name == "depth")
      return WRAP_FUNCTOR(get_wrapper<&get_depth>);
    break;

  case 'e':
    if (name == "end_line")
      return WRAP_FUNCTOR(get_wrapper<&get_end_line>);
    else if (name == "end_pos")
      return WRAP_FUNCTOR(get_wrapper<&get_end_pos>);
    else if (name == "effective_date")
      return WRAP_FUNCTOR(get_wrapper<&get_aux_date>);
    break;

  case 'f':
    if (name == "filename")
      return WRAP_FUNCTOR(get_wrapper<&get_pathname>);
    else if (name == "filebase")
      return WRAP_FUNCTOR(get_wrapper<&get_filebase>);
    else if (name == "filepath")
      return WRAP_FUNCTOR(get_wrapper<&get_filepath>);
     break;

  case 'h':
    if (name == "has_tag")
      return WRAP_FUNCTOR(ledger::has_tag);
    else if (name == "has_meta")
      return WRAP_FUNCTOR(ledger::has_tag);
    break;

  case 'i':
    if (name == "is_account")
      return WRAP_FUNCTOR(get_wrapper<&ignore>);
    else if (name == "id")
      return WRAP_FUNCTOR(get_wrapper<&get_id>);
    break;

  case 'm':
    if (name == "meta")
      return WRAP_FUNCTOR(ledger::get_tag);
    break;

  case 'n':
    if (name == "note")
      return WRAP_FUNCTOR(get_wrapper<&get_note>);
    break;

  case 'p':
    if (name == "pending")
      return WRAP_FUNCTOR(get_wrapper<&get_pending>);
    else if (name == "parent")
      return WRAP_FUNCTOR(get_wrapper<&ignore>);
    else if (name == "primary_date")
      return WRAP_FUNCTOR(get_wrapper<&get_primary_date>);
    break;

  case 's':
    if (name == "status" || name == "state")
      return WRAP_FUNCTOR(get_wrapper<&get_status>);
    else if (name == "seq")
      return WRAP_FUNCTOR(get_wrapper<&get_seq>);
    break;

  case 't':
    if (name == "tag")
      return WRAP_FUNCTOR(ledger::get_tag);
    break;

  case 'u':
    if (name == "uncleared")
      return WRAP_FUNCTOR(get_wrapper<&get_uncleared>);
    else if (name == "uuid")
      return WRAP_FUNCTOR(get_wrapper<&get_id>);
    break;

  case 'v':
    if (name == "value_date")
      return WRAP_FUNCTOR(get_wrapper<&get_date>);
    break;

  case 'L':
    if (name[1] == '\0')
      return WRAP_FUNCTOR(get_wrapper<&get_actual>);
    break;

  case 'X':
    if (name[1] == '\0')
      return WRAP_FUNCTOR(get_wrapper<&get_cleared>);
    break;

  case 'Y':
    if (name[1] == '\0')
      return WRAP_FUNCTOR(get_wrapper<&get_pending>);
    break;
  }

  return NULL;
}

bool item_t::valid() const
{
  if (_state != UNCLEARED && _state != CLEARED && _state != PENDING) {
    DEBUG("ledger.validate", "item_t: state is bad");
    return false;
  }

  return true;
}

void print_item(std::ostream& out, const item_t& item, const string& prefix)
{
  out << source_context(item.pos->pathname, item.pos->beg_pos,
                        item.pos->end_pos, prefix);
}

string item_context(const item_t& item, const string& desc)
{
  if (! item.pos)
    return empty_string;

  std::streamoff len = item.pos->end_pos - item.pos->beg_pos;
  if (! (len > 0))
    return empty_string;

  assert(len < 1024 * 1024);

  std::ostringstream out;

  if (item.pos->pathname.empty()) {
    out << desc << _(" from streamed input:");
    return out.str();
  }

  out << desc << _(" from \"") << item.pos->pathname.string() << "\"";

  if (item.pos->beg_line != item.pos->end_line)
    out << _(", lines ") << item.pos->beg_line << "-"
        << item.pos->end_line << ":\n";
  else
    out << _(", line ") << item.pos->beg_line << ":\n";

  print_item(out, item, "> ");

  return out.str();
}

void put_metadata(property_tree::ptree& st, const item_t::string_map& metadata)
{
  foreach (const item_t::string_map::value_type& pair, metadata) {
    if (pair.second.first) {
      property_tree::ptree& vt(st.add("value", ""));
      vt.put("<xmlattr>.key", pair.first);
      put_value(vt, *pair.second.first);
    } else {
      st.add("tag", pair.first);
    }
  }
}

} // namespace ledger
