/*
 *  HDRMerge - HDR exposure merging software.
 *  Copyright 2012 Javier Celaya
 *  jcelaya@gmail.com
 *
 *  This file is part of HDRMerge.
 *
 *  HDRMerge 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.
 *
 *  HDRMerge 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 HDRMerge. If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <cstdlib>
#include <algorithm>
#include <QImage>
#include <QString>
#include <QRegExp>
#include <QFileInfo>
#include <libraw.h>
#include "ImageIO.hpp"
#include "DngFloatWriter.hpp"
#include "Log.hpp"
using namespace std;
using namespace hdrmerge;

Image ImageIO::loadRawImage(const QString& filename, RawParameters & rawParameters, int shot_select) {
    std::unique_ptr<LibRaw> rawProcessor(new LibRaw);
    auto & d = rawProcessor->imgdata;
    d.params.shot_select = shot_select;
    if (rawProcessor->open_file(rawParameters.fileName.toLocal8Bit().constData()) == LIBRAW_SUCCESS) {
        libraw_decoder_info_t decoder_info;
        rawProcessor->get_decoder_info(&decoder_info);
        if (d.idata.filters <= 1000 && d.idata.filters != 9) {
            Log::msg(Log::DEBUG, "Unsupported filter array (", d.idata.filters, ").");
#ifdef LIBRAW_DECODER_FLATFIELD
        } else if (!decoder_info.decoder_flags & LIBRAW_DECODER_FLATFIELD) {
            Log::msg(Log::DEBUG, "LibRaw decoder is not flatfield (", ios::hex, decoder_info.decoder_flags, ").");
#endif
        } else if (rawProcessor->unpack() != LIBRAW_SUCCESS) {
            Log::msg(Log::DEBUG, "LibRaw::unpack() failed.");
        } else {
            rawParameters.fromLibRaw(*(rawProcessor.get()));
        }
    } else {
        Log::msg(Log::DEBUG, "LibRaw::open_file(", rawParameters.fileName, ") failed.");
    }
    return Image(d.rawdata.raw_image, rawParameters, filename);
}

int ImageIO::getFrameCount(RawParameters & rawParameters) {
    std::unique_ptr<LibRaw> rawProcessor(new LibRaw);
    auto & d = rawProcessor->imgdata;
    if (rawProcessor->open_file(rawParameters.fileName.toLocal8Bit().constData()) == LIBRAW_SUCCESS) {
        Log::msg(Log::DEBUG, "Number of frames : ", d.idata.raw_count);
        return d.idata.raw_count;
    } else {
        return 0;
    }

}

ImageIO::QDateInterval ImageIO::getImageCreationInterval(const QString & fileName) {
    std::unique_ptr<LibRaw> rawProcessor(new LibRaw);
    QDateInterval result;
    if (rawProcessor->open_file(fileName.toLocal8Bit().constData()) == LIBRAW_SUCCESS) {
        result.end = QDateTime::fromTime_t(rawProcessor->imgdata.other.timestamp);
        result.start = result.end.addMSecs(-rawProcessor->imgdata.other.shutter * 1000.0);
    }
    return result;
}


int ImageIO::load(const LoadOptions & options, ProgressIndicator & progress) {
    int numImages = options.fileNames.size();
    int step;
    int p = 0;
    int error = 0, failedImage = 0;
    stack.clear();
    rawParameters.clear();
    {
        Timer t("Load files");
        if(numImages == 1) { // check for multiframe raw files
            const QString name = options.fileNames[0];
            unique_ptr<RawParameters> params(new RawParameters(name));
            int frameCount = getFrameCount(*params);
            step = 100 / (frameCount + 1);
            p = 0;
            if(frameCount > 0 && frameCount <= 4) {
                // framecount == 1 => create a dng from a single file with a single frame
                // framecount == 2 => create a merged dng from a fuji exr file
                // framecount == 3 => create a merged dng from a pentax hdr file
                for (int i = 0; i < frameCount; ++i) {
                    progress.advance(p, "Loading %1", name.toLocal8Bit().constData());
                    p += step;
                    unique_ptr<RawParameters> params(new RawParameters(name));

                    Image image = loadRawImage(name, *params, i);
                    if (!image.good()) {
                        error = 1;
                        failedImage = i;
                        break;
                    } else if (stack.size() && !params->isSameFormat(*rawParameters.front())) {
                        error = 2;
                        failedImage = i;
                        break;
                    } else {
                        int pos = stack.addImage(std::move(image));
                        rawParameters.emplace_back(std::move(params));
                        for (int j = rawParameters.size() - 1; j > pos; --j)
                            rawParameters[j - 1].swap(rawParameters[j]);
                    }
                }
            }
        } else {
            step = 100 / (numImages + 1);
            for (int i = 0; i < numImages; ++i) {
                const QString name = options.fileNames[i];
                progress.advance(p, "Loading %1", name.toLocal8Bit().constData());
                p += step;
                unique_ptr<RawParameters> params(new RawParameters(name));

                Image image = loadRawImage(name, *params);
                if (!image.good()) {
                    error = 1;
                    failedImage = i;
                    break;
                } else if (stack.size() && !params->isSameFormat(*rawParameters.front())) {
                    error = 2;
                    failedImage = i;
                    break;
                } else {
                    int pos = stack.addImage(std::move(image));
                    rawParameters.emplace_back(std::move(params));
                    for (int j = rawParameters.size() - 1; j > pos; --j)
                        rawParameters[j - 1].swap(rawParameters[j]);
                }
            }
        }
    }
    if (error) {
        stack.clear();
        rawParameters.clear();
        return (failedImage << 1) + error - 1;
    }

    progress.advance(p, "Processing stack");

    RawParameters & params = *rawParameters.front();
    stack.setFlip(params.flip);
    if(options.useCustomWl)
        // Use custom white level, but only if it's not greater than the value provided by libraw
        params.max = std::min(params.max, options.customWl);
    stack.calculateSaturationLevel(params, options.useCustomWl);
    if (options.align && params.canAlign()) {
        stack.align();
        if (options.crop) {
            stack.crop();
        }
    }
    stack.computeResponseFunctions();
    stack.generateMask();
    progress.advance(100, "Done loading!");
    return numImages << 1;
}


void ImageIO::save(const SaveOptions & options, ProgressIndicator & progress) {
    string cropped = stack.isCropped() ? " cropped" : "";
    Log::msg(2, "Writing ", options.fileName, ", ", options.bps, "-bit, ", stack.getWidth(), 'x', stack.getHeight(), cropped);

    progress.advance(0, "Rendering image");
    RawParameters params = *rawParameters.back();
    params.width = stack.getWidth();
    params.height = stack.getHeight();
    params.adjustWhite(stack.getImage(stack.size() - 1));
    Array2D<float> composedImage = stack.compose(params, options.featherRadius);

    progress.advance(33, "Rendering preview");
    QImage preview = renderPreview(composedImage, params, stack.getMaxExposure(), options.previewSize <= 1);

    progress.advance(66, "Writing output");
    DngFloatWriter writer;
    writer.setBitsPerSample(options.bps);
    writer.setPreviewWidth((options.previewSize * stack.getWidth()) / 2);
    writer.setPreview(preview);
    writer.write(std::move(composedImage), params, options.fileName);
    progress.advance(100, "Done writing!");

    if (options.saveMask) {
        QString name = replaceArguments(options.maskFileName, options.fileName);
        writeMaskImage(name);
    }
}


void ImageIO::writeMaskImage(const QString & maskFile) {
    Log::debug("Saving mask to ", maskFile);
    EditableMask & mask = stack.getMask();
    QImage maskImage(mask.getWidth(), mask.getHeight(), QImage::Format_Indexed8);
    int numColors = stack.size() - 1;
    for (int c = 0; c < numColors; ++c) {
        int gray = (256 * c) / numColors;
        maskImage.setColor(c, qRgb(gray, gray, gray));
    }
    maskImage.setColor(numColors, qRgb(255, 255, 255));
    for (size_t y = 0, pos = 0; y < mask.getHeight(); ++y) {
        for (size_t x = 0; x < mask.getWidth(); ++x, ++pos) {
            maskImage.setPixel(x, y, mask[pos]);
        }
    }
    if (!maskImage.save(maskFile)) {
        Log::progress("Cannot save mask image to ", maskFile);
    }
}


static void prepareRawBuffer(LibRaw & rawProcessor) {
    rawProcessor.imgdata.progress_flags |= LIBRAW_PROGRESS_LOAD_RAW;
    auto & i = rawProcessor.imgdata;
    auto & r = i.rawdata;
    auto & s = i.sizes;
    r.color4_image = nullptr;
    r.color3_image = nullptr;
    size_t numPixels = s.raw_width * (s.raw_height + 7);
    r.raw_alloc = std::malloc(numPixels * sizeof(ushort));
    r.raw_image = (ushort*) r.raw_alloc;
    s.raw_pitch = s.raw_width*2;
    copy_n(&i.color, 1, &r.color);
    copy_n(&i.sizes, 1, &r.sizes);
    copy_n(&i.idata, 1, &r.iparams);
}


QImage ImageIO::renderPreview(const Array2D<float> & rawData, const RawParameters & params, float expShift, bool halfSize) {
    Timer t("Render preview");
    std::unique_ptr<LibRaw> rawProcessor(new LibRaw);
    auto & d = rawProcessor->imgdata;
    d.params.user_sat = 65535;
    d.params.user_black = 0;
    for (int c = 0; c < 4; ++c) {
        d.params.user_cblack[c] = 0;
    }
    d.params.highlight = 2;
    d.params.user_qual = 3;
    d.params.med_passes = 0;
    copy_n(params.camMul, 4, d.params.user_mul);
    d.params.user_flip = 0;
    d.params.exp_correc = 1;
    d.params.exp_shift = expShift;
    d.params.exp_preser = 1.0;
    d.params.half_size = halfSize ? 1 : 0; // much faster, will be used for preview size 'half' or 'none'
    if (rawProcessor->open_file(params.fileName.toLocal8Bit().constData()) == LIBRAW_SUCCESS) {
//             && rawProcessor.unpack() == LIBRAW_SUCCESS) {
        prepareRawBuffer(*(rawProcessor.get()));
        // Assume the other sizes are the same as in the raw parameters
        d.sizes.width = params.width;
        d.sizes.height = params.height;
        float scale = d.params.user_sat / (float)(params.max - params.black);
        for (size_t y = 0; y < params.rawHeight; ++y) {
            for (size_t x = 0; x < params.rawWidth; ++x) {
                size_t pos = y*params.rawWidth + x;
                int v = (rawData[pos] - params.blackAt(x - params.leftMargin, y - params.topMargin)) * scale;
                if (v < 0) v = 0;
                else if (v > 65535) v = 65535;
                d.rawdata.raw_image[pos] = v;
            }
        }
        rawProcessor->dcraw_process();
        libraw_processed_image_t * image = rawProcessor->dcraw_make_mem_image();
        if (image == nullptr) {
            Log::msg(2, "dcraw_make_mem_image() returned NULL");
        } else {
            QImage interpolated(image->width, image->height, QImage::Format_RGB32);
            if (interpolated.isNull()) return QImage();
            for (int y = 0; y < image->height; ++y) {
                QRgb* scanline = (QRgb*)interpolated.scanLine(y);
                int pos = (y*image->width)*3;
                for (int x = 0; x < image->width; ++x) {
                    int r = image->data[pos++], g = image->data[pos++], b = image->data[pos++];
                    scanline[x] = qRgb(r, g, b);
                }
            }
            LibRaw::dcraw_clear_mem(image);
            // The result may be some pixels bigger than the original...
            return interpolated.copy(0, 0, params.width/(halfSize ? 2 : 1 ), params.height/(halfSize ? 2 : 1 ));
        }
    }
    return QImage();
}


class FileNameManipulator {
public:
    FileNameManipulator(const vector<unique_ptr<RawParameters>> & paramList) {
        names.reserve(paramList.size());
        for (auto & rp : paramList) {
            names.push_back(rp->fileName);
        }
        sort(names.begin(), names.end());
    }

    QString getInputBaseName(int i) {
        i = adjustIndex(i);
        if (i == -1) return QString();
        else return getBaseName(names[i]);
    }

    QString getInputBaseNameNoExt(int i) {
        QString name = getInputBaseName(i);
        return name.mid(0, name.lastIndexOf('.'));
    }

    QString getInputDirName(int i) {
        i = adjustIndex(i);
        if (i == -1) return QString();
        else return getDirName(names[i]);
    }

    QString getInputNumberSuffix(int i) {
        QString name = getInputBaseNameNoExt(i);
        int pos = name.length() - 1;
        while (pos >= 0 && name[pos] >= '0' && name[pos] <= '9') pos--;
        return name.mid(pos + 1);
    }

    static QString getBaseName(const QString & name) {
        return QFileInfo(name).fileName();
    }

    static QString getDirName(const QString & name) {
        return QFileInfo(name).canonicalPath();
    }

private:
    vector<QString> names;
    int adjustIndex(int i) {
        if (i < 0)
            i = names.size() + i;
        return i < 0 || i >= (int)names.size() ? -1 : i;
    }
};


QString ImageIO::buildOutputFileName() const {
    if (rawParameters.size() > 1)
        return replaceArguments("%id[-1]/%iF[0]-%in[-1].dng", "");
    else
        return replaceArguments("%id[-1]/%iF[0].dng", "");
}


QString ImageIO::getInputPath() const {
    return FileNameManipulator::getDirName(rawParameters[0]->fileName);
}


QString ImageIO::replaceArguments(const QString & pattern, const QString & outFileName) const {
    QString result(pattern);
    QRegExp re;
    if (outFileName == "") {
        re = QRegExp("%(?:i[fFdn]\\[(-?[0-9]+)\\]|%)");
    } else {
        re = QRegExp("%(?:o[fd]|i[fFdn]\\[(-?[0-9]+)\\]|%)");
    }
    int index = 0;
    FileNameManipulator fnm(rawParameters);
    while ((index = result.indexOf(re, index)) != -1) {
        // What was matched?
        QString token = re.cap();
        if (token[1] == '%') {
            result.replace(index, 2, '%');
        } else if (token[1] == 'o') {
            if (token[2] == 'f') {
                result.replace(index, 3, fnm.getBaseName(outFileName));
            } else {
                result.replace(index, 3, fnm.getDirName(outFileName));
            }
        } else { // 'i'
            int imageIndex = re.cap(1).toInt();
            int length = re.cap(1).length() + 5;
            if (token[2] == 'f') {
                result.replace(index, length, fnm.getInputBaseName(imageIndex));
            } else if (token[2] == 'F') {
                result.replace(index, length, fnm.getInputBaseNameNoExt(imageIndex));
            } else if (token[2] == 'd') {
                result.replace(index, length, fnm.getInputDirName(imageIndex));
            } else { // 'n'
                result.replace(index, length, fnm.getInputNumberSuffix(imageIndex));
            }
        }
        index++;
    }
    return result;
}
