/* -*- C++ -*-
 *
 *  This file is part of RawTherapee.
 *
 *  Copyright (c) 2017 Alberto Griggio <alberto.griggio@gmail.com>
 *
 *  RawTherapee is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  RawTherapee is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with RawTherapee.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <cctype>
#include <iostream>

#include "imagedata.h"
#include "procparams.h"
#include "rtlensfun.h"
#include "settings.h"


namespace
{

bool isNextLensCropFactorBetter(const lfLens *current_lens, const lfCamera *camera, float next_lens_crop_factor)
{
    if (!current_lens) {
        // No current lens, so next lens's crop factor is
        // automatically better.
        return true;
    }

    const float current_lens_crop_factor = current_lens->CropFactor;

    if (!camera) {
        // Favor the smaller crop factor for maximum coverage.
        return current_lens_crop_factor > next_lens_crop_factor;
    }

    const float camera_crop_factor = camera->CropFactor;

    if (current_lens_crop_factor > camera_crop_factor) {
        // Current lens's data does not cover the entire camera
        // sensor. Any lens's data with a smaller crop factor is
        // better.
        return current_lens->CropFactor > next_lens_crop_factor;
    }

    // Current lens's data covers the entire camera sensor. A lens
    // with data from a larger crop factor will be more precise, but
    // also must not be larger than the camera sensor's crop factor
    // to maintain full coverage.
    return current_lens->CropFactor < next_lens_crop_factor &&
           next_lens_crop_factor <= camera_crop_factor;
}

bool isNextLensBetter(const lfCamera *camera, const lfLens *current_lens, const lfLens &next_lens, const Glib::ustring &lens_name, const Glib::ustring &next_lens_name)
{
    return isNextLensCropFactorBetter(current_lens, camera, next_lens.CropFactor) &&
           lens_name == next_lens_name;
}

/**
 * Trims whitespace from around dashes.
 */
std::string trimDashWhitespace(const std::string &input)
{
    enum class SeekStatus {
        /// Start of whitespace-dash unit.
        WHITESPACE_DASH,
        /// Dash after whitespace.
        DASH,
        /// End of whitespace-dash unit.
        OTHER,
    };

    std::string trimmed;
    trimmed.reserve(input.size());
    unsigned whitespace_index = 0;

    SeekStatus seek_status = SeekStatus::WHITESPACE_DASH;
    for (unsigned i = 0; i < input.size(); i++) {
        const auto cur_char = input[i];
        switch (seek_status) {
            case SeekStatus::WHITESPACE_DASH:
                if (std::isspace(cur_char)) {
                    // Possible beginning of whitespace-dash unit. Start seeking
                    // dash, but record current index in case there is no dash.
                    seek_status = SeekStatus::DASH;
                    whitespace_index = i;
                } else if (cur_char == '-') {
                    // Start of a whitespace-dash unit. Add the dash and skip
                    // all whitespace.
                    seek_status = SeekStatus::OTHER;
                    trimmed += cur_char;
                } else {
                    // Not a whitespace-dash unit, so just copy the character.
                    trimmed += cur_char;
                }
                break;
            case SeekStatus::DASH:
                if (cur_char == '-') {
                    // Found the dash. Now add the dash and skip all whitespace.
                    seek_status = SeekStatus::OTHER;
                    trimmed += cur_char;
                } else if (!std::isspace(cur_char)) {
                    // No dash found after whitespace. Copy the whitespace and
                    // character over and start looking for a whitespace-dash
                    // unit again.
                    seek_status = SeekStatus::WHITESPACE_DASH;
                    trimmed += input.substr(
                        whitespace_index, i - whitespace_index + 1);
                }
                // For whitespace, just continue looking for the dash.
                break;
            case SeekStatus::OTHER:
                if (cur_char == '-') {
                    // Found a dash. Now add the dash and skip all whitespace.
                    trimmed += cur_char;
                } else if (!std::isspace(cur_char)) {
                    // End of whitespace-dash unit, so add the character and
                    // start looking for another unit.
                    seek_status = SeekStatus::WHITESPACE_DASH;
                    trimmed += cur_char;
                }
                // For whitespace, just continue looking for the end of the
                // unit.
                break;
        }
    }

    if (seek_status == SeekStatus::DASH) {
        // Whitespace found, but no dash. Add the whitespace.
        trimmed += input.substr(whitespace_index);
    }
    // If seeking a whitespace-dash unit, all the characters have been added.
    // If seeking the end of a whitespace-dash unit, the dash has been added.

    return trimmed;
}

} // namespace

namespace rtengine
{

//-----------------------------------------------------------------------------
// LFModifier
//-----------------------------------------------------------------------------

LFModifier::~LFModifier()
{
    if (data_) {
        data_->Destroy();
    }
}


LFModifier::operator bool() const
{
    return data_;
}


bool LFModifier::hasDistortionCorrection() const
{
    return (flags_ & LF_MODIFY_DISTORTION);
}

bool LFModifier::hasCACorrection() const
{
    return (flags_ & LF_MODIFY_TCA);
}

bool LFModifier::hasVignettingCorrection() const
{
    return (flags_ & LF_MODIFY_VIGNETTING);
}

void LFModifier::correctDistortion(double &x, double &y, int cx, int cy) const
{
    if (!data_) {
        return;
    }

    float pos[2];
    float xx = x + cx;
    float yy = y + cy;
    if (swap_xy_) {
        std::swap(xx, yy);
    }
    if (data_->ApplyGeometryDistortion(xx, yy, 1, 1, pos)) {  // This is thread-safe
        x = pos[0];
        y = pos[1];
        if (swap_xy_) {
            std::swap(x, y);
        }
        x -= cx;
        y -= cy;
    }
}

void LFModifier::correctCA(double &x, double &y, int cx, int cy, int channel) const
{
    assert(channel >= 0 && channel <= 2);

    // agriggio: RT currently applies the CA correction per channel, whereas
    // lensfun applies it to all the three channels simultaneously. This means
    // we do the work 3 times, because each time we discard 2 of the 3
    // channels. We could consider caching the info to speed this up
    x += cx;
    y += cy;

    float pos[6];
    if (swap_xy_) {
        std::swap(x, y);
    }
    data_->ApplySubpixelDistortion(x, y, 1, 1, pos);  // This is thread-safe
    x = pos[2*channel];
    y = pos[2*channel+1];
    if (swap_xy_) {
        std::swap(x, y);
    }
    x -= cx;
    y -= cy;
}

void LFModifier::correctDistortionAndCA(double &x, double &y, int cx, int cy, int channel) const
{
    assert(channel >= 0 && channel <= 2);

    // RT currently applies the CA correction per channel, whereas
    // lensfun applies it to all the three channels simultaneously. This means
    // we do the work 3 times, because each time we discard 2 of the 3
    // channels. We could consider caching the info to speed this up
    x += cx;
    y += cy;

    float pos[6];
    if (swap_xy_) {
        std::swap(x, y);
    }
    data_->ApplySubpixelGeometryDistortion(x, y, 1, 1, pos);  // This is thread-safe
    x = pos[2*channel];
    y = pos[2*channel+1];
    if (swap_xy_) {
        std::swap(x, y);
    }
    x -= cx;
    y -= cy;
}

#ifdef _OPENMP
void LFModifier::processVignette(int width, int height, float** rawData) const
{
    #pragma omp parallel for schedule(dynamic,16)

    for (int y = 0; y < height; ++y) {
        data_->ApplyColorModification(rawData[y], 0, y, width, 1, LF_CR_1(INTENSITY), 0);
    }
}
#else
void LFModifier::processVignette(int width, int height, float** rawData) const
{
    data_->ApplyColorModification(rawData[0], 0, 0, width, height, LF_CR_1(INTENSITY), width * sizeof(float));
}

#endif

#ifdef _OPENMP
void LFModifier::processVignette3Channels(int width, int height, float** rawData) const
{
    #pragma omp parallel for schedule(dynamic,16)

    for (int y = 0; y < height; ++y) {
        data_->ApplyColorModification(rawData[y], 0, y, width, 1, LF_CR_3(RED, GREEN, BLUE), 0);
    }
}
#else
void LFModifier::processVignette3Channels(int width, int height, float** rawData) const
{
    data_->ApplyColorModification(rawData[0], 0, 0, width, height, LF_CR_3(RED, GREEN, BLUE), width * 3 * sizeof(float));
}

#endif
Glib::ustring LFModifier::getDisplayString() const
{
    if (!data_) {
        return "NONE";
    } else {
        Glib::ustring ret;
        Glib::ustring sep;
        if (flags_ & LF_MODIFY_DISTORTION) {
            ret += "distortion";
            sep = ", ";
        }
        if (flags_ & LF_MODIFY_VIGNETTING) {
            ret += sep;
            ret += "vignetting";
            sep = ", ";
        }
        if (flags_ & LF_MODIFY_TCA) {
            ret += sep;
            ret += "CA";
            sep = ", ";
        }
        if (flags_ & LF_MODIFY_SCALE) {
            ret += sep;
            ret += "autoscaling";
        }
        return ret;
    }
}


LFModifier::LFModifier(lfModifier *m, bool swap_xy, int flags):
    data_(m),
    swap_xy_(swap_xy),
    flags_(flags)
{
}


//-----------------------------------------------------------------------------
// LFCamera
//-----------------------------------------------------------------------------

LFCamera::LFCamera():
    data_(nullptr)
{
}


LFCamera::operator bool() const
{
    return data_;
}


Glib::ustring LFCamera::getMake() const
{
    if (data_) {
        return data_->Maker;
    } else {
        return "";
    }
}


Glib::ustring LFCamera::getModel() const
{
    if (data_) {
        return data_->Model;
    } else {
        return "";
    }
}


float LFCamera::getCropFactor() const
{
    if (data_) {
        return data_->CropFactor;
    } else {
        return 0;
    }
}


bool LFCamera::isFixedLens() const
{
    // per lensfun's main developer Torsten Bronger:
    // "Compact camera mounts can be identified by the fact that the mount
    // starts with a lowercase letter"
    return data_ && data_->Mount && std::islower(data_->Mount[0]);
}


Glib::ustring LFCamera::getDisplayString() const
{
    if (data_) {
        return getMake() + ' ' + getModel();
    } else {
        return "---";
    }
}


//-----------------------------------------------------------------------------
// LFLens
//-----------------------------------------------------------------------------

LFLens::LFLens():
    data_(nullptr)
{
}


LFLens::operator bool() const
{
    return data_;
}


Glib::ustring LFLens::getMake() const
{
    if (data_) {
        return data_->Maker;
    } else {
        return "";
    }
}


Glib::ustring LFLens::getLens() const
{
    if (data_) {
        return Glib::ustring(data_->Maker) + ' ' + data_->Model;
    } else {
        return "---";
    }
}


float LFLens::getCropFactor() const
{
    if (data_) {
        return data_->CropFactor;
    } else {
        return 0;
    }
}

bool LFLens::hasVignettingCorrection() const
{
    if (data_) {
        return data_->CalibVignetting;
    } else {
        return false;
    }
}

bool LFLens::hasDistortionCorrection() const
{
    if (data_) {
        return data_->CalibDistortion;
    } else {
        return false;
    }
}

bool LFLens::hasCACorrection() const
{
    if (data_) {
        return data_->CalibTCA;
    } else {
        return false;
    }
}


//-----------------------------------------------------------------------------
// LFDatabase
//-----------------------------------------------------------------------------

LFDatabase LFDatabase::instance_;


bool LFDatabase::init(const Glib::ustring &dbdir)
{
    instance_.data_ = lfDatabase::Create();

    if (settings->verbose) {
        std::cout << "Loading lensfun database from ";
        if (dbdir.empty()) {
            std::cout << "the default directories";
        } else {
            std::cout << "'" << dbdir << "'";
        }
        std::cout << "..." << std::flush;
    }

    bool ok = false;
    if (dbdir.empty()) {
        ok = (instance_.data_->Load() ==  LF_NO_ERROR);
    } else {
        ok = instance_.LoadDirectory(dbdir.c_str());
    }

    if (settings->verbose) {
        std::cout << (ok ? "OK" : "FAIL") << std::endl;
    }

    return ok;
}


bool LFDatabase::LoadDirectory(const char *dirname)
{
#if RT_LENSFUN_HAS_LOAD_DIRECTORY
    return instance_.data_->LoadDirectory(dirname);
#else
    // backported from lensfun 0.3.x
    bool database_found = false;

    GDir *dir = g_dir_open (dirname, 0, NULL);
    if (dir)
    {
        GPatternSpec *ps = g_pattern_spec_new ("*.xml");
        if (ps)
        {
            const gchar *fn;
            while ((fn = g_dir_read_name (dir)))
            {
                size_t sl = strlen (fn);
                if (g_pattern_match (ps, sl, fn, NULL))
                {
                    gchar *ffn = g_build_filename (dirname, fn, NULL);
                    /* Ignore errors */
                    if (data_->Load (ffn) == LF_NO_ERROR)
                        database_found = true;
                    g_free (ffn);
                }
            }
            g_pattern_spec_free (ps);
        }
        g_dir_close (dir);
    }

    return database_found;
#endif
}


LFDatabase::LFDatabase():
    data_(nullptr)
{
}


LFDatabase::~LFDatabase()
{
    if (data_) {
        MyMutex::MyLock lock(lfDBMutex);
        data_->Destroy();
    }
}


const LFDatabase *LFDatabase::getInstance()
{
    return &instance_;
}


std::vector<LFCamera> LFDatabase::getCameras() const
{
    std::vector<LFCamera> ret;
    if (data_) {
        MyMutex::MyLock lock(lfDBMutex);
        auto cams = data_->GetCameras();
        while (*cams) {
            ret.emplace_back();
            ret.back().data_ = *cams;
            ++cams;
        }
    }
    return ret;
}


std::vector<LFLens> LFDatabase::getLenses() const
{
    std::vector<LFLens> ret;
    if (data_) {
        MyMutex::MyLock lock(lfDBMutex);
        auto lenses = data_->GetLenses();
        while (*lenses) {
            ret.emplace_back();
            ret.back().data_ = *lenses;
            ++lenses;
        }
    }
    return ret;
}


LFCamera LFDatabase::findCamera(const Glib::ustring &make, const Glib::ustring &model, bool autoMatch) const
{
    LFCamera ret;
    if (data_ && !make.empty()) {
        MyMutex::MyLock lock(lfDBMutex);
        if (!autoMatch) {
            // Try to find exact match by name.
            for (auto camera_list = data_->GetCameras(); camera_list[0]; camera_list++) {
                const auto camera = camera_list[0];
                if (make == camera->Maker && model == camera->Model) {
                    ret.data_ = camera;
                    return ret;
                }
            }
        }
        auto found = data_->FindCamerasExt(make.c_str(), model.c_str());
        if (found) {
            ret.data_ = found[0];
            lf_free(found);
        }
    }
    return ret;
}


LFLens LFDatabase::findLens(const LFCamera &camera, const Glib::ustring &name, bool autoMatch) const
{
    LFLens ret;
    if (data_ && !name.empty()) {
        MyMutex::MyLock lock(lfDBMutex);
        if (!autoMatch) {
            // Only the lens name provided. Try to find exact match by name.
            LFLens candidate;
            LFLens bestCandidate;

            for (auto lens_list = data_->GetLenses(); lens_list[0]; lens_list++) {
                candidate.data_ = lens_list[0];
                if (isNextLensBetter(camera.data_, bestCandidate.data_, *(candidate.data_), name, candidate.getLens())) {
                    bestCandidate.data_ = candidate.data_;
                }
            }
            if (bestCandidate.data_) {
                return bestCandidate;
            }
        }

        // Tries to find the lens by name, possibly splitting the maker and
        // model from the lens name.
        const auto find_lens_from_name = [](const lfDatabase *database, const lfCamera *cam, const Glib::ustring &lens_name) {
            auto found = database->FindLenses(cam, nullptr, lens_name.c_str());
            for (size_t pos = 0; !found && pos < lens_name.size(); ) {
                // try to split the maker from the model of the lens -- we have to
                // guess a bit here, since there are makers with a multi-word name
                // (e.g. "Leica Camera AG")
                if (lens_name.find("f/", pos) == 0) {
                    break; // no need to search further
                }
                Glib::ustring make, model;
                auto i = lens_name.find(' ', pos);
                if (i != Glib::ustring::npos) {
                    make = lens_name.substr(0, i);
                    model = lens_name.substr(i+1);
                    found = database->FindLenses(cam, make.c_str(), model.c_str());
                    pos = i+1;
                } else {
                    break;
                }
            }
            return found;
        };

        // Find the lens, starting with the mount, then trying without the mount
        // if no match is found. The latter is necessary for certain adapted
        // lenses.
        const lfLens **found = nullptr;
        lfCamera camera_without_mount;
        std::vector<const lfCamera *> camera_ptrs = {camera.data_};
        if (camera.data_ && camera.data_->Mount) {
            camera_without_mount = *(camera.data_);
            camera_without_mount.SetMount(nullptr);
            camera_ptrs.push_back(&camera_without_mount);
        }
        for (auto camera_data : camera_ptrs) {
            if (!found) {
                found = find_lens_from_name(data_, camera_data, name);
            }
            if (!found) {
                // Some names have white-space around the dash(s) while Lensfun
                // does not have any.
                const auto formatted_name = trimDashWhitespace(name.raw());
                if (name != formatted_name) {
                    found = find_lens_from_name(data_, camera_data, formatted_name);
                }
            }
        }

        // Finally, handle the case of fixed-lens cameras.
        if (!found && camera && camera.isFixedLens()) {
            found = data_->FindLenses(camera.data_, nullptr, "");
        }
        if (found) {
            ret.data_ = found[0];
            lf_free(found);
        }
    }
    return ret;
}


std::unique_ptr<LFModifier> LFDatabase::getModifier(const LFCamera &camera, const LFLens &lens,
                                    float focalLen, float aperture, float focusDist,
                                    int width, int height, bool swap_xy) const
{
    std::unique_ptr<LFModifier> ret;
    if (data_) {
        MyMutex::MyLock lock(lfDBMutex);
        if (camera && lens) {
            lfModifier *mod = lfModifier::Create(lens.data_, camera.getCropFactor(), width, height);
            int flags = LF_MODIFY_DISTORTION | LF_MODIFY_SCALE | LF_MODIFY_TCA;
            if (aperture > 0) {
                flags |= LF_MODIFY_VIGNETTING;
            }
            flags = mod->Initialize(lens.data_, LF_PF_F32, focalLen, aperture, focusDist > 0 ? focusDist : 1000, 0.0, LF_RECTILINEAR, flags, false);
            ret.reset(new LFModifier(mod, swap_xy, flags));
        }
    }
    return ret;
}

std::unique_ptr<LFModifier> LFDatabase::findModifier(
    const procparams::LensProfParams &lensProf,
    const FramesMetaData *idata,
    int width,
    int height,
    const procparams::CoarseTransformParams &coarse,
    int rawRotationDeg
) const
{
    const float focallen = idata->getFocalLen();

    Glib::ustring make, model, lens;
    if (lensProf.lfAutoMatch()) {
        if (focallen <= 0.f) {
            return nullptr;
        }
        make = idata->getMake();
        model = idata->getModel();
        lens = idata->getLens();
    } else {
        make = lensProf.lfCameraMake;
        model = lensProf.lfCameraModel;
        lens = lensProf.lfLens;
    }

    if (make.empty() || model.empty() || lens.empty()) {
        return nullptr;
    }

    const std::string key = (make + model + lens).collate_key();
    if (notFound.find(key) != notFound.end()) {
        // This combination was not found => do not search again
        return nullptr;
    }

    const LFCamera c = findCamera(make, model, lensProf.lfAutoMatch());
    const LFLens l = findLens(c, lens, lensProf.lfAutoMatch());

    bool swap_xy = false;
    if (rawRotationDeg >= 0) {
        int rot = (coarse.rotate + rawRotationDeg) % 360;
        swap_xy = (rot == 90 || rot == 270);
        if (swap_xy) {
            std::swap(width, height);
        }
    }

    std::unique_ptr<LFModifier> ret = getModifier(
        c,
        l,
        idata->getFocalLen(),
        idata->getFNumber(),
        idata->getFocusDist(),
        width,
        height,
        swap_xy
    );

    if (settings->verbose) {
        std::cout << "LENSFUN:\n"
                  << "  camera: " << c.getDisplayString() << "\n"
                  << "  lens: " << l.getDisplayString() << "\n"
                  << "  correction: "
                  << (ret ? ret->getDisplayString() : "NONE")
                  << std::endl;
    }

    if (!ret) {
        notFound.insert(key);
    }

    return ret;
}


} // namespace rtengine
