/*-------------------------------------------------------------------------
 * drawElements Quality Program Test Executor
 * ------------------------------------------
 *
 * Copyright 2014 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.
 *
 *//*!
 * \file
 * \brief Test log writer.
 *//*--------------------------------------------------------------------*/

#include "xeTestLogWriter.hpp"
#include "xeXMLWriter.hpp"
#include "deStringUtil.hpp"

#include <fstream>

namespace xe
{

/* Batch result writer. */

struct ContainerValue
{
    ContainerValue(const std::string &value_) : value(value_)
    {
    }
    ContainerValue(const char *value_) : value(value_)
    {
    }
    std::string value;
};

std::ostream &operator<<(std::ostream &stream, const ContainerValue &value)
{
    if (value.value.find(' ') != std::string::npos)
    {
        // Escape.
        stream << '"';
        for (std::string::const_iterator i = value.value.begin(); i != value.value.end(); i++)
        {
            if (*i == '"' || *i == '\\')
                stream << '\\';
            stream << *i;
        }
        stream << '"';
    }
    else
        stream << value.value;

    return stream;
}

static void writeSessionInfo(const SessionInfo &info, std::ostream &stream)
{
    if (!info.releaseName.empty())
        stream << "#sessionInfo releaseName " << ContainerValue(info.releaseName) << "\n";

    if (!info.releaseId.empty())
        stream << "#sessionInfo releaseId " << ContainerValue(info.releaseId) << "\n";

    if (!info.targetName.empty())
        stream << "#sessionInfo targetName " << ContainerValue(info.targetName) << "\n";

    if (!info.candyTargetName.empty())
        stream << "#sessionInfo candyTargetName " << ContainerValue(info.candyTargetName) << "\n";

    if (!info.configName.empty())
        stream << "#sessionInfo configName " << ContainerValue(info.configName) << "\n";

    if (!info.resultName.empty())
        stream << "#sessionInfo resultName " << ContainerValue(info.resultName) << "\n";

    // \note Current format uses unescaped timestamps for some strange reason.
    if (!info.timestamp.empty())
        stream << "#sessionInfo timestamp " << info.timestamp << "\n";
}

static void writeTestCase(const TestCaseResultData &caseData, std::ostream &stream)
{
    stream << "\n#beginTestCaseResult " << caseData.getTestCasePath() << "\n";

    if (caseData.getDataSize() > 0)
    {
        stream.write((const char *)caseData.getData(), caseData.getDataSize());

        uint8_t lastCh = caseData.getData()[caseData.getDataSize() - 1];
        if (lastCh != '\n' && lastCh != '\r')
            stream << "\n";
    }

    TestStatusCode dataCode = caseData.getStatusCode();
    if (dataCode == TESTSTATUSCODE_CRASH || dataCode == TESTSTATUSCODE_TIMEOUT || dataCode == TESTSTATUSCODE_TERMINATED)
        stream << "#terminateTestCaseResult " << getTestStatusCodeName(dataCode) << "\n";
    else
        stream << "#endTestCaseResult\n";
}

void writeTestLog(const BatchResult &result, std::ostream &stream)
{
    writeSessionInfo(result.getSessionInfo(), stream);

    stream << "#beginSession\n";

    for (int ndx = 0; ndx < result.getNumTestCaseResults(); ndx++)
    {
        ConstTestCaseResultPtr caseData = result.getTestCaseResult(ndx);
        writeTestCase(*caseData, stream);
    }

    stream << "\n#endSession\n";
}

void writeBatchResultToFile(const BatchResult &result, const char *filename)
{
    std::ofstream str(filename, std::ofstream::binary | std::ofstream::trunc);
    writeTestLog(result, str);
    str.close();
}

/* Test result log writer. */

static const char *getImageFormatName(ri::Image::Format format)
{
    switch (format)
    {
    case ri::Image::FORMAT_RGB888:
        return "RGB888";
    case ri::Image::FORMAT_RGBA8888:
        return "RGBA8888";
    default:
        DE_ASSERT(false);
        return DE_NULL;
    }
}

static const char *getImageCompressionName(ri::Image::Compression compression)
{
    switch (compression)
    {
    case ri::Image::COMPRESSION_NONE:
        return "None";
    case ri::Image::COMPRESSION_PNG:
        return "PNG";
    default:
        DE_ASSERT(false);
        return DE_NULL;
    }
}

static const char *getSampleValueTagName(ri::ValueInfo::ValueTag tag)
{
    switch (tag)
    {
    case ri::ValueInfo::VALUETAG_PREDICTOR:
        return "Predictor";
    case ri::ValueInfo::VALUETAG_RESPONSE:
        return "Response";
    default:
        DE_ASSERT(false);
        return DE_NULL;
    }
}

inline const char *getBoolName(bool val)
{
    return val ? "True" : "False";
}

// \todo [2012-09-07 pyry] Move to tcutil?
class Base64Formatter
{
public:
    const uint8_t *data;
    int numBytes;

    Base64Formatter(const uint8_t *data_, int numBytes_) : data(data_), numBytes(numBytes_)
    {
    }
};

std::ostream &operator<<(std::ostream &str, const Base64Formatter &fmt)
{
    static const char s_base64Table[64] = {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
        'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
        's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};

    const uint8_t *data = fmt.data;
    int numBytes        = fmt.numBytes;
    int srcNdx          = 0;

    DE_ASSERT(data && (numBytes > 0));

    /* Loop all input chars. */
    while (srcNdx < numBytes)
    {
        int numRead = de::min(3, numBytes - srcNdx);
        uint8_t s0  = data[srcNdx];
        uint8_t s1  = (numRead >= 2) ? data[srcNdx + 1] : 0;
        uint8_t s2  = (numRead >= 3) ? data[srcNdx + 2] : 0;
        char d[4];

        srcNdx += numRead;

        d[0] = s_base64Table[s0 >> 2];
        d[1] = s_base64Table[((s0 & 0x3) << 4) | (s1 >> 4)];
        d[2] = s_base64Table[((s1 & 0xF) << 2) | (s2 >> 6)];
        d[3] = s_base64Table[s2 & 0x3F];

        if (numRead < 3)
            d[3] = '=';
        if (numRead < 2)
            d[2] = '=';

        /* Write data. */
        str.write(&d[0], sizeof(d));
    }

    return str;
}

inline Base64Formatter toBase64(const uint8_t *bytes, int numBytes)
{
    return Base64Formatter(bytes, numBytes);
}

static const char *getStatusName(bool value)
{
    return value ? "OK" : "Fail";
}

static void writeResultItem(const ri::Item &item, xml::Writer &dst)
{
    using xml::Writer;

    switch (item.getType())
    {
    case ri::TYPE_RESULT:
        // Ignored here, written at end.
        break;

    case ri::TYPE_TEXT:
        dst << Writer::BeginElement("Text") << static_cast<const ri::Text &>(item).text << Writer::EndElement;
        break;

    case ri::TYPE_NUMBER:
    {
        const ri::Number &number = static_cast<const ri::Number &>(item);
        dst << Writer::BeginElement("Number") << Writer::Attribute("Name", number.name)
            << Writer::Attribute("Description", number.description) << Writer::Attribute("Unit", number.unit)
            << Writer::Attribute("Tag", number.tag) << number.value << Writer::EndElement;
        break;
    }

    case ri::TYPE_IMAGE:
    {
        const ri::Image &image = static_cast<const ri::Image &>(item);
        dst << Writer::BeginElement("Image") << Writer::Attribute("Name", image.name)
            << Writer::Attribute("Description", image.description)
            << Writer::Attribute("Width", de::toString(image.width))
            << Writer::Attribute("Height", de::toString(image.height))
            << Writer::Attribute("Format", getImageFormatName(image.format))
            << Writer::Attribute("CompressionMode", getImageCompressionName(image.compression))
            << toBase64(&image.data[0], (int)image.data.size()) << Writer::EndElement;
        break;
    }

    case ri::TYPE_IMAGESET:
    {
        const ri::ImageSet &imageSet = static_cast<const ri::ImageSet &>(item);
        dst << Writer::BeginElement("ImageSet") << Writer::Attribute("Name", imageSet.name)
            << Writer::Attribute("Description", imageSet.description);

        for (int ndx = 0; ndx < imageSet.images.getNumItems(); ndx++)
            writeResultItem(imageSet.images.getItem(ndx), dst);

        dst << Writer::EndElement;
        break;
    }

    case ri::TYPE_SHADER:
    {
        const ri::Shader &shader = static_cast<const ri::Shader &>(item);
        const char *tagName      = DE_NULL;

        switch (shader.shaderType)
        {
        case ri::Shader::SHADERTYPE_VERTEX:
            tagName = "VertexShader";
            break;
        case ri::Shader::SHADERTYPE_FRAGMENT:
            tagName = "FragmentShader";
            break;
        case ri::Shader::SHADERTYPE_GEOMETRY:
            tagName = "GeometryShader";
            break;
        case ri::Shader::SHADERTYPE_TESS_CONTROL:
            tagName = "TessControlShader";
            break;
        case ri::Shader::SHADERTYPE_TESS_EVALUATION:
            tagName = "TessEvaluationShader";
            break;
        case ri::Shader::SHADERTYPE_COMPUTE:
            tagName = "ComputeShader";
            break;
        case ri::Shader::SHADERTYPE_RAYGEN:
            tagName = "RaygenShader";
            break;
        case ri::Shader::SHADERTYPE_ANY_HIT:
            tagName = "AnyHitShader";
            break;
        case ri::Shader::SHADERTYPE_CLOSEST_HIT:
            tagName = "ClosestHitShader";
            break;
        case ri::Shader::SHADERTYPE_MISS:
            tagName = "MissShader";
            break;
        case ri::Shader::SHADERTYPE_INTERSECTION:
            tagName = "IntersectionShader";
            break;
        case ri::Shader::SHADERTYPE_CALLABLE:
            tagName = "CallableShader";
            break;
        case ri::Shader::SHADERTYPE_TASK:
            tagName = "TaskShader";
            break;
        case ri::Shader::SHADERTYPE_MESH:
            tagName = "MeshShader";
            break;

        default:
            throw Error("Unknown shader type");
        }

        dst << Writer::BeginElement(tagName) << Writer::Attribute("CompileStatus", getStatusName(shader.compileStatus));

        writeResultItem(shader.source, dst);
        writeResultItem(shader.infoLog, dst);

        dst << Writer::EndElement;
        break;
    }

    case ri::TYPE_SHADERPROGRAM:
    {
        const ri::ShaderProgram &program = static_cast<const ri::ShaderProgram &>(item);
        dst << Writer::BeginElement("ShaderProgram")
            << Writer::Attribute("LinkStatus", getStatusName(program.linkStatus));

        writeResultItem(program.linkInfoLog, dst);

        for (int ndx = 0; ndx < program.shaders.getNumItems(); ndx++)
            writeResultItem(program.shaders.getItem(ndx), dst);

        dst << Writer::EndElement;
        break;
    }

    case ri::TYPE_SHADERSOURCE:
        dst << Writer::BeginElement("ShaderSource") << static_cast<const ri::ShaderSource &>(item).source
            << Writer::EndElement;
        break;

    case ri::TYPE_SPIRVSOURCE:
        dst << Writer::BeginElement("SpirVAssemblySource") << static_cast<const ri::SpirVSource &>(item).source
            << Writer::EndElement;
        break;

    case ri::TYPE_INFOLOG:
        dst << Writer::BeginElement("InfoLog") << static_cast<const ri::InfoLog &>(item).log << Writer::EndElement;
        break;

    case ri::TYPE_SECTION:
    {
        const ri::Section &section = static_cast<const ri::Section &>(item);
        dst << Writer::BeginElement("Section") << Writer::Attribute("Name", section.name)
            << Writer::Attribute("Description", section.description);

        for (int ndx = 0; ndx < section.items.getNumItems(); ndx++)
            writeResultItem(section.items.getItem(ndx), dst);

        dst << Writer::EndElement;
        break;
    }

    case ri::TYPE_KERNELSOURCE:
        dst << Writer::BeginElement("KernelSource") << static_cast<const ri::KernelSource &>(item).source
            << Writer::EndElement;
        break;

    case ri::TYPE_COMPILEINFO:
    {
        const ri::CompileInfo &compileInfo = static_cast<const ri::CompileInfo &>(item);
        dst << Writer::BeginElement("CompileInfo") << Writer::Attribute("Name", compileInfo.name)
            << Writer::Attribute("Description", compileInfo.description)
            << Writer::Attribute("CompileStatus", getStatusName(compileInfo.compileStatus));

        writeResultItem(compileInfo.infoLog, dst);

        dst << Writer::EndElement;
        break;
    }

    case ri::TYPE_EGLCONFIG:
    {
        const ri::EglConfig &config = static_cast<const ri::EglConfig &>(item);
        dst << Writer::BeginElement("EglConfig") << Writer::Attribute("BufferSize", de::toString(config.bufferSize))
            << Writer::Attribute("RedSize", de::toString(config.redSize))
            << Writer::Attribute("GreenSize", de::toString(config.greenSize))
            << Writer::Attribute("BlueSize", de::toString(config.blueSize))
            << Writer::Attribute("LuminanceSize", de::toString(config.luminanceSize))
            << Writer::Attribute("AlphaSize", de::toString(config.alphaSize))
            << Writer::Attribute("AlphaMaskSize", de::toString(config.alphaMaskSize))
            << Writer::Attribute("BindToTextureRGB", getBoolName(config.bindToTextureRGB))
            << Writer::Attribute("BindToTextureRGBA", getBoolName(config.bindToTextureRGBA))
            << Writer::Attribute("ColorBufferType", config.colorBufferType)
            << Writer::Attribute("ConfigCaveat", config.configCaveat)
            << Writer::Attribute("ConfigID", de::toString(config.configID))
            << Writer::Attribute("Conformant", config.conformant)
            << Writer::Attribute("DepthSize", de::toString(config.depthSize))
            << Writer::Attribute("Level", de::toString(config.level))
            << Writer::Attribute("MaxPBufferWidth", de::toString(config.maxPBufferWidth))
            << Writer::Attribute("MaxPBufferHeight", de::toString(config.maxPBufferHeight))
            << Writer::Attribute("MaxPBufferPixels", de::toString(config.maxPBufferPixels))
            << Writer::Attribute("MaxSwapInterval", de::toString(config.maxSwapInterval))
            << Writer::Attribute("MinSwapInterval", de::toString(config.minSwapInterval))
            << Writer::Attribute("NativeRenderable", getBoolName(config.nativeRenderable))
            << Writer::Attribute("RenderableType", config.renderableType)
            << Writer::Attribute("SampleBuffers", de::toString(config.sampleBuffers))
            << Writer::Attribute("Samples", de::toString(config.samples))
            << Writer::Attribute("StencilSize", de::toString(config.stencilSize))
            << Writer::Attribute("SurfaceTypes", config.surfaceTypes)
            << Writer::Attribute("TransparentType", config.transparentType)
            << Writer::Attribute("TransparentRedValue", de::toString(config.transparentRedValue))
            << Writer::Attribute("TransparentGreenValue", de::toString(config.transparentGreenValue))
            << Writer::Attribute("TransparentBlueValue", de::toString(config.transparentBlueValue))
            << Writer::EndElement;
        break;
    }

    case ri::TYPE_EGLCONFIGSET:
    {
        const ri::EglConfigSet &configSet = static_cast<const ri::EglConfigSet &>(item);
        dst << Writer::BeginElement("EglConfigSet") << Writer::Attribute("Name", configSet.name)
            << Writer::Attribute("Description", configSet.description);

        for (int ndx = 0; ndx < configSet.configs.getNumItems(); ndx++)
            writeResultItem(configSet.configs.getItem(ndx), dst);

        dst << Writer::EndElement;
        break;
    }

    case ri::TYPE_SAMPLELIST:
    {
        const ri::SampleList &list = static_cast<const ri::SampleList &>(item);
        dst << Writer::BeginElement("SampleList") << Writer::Attribute("Name", list.name)
            << Writer::Attribute("Description", list.description);

        writeResultItem(list.sampleInfo, dst);

        for (int ndx = 0; ndx < list.samples.getNumItems(); ndx++)
            writeResultItem(list.samples.getItem(ndx), dst);

        dst << Writer::EndElement;
        break;
    }

    case ri::TYPE_SAMPLEINFO:
    {
        const ri::SampleInfo &info = static_cast<const ri::SampleInfo &>(item);
        dst << Writer::BeginElement("SampleInfo");
        for (int ndx = 0; ndx < info.valueInfos.getNumItems(); ndx++)
            writeResultItem(info.valueInfos.getItem(ndx), dst);
        dst << Writer::EndElement;
        break;
    }

    case ri::TYPE_VALUEINFO:
    {
        const ri::ValueInfo &info = static_cast<const ri::ValueInfo &>(item);
        dst << Writer::BeginElement("ValueInfo") << Writer::Attribute("Name", info.name)
            << Writer::Attribute("Description", info.description)
            << Writer::Attribute("Tag", getSampleValueTagName(info.tag));
        if (!info.unit.empty())
            dst << Writer::Attribute("Unit", info.unit);
        dst << Writer::EndElement;
        break;
    }

    case ri::TYPE_SAMPLE:
    {
        const ri::Sample &sample = static_cast<const ri::Sample &>(item);
        dst << Writer::BeginElement("Sample");
        for (int ndx = 0; ndx < sample.values.getNumItems(); ndx++)
            writeResultItem(sample.values.getItem(ndx), dst);
        dst << Writer::EndElement;
        break;
    }

    case ri::TYPE_SAMPLEVALUE:
    {
        const ri::SampleValue &value = static_cast<const ri::SampleValue &>(item);
        dst << Writer::BeginElement("Value") << value.value << Writer::EndElement;
        break;
    }

    default:
        XE_FAIL("Unsupported result item");
    }
}

void writeTestResult(const TestCaseResult &result, xe::xml::Writer &xmlWriter)
{
    using xml::Writer;

    xmlWriter << Writer::BeginElement("TestCaseResult") << Writer::Attribute("Version", result.caseVersion)
              << Writer::Attribute("CasePath", result.casePath)
              << Writer::Attribute("CaseType", getTestCaseTypeName(result.caseType));

    for (int ndx = 0; ndx < result.resultItems.getNumItems(); ndx++)
        writeResultItem(result.resultItems.getItem(ndx), xmlWriter);

    // Result item is not logged until end.
    xmlWriter << Writer::BeginElement("Result")
              << Writer::Attribute("StatusCode", getTestStatusCodeName(result.statusCode)) << result.statusDetails
              << Writer::EndElement;

    xmlWriter << Writer::EndElement;
}

void writeTestResult(const TestCaseResult &result, std::ostream &stream)
{
    xml::Writer xmlWriter(stream);
    stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
    writeTestResult(result, xmlWriter);
}

void writeTestResultToFile(const TestCaseResult &result, const char *filename)
{
    std::ofstream str(filename, std::ofstream::binary | std::ofstream::trunc);
    writeTestResult(result, str);
    str.close();
}

} // namespace xe
