/*
 * (C) Copyright 1996-2013 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 "eckit/exception/Exceptions.h"
// #include "eckit/thread/Mutex.h"
// #include "eckit/os/Stat.h"
// #include "eckit/log/Timer.h"

#include "metkit/pointdb/PointIndex.h"
#include "eckit/io/FileHandle.h"
#include "eckit/thread/AutoLock.h"
// #include "eckit/io/StdFile.h"
#include "eckit/config/Resource.h"
#include "metkit/codes/api/CodesAPI.h"

// #include "eccodes.h"

using namespace eckit;

namespace metkit {
namespace pointdb {

static Mutex mutex;
static std::set<std::string> done_;
static std::map<PathName, PointIndex*> cache_;

eckit::PathName PointIndex::cachePath(const std::string& dir, const std::string& name) {
    static eckit::PathName pointdbCachePath = eckit::Resource<eckit::PathName>("pointdbCachePath", "~/pointdb");
    return pointdbCachePath / dir / name;
}

std::string PointIndex::cache(const metkit::codes::CodesHandle& h) {

    std::string md5 = h.getString("md5GridSection");


    AutoLock<Mutex> lock(mutex);
    if (done_.find(md5) != done_.end()) {
        return md5;
    }

    eckit::PathName path = cachePath("grids", md5 + ".kdtree");
    if (path.exists()) {
        done_.insert(md5);
        return md5;
    }
    path.dirName().mkdir();

    size_t v = h.size("values");


    std::vector<Point> p;
    p.reserve(v);

    size_t j = 0;
    for (auto data : h.values()) {
        while (data.longitude < 0)
            data.longitude += 360;
        while (data.longitude >= 360)
            data.longitude -= 360;

        p.push_back(Point(data.latitude, data.longitude, j));
        j++;
    }


    PathName tmp = cachePath("grids", md5 + ".tmp");
    tmp.unlink();

    Tree* tree = new Tree(tmp, p.size(), 0);
    tree->build(p.begin(), p.end());

    // PathName dump(std::string("/tmp/cache/pointdb/") + md5 + ".dump");
    // StdFile f(dump, "w");
    // codes_dump_content(h, f, "debug", 0, 0);
    // f.close();

    // Write handle to file
    PathName grib = cachePath("grids", md5 + ".grib");
    eckit::FileHandle fh(grib.localPath());
    auto data = h.messageData();
    fh.openForWrite(data.size());
    fh.write(data.data(), data.size());
    fh.close();

    PathName::rename(tmp, path);

    cache_[md5] = new PointIndex(path, tree);
    done_.insert(md5);
    return md5;
}

PointIndex& PointIndex::lookUp(const std::string& md5) {
    AutoLock<Mutex> lock(mutex);
    std::map<PathName, PointIndex*>::iterator k = cache_.find(md5);
    if (k == cache_.end()) {
        eckit::PathName path = cachePath("grids", md5 + ".kdtree");

        if (!path.exists()) {
            Log::warning() << path << " does not exists" << std::endl;
            PathName grib = cachePath("grids", md5 + ".grib");
            if (grib.exists()) {
                Log::warning() << "Rebuilding index from " << grib << std::endl;
                auto codesHandle = codes::codesHandleFromFile(grib.localPath(), codes::Product::GRIB);

                ASSERT(cache(*codesHandle.get()) == md5);
            }
        }

        Log::warning() << "Loading " << path << std::endl;
        PointIndex* p = new PointIndex(path);
        cache_[md5]   = p;
        return *p;
    }
    return *(*k).second;
}

PointIndex::PointIndex(const PathName& path, PointIndex::Tree* tree) : path_(path), tree_(tree) {

    if (!tree) {
        Log::info() << "Load tree " << path << std::endl;
        ASSERT(path.exists());
        tree_.reset(new Tree(path, 0, 0));
    }
}

PointIndex::~PointIndex() {
    // TODO
}

PointIndex::NodeInfo PointIndex::nearestNeighbour(double lat, double lon) {
    Point p(lat, lon, 0);

    std::map<Point, NodeInfo>::iterator k;

    {
        AutoLock<Mutex> lock(mutex_);

        k = last_.find(p);
        if (k != last_.end())
            return (*k).second;
    }

    NodeInfo n;

    {
        Timer timer("Find nearest");
        n = tree_->nearestNeighbour(p);
    }

    {
        AutoLock<Mutex> lock(mutex_);

        if (last_.size() >= 4096)
            last_.clear();

        last_[p] = n;
    }

    return n;
}

}  // namespace pointdb
}  // namespace metkit
