// -*- C++ -*-
//
// This file is part of YODA -- Yet more Objects for Data Analysis
// Copyright (C) 2008-2025 The YODA collaboration (see AUTHORS for details)
//
#include "YODA/ReaderYODA.h"
#include "YODA/Exceptions.h"
#include "YODA/Utils/StringUtils.h"
#include "YODA/Utils/getline.h"

#include <regex>

#include "yaml-cpp/yaml.h"
#ifdef YAML_NAMESPACE
#define YAML YAML_NAMESPACE
#endif

#ifdef HAVE_LIBZ
#define _XOPEN_SOURCE 700
#include "zstr/zstr.hpp"
#endif

using namespace std;

namespace YODA {


  namespace {

    template <typename... Args, class F>
    constexpr void for_each_arg(F&& f) {
      (( f(Args{}) ), ...);
    }
  }


  /// Singleton creation function
  Reader& ReaderYODA::create() {
    static ReaderYODA _instance;
    _instance.registerDefaultTypes<double,int,string>();
    return _instance;
  }

  template<typename ... Args>
  void ReaderYODA::registerDefaultTypes() {
    registerType<Counter>();
    registerType<Estimate0D>();
    registerType<Scatter1D>();
    registerType<Scatter2D>();
    registerType<Scatter3D>();

    // add BinnedHisto/BinnedProfile in 1D
    for_each_arg<Args...>([&](auto&& arg) {
      using A1 = decay_t<decltype(arg)>;
      using BH = BinnedHisto<A1>;
      registerType<BH>();
      using BP = BinnedProfile<A1>;
      registerType<BP>();
      using BE = BinnedEstimate<A1>;
      registerType<BE>();

      // add BinnedHisto/BinnedProfile in 2D
      for_each_arg<Args...>([&](auto&& arg) {
        using A2 = decay_t<decltype(arg)>;
        using BH = BinnedHisto<A1,A2>;
        registerType<BH>();
        using BP = BinnedProfile<A1,A2>;
        registerType<BP>();
        using BE = BinnedEstimate<A1,A2>;
        registerType<BE>();

        // add BinnedHisto/BinnedProfile in 3D
        for_each_arg<Args...>([&](auto&& arg) {
          using A3 = decay_t<decltype(arg)>;
          //using BH = BinnedHisto<A1,A2,A3>;
          //registerType<BH>();
          //using BP = BinnedProfile<A1,A2,A3>;
          //registerType<BP>();
          using BE = BinnedEstimate<A1,A2,A3>;
          registerType<BE>();
        });
      });
    });
    registerType<HistoND<3>>();
  }


  void ReaderYODA::read(istream& inputStream, vector<AnalysisObject*>& aos,
                                              const string& match,
                                              const string& unmatch) {

    #ifdef HAVE_LIBZ
    // NB. zstr auto-detects if file is deflated or plain-text
    zstr::istream stream(inputStream);
    #else
    istream& stream = inputStream;
    #endif

    /// State of the parser: line number, line,
    /// parser context, and pointer(s) to the object
    /// currently being assembled
    unsigned int nline = 0;
    string s;

    AnalysisObject* aocurr = nullptr; //< Generic current AO pointer

    string pathcurr, annscurr, typestr;

    TypeRegisterItr thisAOR = _register.end();

    vector<regex> patterns, unpatterns;
    for (const string& pat : Utils::split(match,   ",")) { patterns.push_back(regex(pat)); }
    for (const string& pat : Utils::split(unmatch, ",")) { unpatterns.push_back(regex(pat)); }

    // Loop over all lines of the input file
    bool in_anns = false, pattern_pass = true;
    string fmt = "1";
    //int nfmt = 1;

    while (Utils::getline(stream, s)) {
      nline += 1;

      // CLEAN LINES IF NOT IN ANNOTATION MODE
      if (pattern_pass && !in_anns) {

        Utils::itrim(s); // Trim the line

        if (s.empty())  continue; // Ignore blank lines

        // Ignore comments (whole-line only, without indent, and still allowed for compatibility on BEGIN/END lines)
        if (s.find("#") == 0 && s.find("BEGIN") == string::npos && s.find("END") == string::npos) continue;
      }


      // STARTING A NEW CONTEXT
      if (typestr == "") {

        // We require a BEGIN line to start a context
        if (s.find("BEGIN ") == string::npos) {
          stringstream ss;
          ss << "Unexpected line in YODA format parsing when BEGIN expected: '" << s << "' on line " << nline;
          throw ReadError(ss.str());
        }

        // Remove leading #s from the BEGIN line if necessary
        while (s.find("#") == 0) s = Utils::trim(s.substr(1));

        // Split into parts
        vector<string> parts;
        istringstream iss(s); string tmp;
        while (iss >> tmp) parts.push_back(tmp);

        // Extract context from BEGIN type
        if (parts.size() < 2 || parts[0] != "BEGIN") {
          stringstream ss;
          ss << "Unexpected BEGIN line structure when BEGIN expected: '" << s << "' on line " << nline;
          throw ReadError(ss.str());
        }

        // Second part is the context name
        const string ctxstr = parts[1];

        // Extract the AO type
        if (Utils::startswith(ctxstr, "YODA_")) {
          typestr = ctxstr.substr(5);
        }
        if (typestr.find("_V") == typestr.size() - 3) {
          typestr = typestr.substr(0, typestr.size()-3);
        }

        // Get block path if possible
        pathcurr = (parts.size() >= 3) ? parts[2] : "";
        pattern_pass = patternCheck(pathcurr, patterns, unpatterns);
        if (!pattern_pass)  continue;

        // Check that type has been loaded
        thisAOR = _register.find(typestr);
        if (thisAOR == _register.end())
          throw ReadError("Unexpected context found: " + typestr);

        // Get block format version if possible (assume version=1 if none found)
        const size_t vpos = ctxstr.find_last_of("V");
        fmt = vpos != string::npos ? ctxstr.substr(vpos+1) : "1";
        // cout << fmt << endl;

        // From version 2 onwards, use the in_anns state from BEGIN until ---
        if (fmt != "1") in_anns = true;

      }
      else if (s.find("BEGIN ") != string::npos) { ///< @todo require pos = 0 from fmt=V2
        throw ReadError("Unexpected BEGIN line in YODA format parsing before ending current BEGIN..END block");
      }
      else if (s.find("END ") != string::npos) { ///< @todo require pos = 0 from fmt=V2

        if (!pattern_pass) {
          pattern_pass = true;
          typestr = "";
          continue;
        }

        // FINISHING THE CURRENT CONTEXT
        // Set up AO and register it
        /// @todo Throw error if mismatch between BEGIN and END types
        //cout << "assembling " << pathcurr << endl;
        aocurr = thisAOR->second->assemble(pathcurr);

        // Set all annotations
        try {
          YAML::Node anns = YAML::Load(annscurr);
          // for (YAML::const_iterator it = anns.begin(); it != anns.end(); ++it) {
          for (const auto& it : anns) {
            const string key = it.first.as<string>();
            // const string val = it.second.as<string>();
            YAML::Emitter em;
            em << YAML::Flow << it.second; //< use single-line formatting, for lists & maps
            const string val = em.c_str();
            aocurr->setAnnotation(key, val);
          }
        } catch (...) {
          /// @todo Is there a case for just giving up on these annotations, printing the error msg, and keep going? As an option?
          const string err = "Problem during annotation parsing of YAML block:\n'''\n" + annscurr + "\n'''";
          // cerr << err << endl;
          throw ReadError(err);
        }
        annscurr.clear();
        in_anns = false;

        // Put this AO in the completed stack
        aos.push_back(aocurr);

        // Clear all current-object pointers and context
        aocurr = nullptr;
        typestr = "";

        continue;
      }
      else { //< not a BEGIN or END line

        if (!pattern_pass)  continue;

        // ANNOTATIONS PARSING
        if (fmt == "1") {
          // First convert to one-key-per-line YAML syntax
          const size_t ieq = s.find("=");
          if (ieq != string::npos) s.replace(ieq, 1, ": ");
          // Special-case treatment for syntax clashes
          const size_t icost = s.find(": *");
          if (icost != string::npos) {
            s.replace(icost, 1, ": '*");
            s += "'";
          }
          // Store reformatted annotation
          const size_t ico = s.find(":");
          if (ico != string::npos) {
            annscurr += (annscurr.empty() ? "" : "\n") + s;
            continue;
          }
        }
        else if (in_anns) {
          if (s == "---") {
            in_anns = false;
          }
          else {
            annscurr += (annscurr.empty() ? "" : "\n") + s;
          }
          continue;
        }

        // DATA PARSING
        thisAOR->second->parse(s);

      } // end of context
    } // end of while getline
  }

}
