/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//#define LOG_NDEBUG 0
#define LOG_TAG "DngValidateCamera"
#include <log/log.h>
#include <jni.h>

#include <string>
#include <sstream>
#include <iostream>

/**
 * Use DNG SDK to validate captured DNG file.
 *
 * This code is largely based on the dng_validate.cpp implementation included
 * with the DNG SDK. The portions of this file that are from the DNG SDK are
 * covered by the the DNG SDK license in /external/dng_sdk/LICENSE
 */

#include "dng_color_space.h"
#include "dng_date_time.h"
#include "dng_exceptions.h"
#include "dng_file_stream.h"
#include "dng_globals.h"
#include "dng_host.h"
#include "dng_ifd.h"
#include "dng_image_writer.h"
#include "dng_info.h"
#include "dng_linearization_info.h"
#include "dng_mosaic_info.h"
#include "dng_negative.h"
#include "dng_preview.h"
#include "dng_render.h"
#include "dng_simple_image.h"
#include "dng_tag_codes.h"
#include "dng_tag_types.h"
#include "dng_tag_values.h"

// Version of DNG validate referenced for this implementation
#define kDNGValidateVersion "1.4"

static bool gFourColorBayer = false;

static int32 gMosaicPlane = -1;

static uint32 gPreferredSize = 0;
static uint32 gMinimumSize   = 0;
static uint32 gMaximumSize   = 0;

static uint32 gProxyDNGSize = 0;

static const dng_color_space *gFinalSpace = &dng_space_sRGB::Get();

static uint32 gFinalPixelType = ttByte;

static dng_string gDumpStage1;
static dng_string gDumpStage2;
static dng_string gDumpStage3;
static dng_string gDumpTIF;
static dng_string gDumpDNG;

/**
 * Validate DNG file in provided buffer.
 *
 * Returns dng_error_none (0) on success, otherwise one of the
 * dng_error_code enum values is returned.
 *
 * Warnings and errors found during validation are printed to stderr
 */
static dng_error_code dng_validate(const void* data, uint32_t count) {

    ALOGI("Validating DNG buffer");

    try {
        dng_stream stream(data, count);

        dng_host host;

        host.SetPreferredSize(gPreferredSize);
        host.SetMinimumSize(gMinimumSize);
        host.SetMaximumSize(gMaximumSize);

        host.ValidateSizes();

        if (host.MinimumSize()) {
            host.SetForPreview(true);
            gDumpDNG.Clear();
        }

        if (gDumpDNG.NotEmpty()) {
            host.SetSaveDNGVersion(dngVersion_SaveDefault);
            host.SetSaveLinearDNG(false);
            host.SetKeepOriginalFile(false);
        }

        // Read into the negative.

        AutoPtr<dng_negative> negative;
        {
            dng_info info;
            info.Parse(host, stream);
            info.PostParse(host);
            if (!info.IsValidDNG()) {
                return dng_error_bad_format;
            }

            negative.Reset(host.Make_dng_negative());
            negative->Parse(host, stream, info);
            negative->PostParse(host, stream, info);

            {
                dng_timer timer("Raw image read time");
                negative->ReadStage1Image(host, stream, info);
            }

            if (info.fMaskIndex != -1) {
                dng_timer timer("Transparency mask read time");
                negative->ReadTransparencyMask(host, stream, info);
            }

            negative->ValidateRawImageDigest(host);
        }

        // Option to write stage 1 image.

        if (gDumpStage1.NotEmpty()) {
            dng_file_stream stream2 (gDumpStage1.Get(), true);
            const dng_image &stage1 = *negative->Stage1Image();
            dng_image_writer writer;

            writer.WriteTIFF(host,
                    stream2,
                    stage1,
                    stage1.Planes() >= 3 ? piRGB
                    : piBlackIsZero);

            gDumpStage1.Clear();
        }

        // Metadata.

        negative->SynchronizeMetadata();

        // Four color Bayer option.

        if (gFourColorBayer) {
            negative->SetFourColorBayer();
        }

        // Build stage 2 image.

        {
            dng_timer timer("Linearization time");
            negative->BuildStage2Image(host);
        }

        if (gDumpStage2.NotEmpty()) {
            dng_file_stream stream2(gDumpStage2.Get(), true);
            const dng_image &stage2 = *negative->Stage2Image();
            dng_image_writer writer;

            writer.WriteTIFF (host,
                    stream2,
                    stage2,
                    stage2.Planes() >= 3 ? piRGB
                    : piBlackIsZero);

            gDumpStage2.Clear();
        }

        // Build stage 3 image.

        {
            dng_timer timer("Interpolate time");
            negative->BuildStage3Image(host,
                    gMosaicPlane);
        }

        // Convert to proxy, if requested.

        if (gProxyDNGSize) {
            dng_timer timer("ConvertToProxy time");
            dng_image_writer writer;

            negative->ConvertToProxy(host,
                    writer,
                    gProxyDNGSize);
        }

        // Flatten transparency, if required.

        if (negative->NeedFlattenTransparency(host)) {
            dng_timer timer("FlattenTransparency time");
            negative->FlattenTransparency(host);
        }

        if (gDumpStage3.NotEmpty()) {
            dng_file_stream stream2(gDumpStage3.Get(), true);
            const dng_image &stage3 = *negative->Stage3Image();
            dng_image_writer writer;

            writer.WriteTIFF (host,
                    stream2,
                    stage3,
                    stage3.Planes () >= 3 ? piRGB
                    : piBlackIsZero);

            gDumpStage3.Clear();
        }

        // Output DNG file if requested.

        if (gDumpDNG.NotEmpty()) {
            // Build the preview list.
            dng_preview_list previewList;
            dng_date_time_info dateTimeInfo;
            CurrentDateTimeAndZone(dateTimeInfo);

            for (uint32 previewIndex = 0; previewIndex < 2; previewIndex++) {

                // Skip preview if writing a compresssed main image to save space
                // in this example code.
                if (negative->RawJPEGImage() != NULL && previewIndex > 0) {
                    break;
                }

                // Report timing.
                dng_timer timer(previewIndex == 0 ? "Build thumbnail time"
                        : "Build preview time");

                // Render a preview sized image.
                AutoPtr<dng_image> previewImage;

                {
                    dng_render render (host, *negative);
                    render.SetFinalSpace (negative->IsMonochrome() ?
                            dng_space_GrayGamma22::Get() : dng_space_sRGB::Get());
                    render.SetFinalPixelType (ttByte);
                    render.SetMaximumSize (previewIndex == 0 ? 256 : 1024);

                    previewImage.Reset (render.Render());
                }

                // Don't write the preview if it is same size as thumbnail.

                if (previewIndex > 0 &&
                        Max_uint32(previewImage->Bounds().W(),
                                previewImage->Bounds().H()) <= 256) {
                    break;
                }

                // If we have compressed JPEG data, create a compressed thumbnail.  Otherwise
                // save a uncompressed thumbnail.
                bool useCompressedPreview = (negative->RawJPEGImage() != NULL) ||
                        (previewIndex > 0);

                AutoPtr<dng_preview> preview (useCompressedPreview ?
                        (dng_preview *) new dng_jpeg_preview :
                        (dng_preview *) new dng_image_preview);

                // Setup up preview info.

                preview->fInfo.fApplicationName.Set("dng_validate");
                preview->fInfo.fApplicationVersion.Set(kDNGValidateVersion);

                preview->fInfo.fSettingsName.Set("Default");

                preview->fInfo.fColorSpace = previewImage->Planes() == 1 ?
                        previewColorSpace_GrayGamma22 :
                        previewColorSpace_sRGB;

                preview->fInfo.fDateTime = dateTimeInfo.Encode_ISO_8601();

                if (!useCompressedPreview) {
                    dng_image_preview *imagePreview = static_cast<dng_image_preview *>(preview.Get());
                    imagePreview->fImage.Reset(previewImage.Release());
                } else {
                    dng_jpeg_preview *jpegPreview = static_cast<dng_jpeg_preview *>(preview.Get());
                    int32 quality = (previewIndex == 0 ? 8 : 5);
                    dng_image_writer writer;
                    writer.EncodeJPEGPreview (host,
                            *previewImage,
                            *jpegPreview,
                            quality);
                }
                previewList.Append (preview);
            }

            // Write DNG file.

            dng_file_stream stream2(gDumpDNG.Get(), true);

            {
                dng_timer timer("Write DNG time");
                dng_image_writer writer;

                writer.WriteDNG(host,
                        stream2,
                        *negative.Get(),
                        &previewList,
                        dngVersion_Current,
                        false);
            }

            gDumpDNG.Clear();
        }

        // Output TIF file if requested.
        if (gDumpTIF.NotEmpty()) {

            // Render final image.

            dng_render render(host, *negative);

            render.SetFinalSpace(*gFinalSpace   );
            render.SetFinalPixelType(gFinalPixelType);

            if (host.MinimumSize()) {
                dng_point stage3Size = negative->Stage3Image()->Size();
                render.SetMaximumSize (Max_uint32(stage3Size.v,
                                stage3Size.h));
            }

            AutoPtr<dng_image> finalImage;

            {
                dng_timer timer("Render time");
                finalImage.Reset(render.Render());
            }

            finalImage->Rotate(negative->Orientation());

            // Now that Camera Raw supports non-raw formats, we should
            // not keep any Camera Raw settings in the XMP around when
            // writing rendered files.
#if qDNGUseXMP
            if (negative->GetXMP()) {
                negative->GetXMP()->RemoveProperties(XMP_NS_CRS);
                negative->GetXMP()->RemoveProperties(XMP_NS_CRSS);
            }
#endif

            // Write TIF file.
            dng_file_stream stream2(gDumpTIF.Get(), true);

            {
                dng_timer timer("Write TIFF time");
                dng_image_writer writer;

                writer.WriteTIFF(host,
                        stream2,
                        *finalImage.Get(),
                        finalImage->Planes() >= 3 ? piRGB
                        : piBlackIsZero,
                        ccUncompressed,
                        negative.Get(),
                        &render.FinalSpace());
            }
            gDumpTIF.Clear();
        }
    } catch (const dng_exception &except) {
        return except.ErrorCode();
    } catch (...) {
        return dng_error_unknown;
    }

    ALOGI("DNG validation complete");

    return dng_error_none;
}

extern "C" jboolean
Java_android_hardware_camera2_cts_DngCreatorTest_validateDngNative(
    JNIEnv* env, jclass /*clazz*/, jbyteArray dngBuffer) {

    jbyte* buffer = env->GetByteArrayElements(dngBuffer, NULL);
    jsize bufferCount = env->GetArrayLength(dngBuffer);
    if (buffer == nullptr) {
        ALOGE("Unable to map DNG buffer to native");
        return JNI_FALSE;
    }

    // DNG parsing warnings/errors fprintfs are spread throughout the DNG SDK,
    // guarded by the qDNGValidate define flag. To avoid modifying the SDK,
    // redirect stderr to a pipe to capture output locally.

    int pipeFds[2];
    int err;

    err = pipe(pipeFds);
    if (err != 0) {
        ALOGE("Error redirecting dng_validate output: %d", errno);
        env->ReleaseByteArrayElements(dngBuffer, buffer, 0);
        return JNI_FALSE;
    }

    int stderrFd = dup(fileno(stderr));
    dup2(pipeFds[1], fileno(stderr));
    close(pipeFds[1]);

    // Actually run the validation
    dng_error_code dng_err = dng_validate(buffer, bufferCount);

    env->ReleaseByteArrayElements(dngBuffer, buffer, 0);

    // Restore stderr and read out pipe
    dup2(stderrFd, fileno(stderr));

    std::stringstream errorStream;
    const size_t BUF_SIZE = 256;
    char readBuf[BUF_SIZE];

    ssize_t count = 0;
    while((count = read(pipeFds[0], readBuf, BUF_SIZE)) > 0) {
        errorStream.write(readBuf, count);
    }
    if (count < 0) {
        ALOGE("Error reading from dng_validate output pipe: %d", errno);
        return JNI_FALSE;
    }
    close(pipeFds[1]);

    std::string line;
    int lineCount = 0;
    ALOGI("Output from DNG validation:");
    // dng_validate doesn't actually propagate all errors/warnings to the
    // return error code, so look for an error pattern in output to detect
    // problems. Also make sure the output is long enough since some non-error
    // content should always be printed.
    while(std::getline(errorStream, line, '\n')) {
        lineCount++;
        if ( (line.size() > 3) &&
                (line[0] == line[1]) &&
                (line[1] == line[2]) &&
                (line[2] == '*') ) {
            // Found a warning or error, so need to fail the test
            if (dng_err == dng_error_none) {
                dng_err = dng_error_bad_format;
            }
            ALOGE("**|%s", line.c_str());
        } else {
            ALOGI("  |%s", line.c_str());
        }
    }
    // If no output is produced, assume something went wrong
    if (lineCount < 3) {
        ALOGE("Validation output less than expected!");
        dng_err = dng_error_unknown;
    }
    if (dng_err != dng_error_none) {
        ALOGE("DNG validation failed!");
    }

    return (dng_err == dng_error_none) ? JNI_TRUE : JNI_FALSE;
}