//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      Device/IO/IOFactory.cpp
//! @brief     Implements data import/export functions.
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2018
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "Device/IO/IOFactory.h"
#include "Base/Util/Assert.h"
#include "Base/Util/PathUtil.h"
#include "Base/Util/StringUtil.h"
#include "Device/Data/Datafield.h"
#include "Device/Histo/DiffUtil.h"
#include "Device/Histo/SimulationResult.h"
#include "Device/IO/ImportSettings.h"
#include "Device/IO/ParseUtil.h"
#include "Device/IO/ReadReflectometry.h"
#include "Device/IO/ReadRefsans.h"
#include "Device/IO/ReadWriteINT.h"
#include "Device/IO/ReadWriteNicos.h"
#include "Device/IO/ReadWriteNumpyTXT.h"
#include "Device/IO/ReadWriteTiff.h"
#include <algorithm>
#include <cctype>
#include <exception>
#include <fstream>
#include <iostream>
#include <memory>
#include <sstream>

#ifdef _WIN32
#pragma warning(push)
#pragma warning(disable : 4244 4275)
#include "Device/IO/boost_streams.h"
#pragma warning(pop)
#else
#include "Device/IO/boost_streams.h"
#endif

namespace {

const std::string GzipExtension = ".gz";
const std::string BzipExtension = ".bz2";

bool isGZipped(const std::string& file_name)
{
    return Base::Path::hasExtension(file_name, GzipExtension);
}

bool isBZipped(const std::string& file_name)
{
    return Base::Path::hasExtension(file_name, BzipExtension);
}

std::string uncompressedExtension(std::string file_name)
{
    if (isGZipped(file_name))
        file_name = file_name.substr(0, file_name.size() - GzipExtension.size());
    else if (isBZipped(file_name))
        file_name = file_name.substr(0, file_name.size() - BzipExtension.size());

    return Base::String::to_lower(Base::Path::extension(file_name));
}

bool is_binary2D(const std::string& file_name)
{
    const IO::Filetype2D ftype = IO::filename2type2D(file_name);
    return ftype == IO::tiff || IO::isCompressed(file_name);
}

std::stringstream file2stream(const std::string& file_name)
{
    if (!Base::Path::IsFileExists(file_name))
        throw std::runtime_error("File does not exist: " + file_name);

    using namespace Util::Parse;
    std::ifstream input_stream;
    std::ios_base::openmode openmode = std::ios::in;
    if (is_binary2D(file_name))
        openmode |= std::ios_base::binary;

#ifdef _WIN32
    input_stream.open(Base::Path::convert_utf8_to_utf16(file_name), openmode);
#else
    input_stream.open(file_name, openmode);
#endif

    if (!input_stream.is_open())
        throw std::runtime_error("Cannot open file for reading: " + file_name);
    if (!input_stream.good())
        throw std::runtime_error("File is not good, probably it is a directory:" + file_name);

    boost::iostreams::filtering_streambuf<boost::iostreams::input> input_filtered;
    if (::isGZipped(file_name))
        input_filtered.push(boost::iostreams::gzip_decompressor());
    else if (::isBZipped(file_name))
        input_filtered.push(boost::iostreams::bzip2_decompressor());
    input_filtered.push(input_stream);
    // we use stringstream since it provides random access which is important for tiff files
    std::stringstream str;
    boost::iostreams::copy(input_filtered, str);

    return str;
}

void stream2file(const std::string& file_name, std::stringstream& s)
{
    using namespace Util::Parse;

    std::ofstream fout;
    std::ios_base::openmode openmode = std::ios::out;
    if (is_binary2D(file_name))
        openmode |= std::ios_base::binary;

#ifdef _WIN32
    fout.open(Base::Path::convert_utf8_to_utf16(file_name), openmode);
#else
    fout.open(file_name, openmode);
#endif

    if (!fout.is_open())
        throw std::runtime_error("Cannot open file for writing: " + file_name);
    if (!fout.good())
        throw std::runtime_error("File is not good, probably it is a directory: " + file_name);

    boost::iostreams::filtering_streambuf<boost::iostreams::input> input_filtered;
    if (::isGZipped(file_name))
        input_filtered.push(boost::iostreams::gzip_compressor());
    else if (::isBZipped(file_name))
        input_filtered.push(boost::iostreams::bzip2_compressor());
    input_filtered.push(s);

    boost::iostreams::copy(input_filtered, fout);

    fout.close();
}

} // namespace


bool IO::isCompressed(const std::string& name)
{
    return isGZipped(name) || isBZipped(name);
}

IO::Filetype1D IO::filename2type1D(const std::string& filename)
{
    std::string s = ::uncompressedExtension(filename);

    if (s == ".int")
        return bornagain1D;
    if (s == ".mft")
        return mft;
    return csv1D;
}

IO::Filetype2D IO::filename2type2D(const std::string& filename)
{
    std::string s = ::uncompressedExtension(filename);

    if (s == ".int")
        return bornagain2D;
    if (s == ".001")
        return nicos2D;
    if (s == ".tif" || s == ".tiff")
        return tiff;
    return csv2D;
}

Datafield* IO::readData1D(const std::string& file_name, Filetype1D ftype,
                          const ImportSettings1D* importSettings)
{
    if (ftype == unknown1D)
        ftype = filename2type1D(file_name);

    std::stringstream s = ::file2stream(file_name);

    if (ftype == csv1D) {
        if (!importSettings)
            throw std::runtime_error("No import settings given for 'other legacy' data");
        return Util::RW::readReflectometryTable(s, *importSettings);
    }

    if (importSettings)
        throw std::runtime_error("Import settings given in spite of fully specified data format");

    if (ftype == mft)
        return Util::RW::readMotofit(s);

    if (ftype == bornagain1D)
        return Util::RW::readBAInt(s);

    if (ftype == csv1D_2cols)
        return Util::RW::readReflectometryTable(s, legacy1D_2cols);
    if (ftype == csv1D_3cols)
        return Util::RW::readReflectometryTable(s, legacy1D_3cols);
    if (ftype == csv1D_4cols)
        return Util::RW::readReflectometryTable(s, legacy1D_4cols);
    if (ftype == csv1D_5cols)
        return Util::RW::readReflectometryTable(s, legacy1D_5cols);

    ASSERT(false);
}

Datafield* IO::readData2D(const std::string& file_name, Filetype2D ftype)
{
    if (ftype == unknown2D)
        ftype = filename2type2D(file_name);

    std::stringstream s = ::file2stream(file_name);

    if (ftype == bornagain2D)
        return Util::RW::readBAInt(s);

    if (ftype == nicos2D)
        return Util::RW::readNicos(s);

    if (ftype == refsans2D)
        return Util::RW::readRefsans(s);

#ifdef BA_TIFF_SUPPORT
    if (ftype == tiff)
        return Util::RW::readTiff(s);
#endif

    // Try to read ASCII by default. Binary maps to ASCII.
    // If the file is not actually a matrix of numbers,
    // the error will be thrown during the reading.
    return Util::RW::readNumpyTxt(s);
}

void IO::writeDatafield(const Datafield& data, const std::string& file_name)
{
    const std::string ext = ::uncompressedExtension(file_name);
    try {
        std::stringstream s;

        if (ext == ".int")
            Util::RW::writeBAInt(data, s);

#ifdef BA_TIFF_SUPPORT
        else if (ext == ".tiff" || ext == ".tif")
            Util::RW::writeTiff(data, s);
#endif

        else
            Util::RW::writeNumpyTxt(data, s);

        ::stream2file(file_name, s);

    } catch (const std::exception& ex) {
        throw std::runtime_error("Failed writing to " + file_name + ": " + ex.what());
    }
}

bool Util::RW::dataMatchesFile(const Datafield& data, const std::string& refFileName, double tol)
{
    std::unique_ptr<Datafield> refDat;
    try {
        refDat.reset(IO::readData2D(refFileName));
    } catch (...) {
        std::cerr << "File comparison: Could not read reference data from file " << refFileName
                  << std::endl;
        return false;
    }
    ASSERT(refDat);

    return DiffUtil::checkRelativeDifference(data.flatVector(), refDat->flatVector(), tol);
}
