// -*- C++ -*-
//
// This file is part of YODA -- Yet more Objects for Data Analysis
// Copyright (C) 2008-2025 The YODA collaboration (see AUTHORS for details)
//
#ifndef YODA_READER_H
#define YODA_READER_H

#include "YODA/Config/BuildConfig.h"
#include "YODA/Utils/ReaderUtils.h"
#include "YODA/Utils/Traits.h"

#ifdef HAVE_HDF5
#include <YODA/Config/BuildConfig.h>
#ifdef WITH_HIGHFIVE
#include <YODA/highfive/H5File.hpp>
#else
#include <highfive/H5File.hpp>
#define YODA_H5 HighFive
#endif
#endif

#include <memory>
#include <fstream>
#include <iostream>
#include <sstream>
#include <type_traits>
#include <unordered_map>
#include <vector>

namespace YODA {


  /// Pure virtual base class for various output writers.
  class Reader {

  public:

    /// Convenience alias for AO Reader
    using TypeRegisterItr = typename std::unordered_map<std::string, std::unique_ptr<AOReaderBase>>::const_iterator;

    /// Virtual destructor
    virtual ~Reader() {
      // This is technically leaking memory, but since this
      // is a (static) singleton, there should be no issue.
      // Cython relies on it for data-structure alignment, too.
      for (auto& aor : _register) { aor.second.release(); }
    }


    /// @name Reading multiple analysis objects,
    /// @{

    /// @brief Read in a collection of objects @a objs from output stream @a stream.
    ///
    /// This version fills (actually, appends to) a variable supplied container
    /// Note: SFINAE is used to check for a void push_back(const AnalysisObject*) method
    ///
    /// @todo Extend SFINAE Pushable cf. Writer to allow adding to containers of smart ptr type
    template<typename CONT>
    typename std::enable_if_t<YODA::Pushable<CONT,AnalysisObject*>::value>
    read(std::istream& stream, CONT& aos, const std::string& match = "", const std::string& unmatch = "") {
      // if CONT==std::vector<AnalysisObject*>, the compiler should select
      // the virtual method below, since it prefers non-templated methods in the lookup
      // otherwise we would enter a endless recursion. Check in case of problems.
      std::vector<AnalysisObject*> v_aos;
      read(stream, v_aos, match, unmatch);
      for (const AnalysisObject* ao : v_aos) aos.push_back(ao);
    }

    /// @brief Read in a collection of objects @a objs from output stream @a stream.
    ///
    /// This version fills (actually, appends to) a supplied vector, avoiding copying,
    /// and is hence CPU efficient.
    ///
    virtual void read(std::istream& stream, std::vector<AnalysisObject*>& aos,
                      const std::string& match = "", const std::string& unmatch = "") = 0;

    /// @brief Read in a collection of objects @a objs from an HDF5 @a file.
    ///
    /// This version fills (actually, appends to) a supplied vector, avoiding copying,
    /// and is hence CPU efficient.
    ///
    #ifdef HAVE_HDF5
    virtual void read(const YODA_H5::File& file, std::vector<AnalysisObject*>& aos,
                      const std::string& match = "", const std::string& unmatch = "") = 0;
    #endif

    /// @brief Read in a collection of objects from output stream @a stream.
    ///
    /// This version returns a vector by value, involving copying, and is hence less
    /// CPU efficient than the alternative version where a vector is filled by reference.
    std::vector<AnalysisObject*> read(std::istream& stream, const std::string& match = "",
                                                            const std::string& unmatch = "") {
      std::vector<AnalysisObject*> rtn;
      read(stream, rtn, match, unmatch);
      return rtn;
    }


    /// @brief Read in a collection of objects from an H5 @a file.
    ///
    /// This version returns a vector by value, involving copying, and is hence less
    /// CPU efficient than the alternative version where a vector is filled by reference.
    #ifdef HAVE_HDF5
    std::vector<AnalysisObject*> read(const YODA_H5::File& file, const std::string& match = "",
                                                                  const std::string& unmatch = "") {
      std::vector<AnalysisObject*> rtn;
      read(file, rtn, match, unmatch);
      return rtn;
    }
    #endif


    /// @brief Read in a collection of objects @a objs from file @a filename.
    ///
    ///
    /// This version fills (actually, appends to) a variable supplied container
    /// Note: SFINAE is used to check for a void push_back(const AnalysisObject*) method
    ///
    /// @todo Extend SFINAE Pushable cf. Writer to allow adding to containers of smart ptr type
    template<typename CONT>
    typename std::enable_if_t<YODA::Pushable<CONT,AnalysisObject*>::value>
    read(const std::string& filename, CONT& aos, const std::string& match = "", const std::string& unmatch = "") {
      // if CONT==std::vector<AnalysisObject*>, the compiler should select
      // the virtual method below, since it prefers non-templated methods in the lookup
      // otherwise we would enter a endless recursion. Check in case of problems.
      std::vector<AnalysisObject*> v_aos;
      read(filename, v_aos, match, unmatch);
      for (const AnalysisObject* ao : v_aos) aos.push_back(ao);
    }

    /// @brief Read in a collection of objects @a objs from file @a filename.
    ///
    /// This version fills (actually, appends to) a supplied vector, avoiding copying,
    /// and is hence CPU efficient.
    ///
    void read(const std::string& filename, std::vector<AnalysisObject*>& aos,
              const std::string& match = "", const std::string& unmatch = "") {
      if (filename != "-") {
        try {
          const size_t lastdot = filename.find_last_of(".");
          std::string fmt = Utils::toLower(lastdot == std::string::npos ? filename : filename.substr(lastdot+1));
          #ifdef HAVE_HDF5
          // check if the requested format is H5
          if (Utils::startswith(fmt, "h5")) {
            try {
              const YODA_H5::File h5(filename, YODA_H5::File::ReadOnly);
              read(h5, aos, match, unmatch);
            } catch (YODA::ReadError& e) {
              throw ReadError("Reading from filename " + filename + " failed: " + e.what());
            }
            return;
          }
          #endif
          std::ifstream instream;
          instream.open(filename.c_str());
          if (instream.fail())
            throw ReadError("Reading from filename " + filename + " failed");
          read(instream, aos, match, unmatch);
          instream.close();
        } catch (std::ifstream::failure& e) {
          throw ReadError("Reading from filename " + filename + " failed: " + e.what());
        }
      } else {
        try {
          read(std::cin, aos, match, unmatch);
        } catch (std::runtime_error& e) {
          throw ReadError("Reading from stdin failed: " + std::string(e.what()));
        }
      }
    }

    /// @brief Read in a collection of objects from output stream @a stream.
    ///
    /// This version returns a vector by value, involving copying, and is hence less
    /// CPU efficient than the alternative version where a vector is filled by reference.
    std::vector<AnalysisObject*> read(const std::string& filename, const std::string& match = "",
                                                                   const std::string& unmatch = "") {
      std::vector<AnalysisObject*> rtn;
      read(filename, rtn, match, unmatch);
      return rtn;
    }

    /// @}

    /// @name Utilities
    /// @{

    /// @brief AO type registration
    template<typename T>
    void registerType() {
      const string key = Utils::toUpper(T().type());
      const TypeRegisterItr& res = _register.find(key);
      if (res == _register.end())  _register[key] = std::make_unique<AOReader<T>>();
    }

    /// @brief Check if a string matches any of the given @a patterns,
    /// and that it doesn't match any @a unpatterns (for path filtering)
    bool patternCheck(const std::string& path, const std::vector<std::regex>& patterns,
                                               const std::vector<std::regex>& unpatterns) {
      bool skip = false;
      if (patterns.size()) {
        skip = true;
        for (const std::regex& re : patterns) {
          if (std::regex_search(path, re)) { skip = false; break; }
        }
      }
      if (!skip && unpatterns.size()) {
        for (const std::regex& re : unpatterns) {
          if (std::regex_search(path, re)) { skip = true; break; }
        }
      }
      return !skip;
    }

    /// @}

    /// Map of registered AO Readers
    std::unordered_map<string, std::unique_ptr<AOReaderBase>> _register;

  };


  /// Factory function to make a reader object by format name or a filename
  Reader& mkReader(const std::string& format_name);


}

#endif
