/*
 * © Copyright 1996-2012 ECMWF.
 * 
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. 
 * In applying this licence, ECMWF does not waive the privileges and immunities 
 * granted to it by virtue of its status as an intergovernmental organisation nor
 * does it submit to any jurisdiction.
 */

#include "odb.h"
#include "mars.h"

#include "eckit/eckit_version.h"
#include "eckit/exception/Exceptions.h"
#include "eckit/filesystem/PathName.h"
#include "eckit/io/Length.h"
#include "eckit/io/StdFileHandle.h"
#include "eckit/utils/StringTools.h"

#include "metkit/odb/OdbToRequest.h"

#include "odc/Select.h"
#include "odc/Writer.h"

#include <set>
#include <string>
#include <memory>
#include <algorithm>

using namespace eckit;
using namespace metkit::odb;


boolean is_part_of_mars_language(const char *n)
{
	static request *archive = NULL;
	const char *s = NULL;

	if(!archive)
	{
		request *r = mars_language();
		while(r && !EQ(r->name, "ARCHIVE"))
			r = r->next;
		if(r == NULL)
		{
			marslog(LOG_EROR, const_cast<char *>("ARCHIVE request not found in language. OOOPPPSSS!!!"));
			marsexit(1);
		}

		archive = r;
	}

	if((s = get_value(archive,n,0)) != NULL)
		return true;

	return false;
}

err odb_to_request_from_file(request *r, const char *fileName)
{
    try {
		marslog(LOG_DBUG, const_cast<char *>("odb_to_request_from_file: fileName = %s."), fileName); 

        PathName pn(fileName);
        if (!pn.exists()) {
			marslog(LOG_EROR, (char *)"oda_to_request_from_file: file '%s' does not exist.", (char *) fileName);
            return TOO_SHORT_ERR;
		}
        if (!pn.size()) {
			marslog(LOG_EROR, (char *)"oda_to_request_from_file: file '%s' empty.", fileName);
            return TOO_SHORT_ERR;
		}

        // Extract the MARS keywords needed to archive this data

        bool singleRequest = true;
        bool constantColumns = true;
        OdbToRequest o2r("archive", singleRequest, constantColumns);

        metkit::mars::MarsRequest rq;
        try {
            std::unique_ptr<DataHandle> dh(pn.fileHandle());
            dh->openForRead();
            std::vector<metkit::mars::MarsRequest> requests = o2r.odbToRequest(*dh);
            ASSERT(requests.size() == 1);
            rq = requests[0];
        } catch (Exception& ex) {
            return HYPERCUBE_ERROR;
        }

        // Convert the metkit mars rq to one for MARS client

        std::ostringstream ss_rq;
        rq.dump(ss_rq, "", "");
        Log::debug() << "odb_to_request_from_file: " << ss_rq.str() << std::endl;

        request* n = string2request(ss_rq.str().c_str());
        if (!n) {
            marslog(LOG_EROR, "Error creating a MARS request from data");
            return TOO_SHORT_ERR;
        }
        reqmerge(r, n);
        free_all_requests(n);
	}
    catch (Exception& ex)
	{
		marslog(LOG_EROR, (char *)"Error making a request from file %s\n",ex.what());
		marslog(LOG_EROR, (char *)"Exception ignored");
        return TOO_SHORT_ERR;
	}
    return NOERR;
}

typedef std::map<std::string, std::set<std::string> > Parameters ;

Parameters parameters(request* r)
{
	Parameters ret;

	for (parameter *p = r->params; p; p = p->next)
	{
        std::string parameterName = p->name;
		size_t n = count_values(r, p->name);
		if (n == 0)
		{
			marslog(LOG_EROR, (char *)"parameters: no values of param '%s'", p->name);
			ASSERT(n != 0);
		}
        std::set<std::string> values;
		for (size_t i = 0; i < n; ++i)
			values.insert(get_value(r, p->name, i));
		ret[parameterName] = values;
	}
	return ret;
}

bool set_values_compare(const std::set<std::string>& s1, const std::set<std::string>& s2) {\

    if (s1.size() != s2.size()) return false;

    for (const auto& v : s1) {
        if (s2.find(v) != s2.end()) continue;
        if (s2.find(StringTools::upper(v)) != s2.end()) continue;
        if (s2.find(StringTools::lower(v)) != s2.end()) continue;
        return false;
    }

    return true;
}

err odb_compare_attributes_of_first_request(request* first, request* second)
{
	typedef Parameters P;

	P firstParams = parameters(first);
	P secondParams = parameters(second);

	for (P::iterator it = firstParams.begin(); it != firstParams.end(); ++it)
	{
        const std::string& paramName = it->first;
        const std::set<std::string>& values = it->second;

		P::iterator jt = secondParams.find(paramName);
        if (jt == secondParams.end()) jt = secondParams.find(StringTools::upper(paramName));
        if (jt == secondParams.end()) jt = secondParams.find(StringTools::lower(paramName));
        if (jt == secondParams.end())
		{
			marslog(LOG_EROR, (char *)"odb_compare_attributes_of_first_request: second request has no param '%s'", paramName.c_str());
			return -1;
		}
        const std::set<std::string>& otherValues = jt->second;
        if (!set_values_compare(values, otherValues))
		{
            std::stringstream ss;
			if (values.size() == 1 && otherValues.size() == 1)
			{
				ss << "Values of '" << paramName << "' differ: " << *values.begin() << " <> " << *otherValues.begin();
				marslog(LOG_EROR, (char *)"odb_compare_attributes_of_first_request: %s", ss.str().c_str());
			}
			else
			{
				marslog(LOG_EROR, (char *)"odb_compare_attributes_of_first_request: values for param '%s' differ", paramName.c_str());
                std::ostream_iterator<std::string> out(ss, ", ");
                std::set_symmetric_difference(values.begin(), values.end(), otherValues.begin(), otherValues.end(), out);
				marslog(LOG_EROR,(char *)
					"odb_compare_attributes_of_first_request: values present in one of the sets, but not in the other: %s",
					ss.str().c_str());
			}
			return -1;
		}
	}
	return NOERR;
}

class CloneCopyStdFileHandle : public StdFileHandle {
public:
    using StdFileHandle::StdFileHandle;
    CloneCopyStdFileHandle(FILE* f) : StdFileHandle(f), f_(f) {}
    DataHandle* clone() const { return new CloneCopyStdFileHandle(f_); }
    FILE* f_;
};

long long odb_filter(const char *sql, FILE *fin, FILE *fout, long long total_to_read)
{
    try {

        /// @note - this is not very nice. It uses a legacy (odb_api) interface to make the
        ///         SQL functionality available. This is the same as is done in the odc tools.
        ///         Ideally we will do better than this... so when we update the tools, then
        ///         update this.

        marslog(LOG_INFO, const_cast<char *>("odb_filter: sql = '%s', total_to_read = %lld"),
            ((sql == 0) ? "NULL" : sql),
            total_to_read);

        if (total_to_read == 0)
            return 0;

        // TODO: check sql is a select really and does not have INTO clause (?)
        CloneCopyStdFileHandle fhin(fin), fhout(fout);
        fhin.openForRead();

        if ( ! sql || strlen(sql) == 0 )
        {
          Length n = fhin.saveInto(fhout);
            ASSERT(total_to_read == n);
        }
        else
        {
            std::string s = StringTools::unQuote(StringTools::trim(sql));

            odc::Select odb(s, fhin);
            odc::Select::iterator it = odb.begin();
            odc::Select::iterator end = odb.end();

            odc::Writer<> writer(fhout);
            odc::Writer<>::iterator outit = writer.begin();
            outit->pass1(it, end);
        }
        marslog(LOG_INFO, const_cast<char *>(" => odb_filter"));
	}
    catch (Exception &ex)
	{
		marslog(LOG_EROR, (char*)"Error in odb_filter %s\n",ex.what());
		return -1;
	}

	// TODO: make sure the below is true
	return total_to_read;
}
