<file_sep>/**
 * Copyright (c) 2017 Melown Technologies SE
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * *  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * *  Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <map>
#include <queue>
#include <string>
#include <tuple>
#include <fstream>
#include <algorithm>
#include <atomic>
#include <mutex>
#include <utility>

#include <boost/utility/in_place_factory.hpp>
#include <boost/filesystem.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/format.hpp>
#include <boost/iostreams/filter/gzip.hpp>

#include "dbglog/dbglog.hpp"

#include "utility/path.hpp"
#include "utility/streams.hpp"
#include "utility/path.hpp"
#include "utility/uri.hpp"
#include "utility/binaryio.hpp"
#include "utility/format.hpp"
#include "utility/stl-helpers.hpp"

#include "imgproc/readimage.hpp"

#include "jsoncpp/json.hpp"
#include "jsoncpp/as.hpp"
#include "jsoncpp/io.hpp"

#include "writer.hpp"
#include "restapi.hpp"
#include "detail/files.hpp"

namespace fs = boost::filesystem;
namespace bio = boost::iostreams;
namespace bin = utility::binaryio;

namespace slpk {

TextureSaver::~TextureSaver() {}
MeshSaver::~MeshSaver() {}

namespace {

template <typename T>
std::string asString(const T &value)
{
    return boost::lexical_cast<std::string>(value);
}

// fwd
template <typename T>
void build(Json::Value &value, const std::vector<T> &array);

// builders

void build(Json::Value &value, const Capabilities &capabilities)
{
    value = Json::arrayValue;
    for (auto capability : capabilities) {
        value.append(asString(capability));
    }
}

void build(Json::Value &value, const ResourcePatterns &resourcepatterns)
{
    value = Json::arrayValue;
    for (auto resourcepattern : resourcepatterns) {
        value.append(asString(resourcepattern));
    }
}

void build(Json::Value &value, const SpatialReference &srs)
{
    value = Json::objectValue;
    if (srs.wkid) {
        value["wkid"] = srs.wkid;
        if (!srs.latestWkid) { value["latestWkid"] = srs.wkid; }
    }
    if (srs.latestWkid) { value["latestWkid"] = srs.latestWkid; }

    if (srs.vcsWkid) {
        value["vcsWkid"] = srs.vcsWkid;
        if (!srs.latestVcsWkid) { value["latestVcsWkid"] = srs.vcsWkid; }
    }
    if (srs.latestVcsWkid) { value["latestVcsWkid"] = srs.latestVcsWkid; }

    if (!srs.wkt.empty()) {
        value["wkt"] = srs.wkt;
    } else if (srs.wkid) {
        value["wkt"] = srs.srs().as(geo::SrsDefinition::Type::wkt).srs;
    }
}

void build(Json::Value &value, const HeightModelInfo &hmi)
{
    value = Json::objectValue;
    value["heightModel"] = asString(hmi.heightModel);
    value["ellipsoid"] = hmi.ellipsoid;
    value["heightUnit"] = hmi.heightUnit;
}

void build(Json::Value &value, const FeatureRange &range)
{
    value = Json::arrayValue;
    value.append(range.min);
    value.append(range.max);
}

void build(Json::Value &value, const math::Point3 &p)
{
    value = Json::arrayValue;
    value.append(p(0));
    value.append(p(1));
    value.append(p(2));
}

void build(Json::Value &value, const math::Extents2 &extents)
{
    value = Json::arrayValue;
    value.append(extents.ll(0));
    value.append(extents.ll(1));
    value.append(extents.ur(0));
    value.append(extents.ur(1));
}

void build(Json::Value &value, const math::Extents3 &extents)
{
    value = Json::arrayValue;
    value.append(extents.ll(0));
    value.append(extents.ll(1));
    value.append(extents.ll(2));
    value.append(extents.ur(0));
    value.append(extents.ur(1));
    value.append(extents.ur(2));
}

void build(Json::Value &value, const math::Matrix4 &m)
{
    value = Json::arrayValue;
    for (int j(0); j < 4; ++j) {
        for (int i(0); i < 4; ++i) {
            value.append(m(j, i));
        }
    }
}

void build(Json::Value &value, const Encoding &encoding)
{
    value = encoding.mime;
}

void build(Json::Value &value, const Encoding::list &encodings)
{
    value = Json::arrayValue;
    for (const auto encoding : encodings) { value.append(encoding.mime); }
}

void build(Json::Value &value, const Cardinality &cardinality)
{
    value = Json::arrayValue;
    value.append(cardinality.min);
    value.append(cardinality.max);
}

void build(Json::Value &value, const IndexScheme &indexingScheme)
{
    value = Json::objectValue;
    value["name"] = asString(indexingScheme.name);
    value["inclusive"] = indexingScheme.inclusive;
    value["dimensionality"] = indexingScheme.dimensionality;
    build(value["childrenCardinality"], indexingScheme.childrenCardinality);
    build(value["neighborCardinality"], indexingScheme.neighborCardinality);
}

void build(Json::Value &value, const HeaderAttribute &header)
{
    value = Json::objectValue;
    value["property"] = header.property;
    value["type"] = asString(header.type);
}

void build(Json::Value &value, const GeometryAttribute &attr)
{
    value = Json::objectValue;
    if (attr.byteOffset) { value["byteOffset"] = Json::Int(attr.byteOffset); }
    if (attr.count) { value["count"] = Json::Int(attr.count); }
    value["valueType"] = asString(attr.valueType);
    value["valuesPerElement"] = Json::Int(attr.valuesPerElement);

    // TODO: values?
    if (!attr.componentIndices.empty()) {
        auto jcomponentIndices(value["componentIndices"] = Json::arrayValue);
        for (auto ci : attr.componentIndices) { jcomponentIndices.append(ci); }
    }
}

void build(Json::Value &jattrs, Json::Value &jordering
           , const GeometryAttribute::list &attrs)
{
    jattrs = Json::objectValue;
    jordering = Json::arrayValue;
    for (const auto &attr : attrs) {
        build(jattrs[attr.key], attr);
        jordering.append(attr.key);
    }
}

void build(Json::Value &value, const GeometryAttribute::list &attrs)
{
    value = Json::objectValue;
    for (const auto &attr : attrs) {
        build(value[attr.key], attr);
    }
}

void build(Json::Value &value, const GeometrySchema &geometrySchema)
{
    value = Json::objectValue;
    value["geometryType"] = asString(geometrySchema.geometryType);
    value["topology"] = asString(geometrySchema.topology);
    build(value["header"], geometrySchema.header);

    if (!geometrySchema.vertexAttributes.empty()) {
        build(value["vertexAttributes"], value["ordering"]
              , geometrySchema.vertexAttributes);
    }

    if (!geometrySchema.faces.empty()) {
        build(value["faces"], geometrySchema.faces);
    }

    if (!geometrySchema.featureAttributes.empty()) {
        build(value["featureAttributes"], value["featureAttributeOrder"]
              , geometrySchema.featureAttributes);
    }
}

void build(Json::Value &value, const Store &store, const Metadata &metadata)
{
    value = Json::objectValue;
    value["id"] = store.id;
    value["profile"] = asString(store.profile);
    build(value["resourcePattern"], store.resourcePattern);

    value["rootNode"] = store.rootNode;
    value["version"] = store.version;
    build(value["extent"], store.extents);

    value["normalReferenceFrame"] = asString(store.normalReferenceFrame);

    {
        const auto gzipped
            (metadata.resourceCompressionType
             == ResourceCompressionType::gzip);
        std::string jsonEncoding
            (str(boost::format
                 ("application/vnd.esri.I3S.json%s; version=%d.%d")
                 % (gzipped ? "+gzip" : "")
                 % metadata.version.major
                 % metadata.version.minor));

        value["nidEncoding"] = jsonEncoding;
        value["featureEncoding"] = jsonEncoding;
        value["geometryEncoding"] = jsonEncoding;
    }

    build(value["textureEncoding"], store.textureEncoding);
    value["lodType"] = asString(store.lodType);
    value["lodModel"] = asString(store.lodModel);

    build(value["indexingScheme"], store.indexingScheme);

    if (store.defaultGeometrySchema) {
        build(value["defaultGeometrySchema"], *store.defaultGeometrySchema);
    }
}

void build(Json::Value &value, const Version &version)
{
    value = utility::format("%d.%d", version.major, version.minor);
}

void build(Json::Value &value, const SceneLayerInfo &sli
           , const Metadata &metadata)
{
    value = Json::objectValue;
    value["id"] = sli.id;
    value["layerType"] = asString(sli.layerType);
    value["href"] = sli.href;
    value["name"] = sli.name;
    if (sli.alias) { value["alias"] = *sli.alias; }
    if (sli.description) { value["description"] = *sli.description; }
    if (sli.copyrightText) { value["copyrightText"] = *sli.copyrightText; }

    build(value["capabilities"], sli.capabilities);

    build(value["spatialReference"], sli.spatialReference);

    build(value["heightModelInfo"], sli.heightModelInfo);

    if (sli.store) {
        auto &store(value["store"]);
        build(store, *sli.store, metadata);
        auto crs(str(boost::format("http://www.opengis.net/def/crs/EPSG/0/%d")
                     % sli.spatialReference.wkid));
        store["indexCRS"] = crs;
        store["vertexCRS"] = crs;
        build(store["version"], metadata.version);
    }
}

void build(Json::Value &value, const Metadata &metadata)
{
    value = Json::objectValue;
    value["folderPattern"] = asString(metadata.folderPattern);
    value["ArchiveCompressionType"]
        = asString(metadata.archiveCompressionType);
    value["ResourceCompressionType"]
        = asString(metadata.resourceCompressionType);
    build(value["I3SVersion"], metadata.version);
    value["nodeCount"] = Json::UInt64(metadata.nodeCount);
}

void build(Json::Value &value, const MinimumBoundingSphere &mbs)
{
    value = Json::arrayValue;
    value.append(mbs.center(0));
    value.append(mbs.center(1));
    value.append(mbs.center(2));
    value.append(mbs.r);
}

void build(Json::Value &value, const NodeReference &nodeReference)
{
    value = Json::objectValue;
    value["id"] = nodeReference.id;
    build(value["mbs"], nodeReference.mbs);
    value["href"] = nodeReference.href;
    if (!nodeReference.version.empty()) {
        value["version"] = nodeReference.version;
    }
    if (nodeReference.featureCount) {
        value["featureCount"] = nodeReference.featureCount;
    }
}

void build(Json::Value &value, const Resource &resource)
{
    value = Json::objectValue;
    value["href"] = resource.href;
    // TODO: rest
}

void build(Json::Value &value, const LodSelection &lodSelection)
{
    value = Json::objectValue;
    value["metricType"] = asString(lodSelection.metricType);
    if (lodSelection.maxValue) { value["maxValue"] = lodSelection.maxValue; }
    if (lodSelection.avgValue) { value["avgValue"] = lodSelection.avgValue; }
    if (lodSelection.minValue) { value["minValue"] = lodSelection.minValue; }
    if (lodSelection.maxError) { value["maxError"] = lodSelection.maxError; }
}

void build(Json::Value &value, const Node &node
           , bool standardSharedResource = false)
{
    value = Json::objectValue;
    value["id"] = node.id;
    value["level"] = node.level;
    if (!node.version.empty()) { value["version"] = node.version; }
    build(value["mbs"], node.mbs);

    if (node.parentNode) { build(value["parentNode"], *node.parentNode); }

    if (!node.children.empty()) { build(value["children"], node.children); }
    if (!node.neighbors.empty()) { build(value["neighbors"], node.neighbors); }

    if (node.sharedResource) {
        build(value["sharedResource"], *node.sharedResource);
    } else if (standardSharedResource) {
        build(value["sharedResource"], Resource("./shared"));
    }

    if (!node.featureData.empty()) {
        build(value["featureData"], node.featureData);
    }
    if (!node.geometryData.empty()) {
        build(value["geometryData"], node.geometryData);
    }
    if (!node.textureData.empty()) {
        build(value["textureData"], node.textureData);
    }

    if (!node.lodSelection.empty()) {
        build(value["lodSelection"], node.lodSelection);
    }
}

void build(Json::Value &value, const Color &color)
{
    value = Json::arrayValue;

    value.append(color[0]);
    value.append(color[1]);
    value.append(color[2]);
}

void build(Json::Value &value, const Material &material)
{
    value = Json::objectValue;

    value["name"] = material.name;
    value["type"] = asString(material.type);
    if (!material.ref.empty()) { value["$ref"] = material.ref; }

    auto &params(value["params"] = Json::objectValue);
    params["vertexRegions"] = material.params.vertexRegions;
    params["vertexColors"] = material.params.vertexColors;
    params["useVertexColorAlpha"] = material.params.useVertexColorAlpha;
    params["transparency"] = material.params.transparency;
    params["reflectivity"] = material.params.reflectivity;
    params["shininess"] = material.params.shininess;
    build(params["ambient"], material.params.ambient);
    build(params["difuse"], material.params.difuse);
    build(params["specular"], material.params.specular);
    params["renderMode"] = asString(material.params.renderMode);
    params["castShadows"] = material.params.castShadows;
    params["receiveShadows"] = material.params.receiveShadows;
    params["cullFace"] = asString(material.params.cullFace);
}

void build(Json::Value &value, const Image &image)
{
    value = Json::objectValue;

    value["id"] = image.id;
    value["size"] = Json::UInt64(image.size);
    value["pixelInWorldUnits"] = image.pixelInWorldUnits;

    auto &href(value["href"] = Json::arrayValue);
    auto &byteOffset(value["byteOffset"] = Json::arrayValue);
    auto &length(value["length"] = Json::arrayValue);

    for (const auto &version : image.versions) {
        href.append(version.href);
        byteOffset.append(Json::UInt64(version.byteOffset));
        length.append(Json::UInt64(version.length));
    }
}

void build(Json::Value &value, const Texture &texture)
{
    value = Json::objectValue;

    auto &encoding(value["encoding"] = Json::arrayValue);
    for (const auto &e : texture.encoding) { encoding.append(e.mime); }

    auto &wrap(value["wrap"] = Json::arrayValue);
    wrap.append(asString(texture.wrap[0]));
    wrap.append(asString(texture.wrap[1]));

    value["atlas"] = texture.atlas;
    value["uvSet"] = texture.uvSet;
    value["channels"] = texture.channels;

    auto &images(value["images"] = Json::arrayValue);
    for (const auto &image : texture.images) {
        build(images.append({}), image);
    }
}

void build(Json::Value &value, const SharedResource &sr)
{
    if (!sr.materialDefinitions.empty()) {
        auto &md(value["materialDefinitions"] = Json::objectValue);
        for (const auto &materialDefinition : sr.materialDefinitions) {
            build(md[materialDefinition.key], materialDefinition);
        }
    }

    if (!sr.textureDefinitions.empty()) {
        auto &md(value["textureDefinitions"] = Json::objectValue);
        for (const auto &textureDefinition : sr.textureDefinitions) {
            build(md[textureDefinition.key], textureDefinition);
        }
    }
}

void build(Json::Value &value, const GeometryReference &geometry)
{
    value = Json::objectValue;
    value["type"] = asString(GeometryClass::GeometryReference);

    value["$ref"] = geometry.ref;
    build(value["faceRange"], geometry.faceRange);
    value["lodGeometry"] = geometry.lodGeometry;
}

void build(Json::Value &value, GeometryReference::list &geometries)
{
    value = Json::arrayValue;
    for (const auto geometry : geometries) {
        build(value.append({}), geometry);
    }
}

void build(Json::Value &value, const FeatureData::Feature &feature)
{
    value = Json::objectValue;

    value["id"] = feature.id;
    build(value["position"], feature.position);
    build(value["pivotOffset"], feature.pivotOffset);
    build(value["mbb"], feature.mbb);
    value["layer"] = feature.layer;

    build(value["geometries"], feature.geometries);
}

void build(Json::Value &value, const ArrayBufferView &geometry)
{
    value = Json::objectValue;
    value["type"] = asString(GeometryClass::ArrayBufferView);

    value["id"] = geometry.id;
    build(value["transformation"], geometry.transformation);

    auto &params(value["params"]);
    params["type"] = asString(geometry.type);
    params["topology"] = asString(geometry.topology);
    params["material"] = geometry.material;
    params["texture"] = geometry.texture;

    if (!geometry.vertexAttributes.empty()) {
        build(params["vertexAttributes"], geometry.vertexAttributes);
    }
    if (!geometry.faces.empty()) {
        build(params["faces"], geometry.faces);
    }
}

void build(Json::Value &value, const FeatureData::Feature::list &features)
{
    value = Json::arrayValue;
    for (const auto feature : features) {
        build(value.append({}), feature);
    }
}

void build(Json::Value &value, const ArrayBufferView::list &geometries)
{
    value = Json::arrayValue;
    for (const auto geometry : geometries) {
        build(value.append({}), geometry);
    }
}

void build(Json::Value &value, const FeatureData &fd)
{
    value = Json::objectValue;

    build(value["featureData"], fd.featureData);
    build(value["geometryData"], fd.geometryData);
}

// implementation must be here to see other build functions
template <typename T>
void build(Json::Value &value, const std::vector<T> &array)
{
    value = Json::arrayValue;
    for (const auto &item : array) {
        build(value.append({}), item);
    }
}

template <typename T1, typename T2>
void build(Json::Value &value, const std::tuple<T1, T2> &tupple)
{
    build(value, std::get<0>(tupple), std::get<1>(tupple));
}

template <typename T>
void write(std::ostream &out, DataType type, const T &value)
{
#define WRITE_DATATYPE(ENUM, TYPE)                               \
    case DataType::ENUM: bin::write(out, TYPE(value)); return

    switch (type) {
        WRITE_DATATYPE(uint8, std::uint8_t);
        WRITE_DATATYPE(uint16, std::uint16_t);
        WRITE_DATATYPE(uint32, std::uint32_t);
        WRITE_DATATYPE(uint64, std::uint64_t);

        WRITE_DATATYPE(int8, std::int8_t);
        WRITE_DATATYPE(int16, std::int16_t);
        WRITE_DATATYPE(int32, std::int32_t);
        WRITE_DATATYPE(int64, std::int64_t);

        WRITE_DATATYPE(float32, float);
        WRITE_DATATYPE(float64, double);
    }
#undef WRITE_DATATYPE

    LOGTHROW(err1, std::logic_error)
        << "Invalid datatype (int code="
        << static_cast<int>(type) << ").";
    throw;
}

void write(std::ostream &out, const GeometryAttribute &ga
           , const math::Point3 &p)
{
    write(out, ga.valueType, p(0));
    write(out, ga.valueType, p(1));
    write(out, ga.valueType, p(2));
}

void write(std::ostream &out, const GeometryAttribute &ga
           , const math::Point2 &p)
{
    write(out, ga.valueType, p(0));
    write(out, ga.valueType, p(1));
}

class SavePerAttributeArray {
public:
    SavePerAttributeArray(std::ostream &os, const Node &node
                          , const MeshSaver &meshSaver
                          , const GeometrySchema &gs
                          , FeatureData::Feature &feature
                          , ArrayBufferView &arrayBufferView)
        : os_(os), node_(node), meshSaver_(meshSaver), gs_(gs)
        , feature_(feature), arrayBufferView_(arrayBufferView)
        , properties_(meshSaver_.properties())
        , vertexCount_(3 * properties_.faceCount)
    {
        // store array buffer view info
        arrayBufferView_.type = gs.geometryType;
        arrayBufferView_.topology = gs.topology;

        // save header
        for (const auto &header : gs_.header) {
            if (header.property == "vertexCount") {
                // vertex count
                write(os, header.type, vertexCount_);
            } else if (header.property == "featureCount") {
                // feature count: whole mesh is a single feature
                write(os, header.type, 1);
            } else {
                LOGTHROW(err2, std::runtime_error)
                    << "Header element <" << header.property
                    << "> not supported.";
            }
        }

        // save mesh data
        for (const auto &ga : gs_.vertexAttributes) {
            if (ga.key == "position") {
                auto &oga(utility::append
                          (arrayBufferView_.vertexAttributes, ga));
                oga.byteOffset = os.tellp();
                oga.count = vertexCount_;
                saveFaces(ga);
            } else if (ga.key == "uv0") {
                auto &oga(utility::append
                          (arrayBufferView_.vertexAttributes, ga));
                oga.byteOffset = os.tellp();
                oga.count = vertexCount_;
                saveFacesTc(ga);
            } else {
                LOGTHROW(err2, std::runtime_error)
                    << "Geometry attribute <" << ga.key << "> not supported.";
            }
        }

        // save features
        for (const auto &fa : gs_.featureAttributes) {
            if (fa.key == "id") {
                if (fa.valuesPerElement != 1) {
                    LOGTHROW(err1, std::runtime_error)
                        << "Number of feaure.id elements must be 1 not "
                        << fa.valuesPerElement << ".";
                }

                // ID = 0
                write(os, fa.valueType, 0);

            } else if (fa.key == "faceRange") {
                if (fa.valuesPerElement != 2) {
                    LOGTHROW(err1, std::runtime_error)
                        << "Number of feaure.faceRanage  elements must be "
                        "2 not " << fa.valuesPerElement << ".";
                }

                // whole mesh; doc says inclusive range -> faceCount - 1
                write(os, fa.valueType, 0);
                write(os, fa.valueType, (properties_.faceCount - 1));
            } else {
                LOGTHROW(err2, std::runtime_error)
                    << "Feature attribute <" << fa.key << "> not supported.";
            }
        }

        auto &geometry(utility::append(feature_.geometries));
        geometry.ref = "/geometryData/1";
        geometry.faceRange.min = 0;
        geometry.faceRange.max = (properties_.faceCount - 1);
        geometry.lodGeometry = true;
    }

private:
    void saveFaces(const GeometryAttribute &ga) {
        if (ga.valuesPerElement != 3) {
            LOGTHROW(err1, std::runtime_error)
                << "Number of vertex elements must be 3 not "
                << ga.valuesPerElement << ".";
        }

        feature_.mbb = math::Extents3(math::InvalidExtents{});
        for (std::size_t i(0), e(properties_.faceCount); i != e; ++i) {
            const auto &face(meshSaver_.face(i));
            for (const auto &point : face) {
                // update mesh extents
                math::update(feature_.mbb, point);
                // write localized point
                write(os_, ga, math::Point3(point - node_.mbs.center));
            }
        }
    }

    void saveFacesTc(const GeometryAttribute &ga) {
        if (ga.valuesPerElement != 2) {
            LOGTHROW(err1, std::runtime_error)
                << "Number of UV elements must be 2 not "
                << ga.valuesPerElement << ".";
        }

        for (std::size_t i(0), e(properties_.faceCount); i != e; ++i) {
            const auto &face(meshSaver_.faceTc(i));
            for (const auto &point : face) {
                write(os_, ga, point);
            }
        }
    }

    std::ostream &os_;
    const Node &node_;
    const MeshSaver &meshSaver_;
    const GeometrySchema &gs_;
    FeatureData::Feature &feature_;
    ArrayBufferView &arrayBufferView_;

    const MeshSaver::Properties properties_;

    std::size_t vertexCount_;
};

void saveMesh(std::ostream &os, const Node &node
              , const MeshSaver &meshSaver
              , const GeometrySchema &gs
              , FeatureData::Feature &feature
              , ArrayBufferView &arrayBufferView)
{
    switch (gs.topology) {
    case Topology::perAttributeArray:
        SavePerAttributeArray(os, node, meshSaver, gs
                              , feature, arrayBufferView);
        return;

    default:
        LOGTHROW(err2, std::runtime_error)
            << "Unsupported geomety topology " << gs.topology << ".";
    }
    throw;
}

const GeometrySchema& getGeometrySchema(const SceneLayerInfo &sli)
{
    if (!sli.store || !sli.store->defaultGeometrySchema) {
        LOGTHROW(err2, std::runtime_error)
            << "No default geometry schema present.";
    }

    const auto &gs(*sli.store->defaultGeometrySchema);

    if (gs.geometryType != GeometryType::triangles) {
        LOGTHROW(err2, std::runtime_error)
            << "Cannot store nothing else then trianges geometries.";
    }

    if (gs.topology != Topology::perAttributeArray) {
        LOGTHROW(err2, std::runtime_error)
            << "Cannot store nothing else then PerAttributeArray geometries.";
    }

    return gs;
}

} // namespace

struct Writer::Detail {
    Detail(const boost::filesystem::path &path
           , const Metadata &metadata, const SceneLayerInfo &sli
           , bool overwrite)
        : sli(sli), gs(getGeometrySchema(this->sli)), zip(path, overwrite)
        , metadata(metadata), nodeCount(), textureCount()
    {}

    void flush(const SceneLayerInfoCallback &callback);

    utility::zip::Writer::OStream::pointer
    ostream(const fs::path &path, bool raw = false);

    void write(Node &node, SharedResource &sharedResource
               , const MeshSaver &meshSaver
               , const TextureSaver &textureSaver);

    template <typename T>
    void store(const T &value, const fs::path &path, bool raw = false);

    SceneLayerInfo sli;
    const GeometrySchema &gs;
    utility::zip::Writer zip;
    std::mutex mutex;
    Metadata metadata;

    std::atomic<std::size_t> nodeCount;
    std::atomic<std::size_t> textureCount;
};

template <typename T>
void Writer::Detail::store(const T &value, const fs::path &path, bool raw)
{
    Json::Value jValue;
    build(jValue, value);

    std::unique_lock<std::mutex> lock(mutex);
    auto os(ostream( path, raw));
    Json::write(os->get(), jValue, false);
    os->close();
}

void Writer::Detail::flush(const SceneLayerInfoCallback &callback) {
    // update and store scene layer
    if (callback) { callback(sli); }
    store(std::make_tuple(std::cref(sli), std::cref(metadata))
          , detail::constants::SceneLayer);

    // update and save metadata
    metadata.nodeCount = nodeCount;
    store(metadata, detail::constants::MetadataName, true);

        // done
    zip.close();
}

utility::zip::Writer::OStream::pointer
Writer::Detail::ostream(const fs::path &path, bool raw)
{
    const utility::zip::Compression compression
        ((!raw && (metadata.archiveCompressionType
                   != ArchiveCompressionType::store))
         ? utility::zip::Compression::deflate
         : utility::zip::Compression::store);

    if (!raw && (metadata.resourceCompressionType
                 == ResourceCompressionType::gzip))
    {
        // add .gz extension and push gzip compressor at the top of this
        // filter stack
        return zip.ostream(utility::addExtension
                           (path, detail::constants::ext::gz)
                           , compression
                           , [](bio::filtering_ostream &fos) {
                               bio::zlib_params p;
                               p.window_bits |= 16;
                               fos.push(bio::zlib_compressor(p));
                           });
    }

    return zip.ostream(path, compression);
}

std::uint64_t buildId(std::uint64_t id, const math::Size2 &size
                      , unsigned int l, unsigned int al)
{
    std::uint64_t l_al(std::uint64_t(al) << 60);
    std::uint64_t l_l(std::uint64_t(l) << 56);
    std::uint64_t l_w (std::uint64_t(size.width - 1) << 44);
    std::uint64_t l_h(std::uint64_t(size.height - 1) << 32);
    return  l_al + l_l + l_w + l_h + std::uint64_t(id);
}

void Writer::Detail::write(Node &node, SharedResource &sharedResource
                           , const MeshSaver &meshSaver
                           , const TextureSaver &textureSaver)
{
    if (sharedResource.materialDefinitions.empty()) {
        LOGTHROW(err2, std::runtime_error)
            << "No material defined in shared resource.";
    }

    auto &textures(sharedResource.textureDefinitions);

    if (!textures.empty()) {
        LOGTHROW(err2, std::runtime_error)
            << "Multi texture bundle not supported.";
    }

    const auto txId(textureCount++);

    // add texture
    textures.emplace_back(utility::format("tex%d", txId));
    auto &texture(textures.back());

    // setup texture
    texture.atlas = true;
    texture.uvSet = "uv0";
    texture.channels = "rgb";

    auto &image(utility::append(texture.images));
    const auto imageSize(textureSaver.imageSize());
    image.size = imageSize.width;

    // TODO: what are these levels?
    image.id = asString(buildId(txId, imageSize, 0, 0));

    const auto index(node.geometryData.size());

    // write textures
    int txi(0);
    for (const auto &encoding : sli.store->textureEncoding) {
        // node
        const auto href
            (utility::format("textures/0_%d", txi++));
        auto &td(utility::append(node.textureData, "./" + href));
        td.multiTextureBundle = false;

        // texture
        texture.encoding.push_back(encoding);

        auto &imageVersion(utility::append(image.versions, "../" + href));

        const fs::path texturePath
            (detail::constants::Nodes / node.id / (href + ".bin"));

        std::unique_lock<std::mutex> lock(mutex);
        // TODO: do not report DDS as raw (if ever used)
        auto os(ostream(texturePath, true));
        textureSaver.save(os->get(), encoding.mime);

        const auto stat(os->close());
        imageVersion.length = stat.uncompressedSize;
    }

    // write meshes and features
    const auto mhref(utility::format("geometries/%d", index));
    node.geometryData.emplace_back("./" + mhref);
    const fs::path geometryPath
        (detail::constants::Nodes / node.id / (mhref + ".bin"));

    const auto fhref(utility::format("features/%d", index));
    node.featureData.emplace_back("./" + fhref);
    const fs::path featurePath
        (detail::constants::Nodes / node.id / (fhref + ".json"));

    // save mesh to temporary stream
    std::stringstream tmp;

    // feature stuff
    FeatureData featureData;
    {
        auto &fd(utility::append(featureData.featureData, 0));
        fd.position = node.mbs.center;
        fd.layer = "3D model";

        auto &gd(utility::append(featureData.geometryData, 0));

        // only per-attribute array geometry type
        saveMesh(tmp, node, meshSaver, gs, fd, gd);

        // store references to material and textures
        gd.material = ("/materialDefinitions/"
                       + sharedResource.materialDefinitions.back().key);
        gd.texture = "/textureDefinitions/" + texture.key;
    }

    {
        std::unique_lock<std::mutex> lock(mutex);
        auto os(ostream(geometryPath));
        os->get() << tmp.rdbuf();
        os->close();
    }

    // store feature data
    store(featureData, featurePath);
}

Writer::Writer(const boost::filesystem::path &path
               , const Metadata &metadata, const SceneLayerInfo &sli
               , bool overwrite)
    : detail_(std::make_shared<Detail>(path, metadata, sli, overwrite))
{}

void Writer::write(const Node &node, const SharedResource *sharedResource)
{
    const auto dir(detail::constants::Nodes / node.id);
    detail_->store(std::make_tuple(std::cref(node), bool(sharedResource))
                   , dir / detail::constants::NodeIndex);

    if (sharedResource) {
        detail_->store(*sharedResource
                       , (dir / detail::constants::Shared
                          / detail::constants::SharedResource));
    }

    ++detail_->nodeCount;
}

void Writer::write(Node &node, SharedResource &sharedResource
                   , const MeshSaver &meshSaver
                   , const TextureSaver &textureSaver)
{
    return detail_->write(node, sharedResource, meshSaver, textureSaver);
}

void Writer::flush(const SceneLayerInfoCallback &callback)
{
    detail_->flush(callback);
}

} // namespace slpk
<file_sep>/**
 * Copyright (c) 2017 Melown Technologies SE
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * *  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * *  Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <set>

#include <boost/optional.hpp>
#include <boost/utility/in_place_factory.hpp>

#include <opencv2/highgui/highgui.hpp>

#include "utility/buildsys.hpp"
#include "utility/gccversion.hpp"
#include "utility/limits.hpp"
#include "utility/path.hpp"
#include "utility/openmp.hpp"

#include "service/cmdline.hpp"

#include "geometry/meshop.hpp"

#include "imgproc/readimage.cpp"
#include "imgproc/texturing.cpp"

#include "geo/csconvertor.hpp"

#include "slpk/reader.hpp"

namespace po = boost::program_options;
namespace bio = boost::iostreams;
namespace fs = boost::filesystem;
namespace ublas = boost::numeric::ublas;

namespace {

typedef std::vector<std::string> NodeIdList;
typedef std::set<std::string> NodeIdSet;

bool notPicked(const std::string &nodeId
               , const boost::optional<NodeIdSet> &picked)
{
    if (!picked) { return false; }
    return (picked->find(nodeId) == picked->end());
}


class Slpk2Obj : public service::Cmdline
{
public:
    Slpk2Obj()
        : service::Cmdline("slpk2obj", BUILD_TARGET_VERSION)
        , overwrite_(false), srs_(3857)
    {}

private:
    virtual void configuration(po::options_description &cmdline
                               , po::options_description &config
                               , po::positional_options_description &pd)
        UTILITY_OVERRIDE;

    virtual void configure(const po::variables_map &vars)
        UTILITY_OVERRIDE;

    virtual bool help(std::ostream &out, const std::string &what) const
        UTILITY_OVERRIDE;

    virtual int run() UTILITY_OVERRIDE;

    fs::path output_;
    fs::path input_;
    bool overwrite_;
    geo::SrsDefinition srs_;

    boost::optional<NodeIdSet> nodes_;
};

void Slpk2Obj::configuration(po::options_description &cmdline
                             , po::options_description &config
                             , po::positional_options_description &pd)
{
    cmdline.add_options()
        ("output", po::value(&output_)->required()
         , "Path to output converted input.")
        ("input", po::value(&input_)->required()
         , "Path to input SLPK archive.")
        ("overwrite", "Generate output even if output directory exists.")
        ("srs", po::value(&srs_)->default_value(srs_)->required()
         , "Destination SRS of converted meshes.")
        ("nodes", po::value<NodeIdList>()
         , "Limit output to listed nodes.")
        ;

    pd
        .add("input", 1)
        .add("output", 1)
        .add("nodes", -1);

    (void) config;
}

void Slpk2Obj::configure(const po::variables_map &vars)
{
    overwrite_ = vars.count("overwrite");

    if (vars.count("nodes")) {
        const auto &raw(vars["nodes"].as<NodeIdList>());
        nodes_ = boost::in_place(raw.begin(), raw.end());
    }
}

bool Slpk2Obj::help(std::ostream &out, const std::string &what) const
{
    if (what.empty()) {
        out << R"RAW(slpk2obj

    Converts SLPK archive into textured meshes in OBJ format.

usage
    slpk2obj INPUT OUTPUT [OPTIONS]
)RAW";
    }
    return false;
}

void writeMtl(const fs::path &path, const std::string &name)
{
    LOG(info1) << "Writing " << path;
    std::ofstream f(path.string());

    f << "newmtl 0\n"
      << "map_Kd " << name
      << "\n";
}

class DeepCopyCsCovertor {
public:
    DeepCopyCsCovertor(geo::SrsDefinition src, geo::SrsDefinition dst)
        : src_(src), dst_(dst), conv_(src_, dst_)
    {}

    DeepCopyCsCovertor(const DeepCopyCsCovertor &c)
        : src_(c.src_), dst_(c.dst_), conv_(src_, dst_)
    {}

    DeepCopyCsCovertor& operator=(const DeepCopyCsCovertor &c) {
        src_ = c.src_;
        dst_ = c.dst_;
        conv_ = geo::CsConvertor(src_, dst_);
        return *this;
    }

    operator const geo::CsConvertor&() const { return conv_; }

    template <typename T> T operator()(const T &p) const { return conv_(p); }

private:
    geo::SrsDefinition src_;
    geo::SrsDefinition dst_;

    geo::CsConvertor conv_;
};

/** Loads SLPK geometry as a list of submeshes.
 */
class MeasureMesh
    : public slpk::GeometryLoader
    , public slpk::MeshLoader
{
public:
    MeasureMesh(const geo::CsConvertor &conv, math::Extents2 &extents)
        : conv_(conv), extents_(extents)
    {}

    virtual slpk::MeshLoader& next() { return *this; }

    virtual void addVertex(const math::Point3d &v) {
        math::update(extents_, conv_(v));
    }

private:
    virtual void addTexture(const math::Point2d&) {}
    virtual void addFace(const Face&, const FaceTc&, const Face&) {}
    virtual void addNormal(const math::Point3d&) {}
    virtual void addTxRegion(const Region&) {}

    const geo::CsConvertor &conv_;
    math::Extents2 &extents_;
};

math::Extents2 measureMesh(const slpk::Tree &tree
                           , const slpk::Archive &input
                           , DeepCopyCsCovertor conv
                           , const boost::optional<NodeIdSet> &pickedNodes)
{
    // find topLevel
    auto topLevel(std::numeric_limits<int>::max());
    for (const auto item : tree.nodes) {
        if (notPicked(item.first, pickedNodes)) { continue; }
        const auto &node(item.second.node);
        if (node.hasGeometry()) {
            topLevel = std::min(topLevel, node.level);
        }
    }

    // collect nodes for OpenMP
    std::vector<const slpk::TreeNode*> treeNodes;
    for (const auto &item : tree.nodes) {
        if (notPicked(item.first, pickedNodes)) { continue; }
        const auto &node(item.second.node);
        if ((node.level == topLevel) && (node.hasGeometry())) {
            treeNodes.push_back(&item.second);
        }
    }

    math::Extents2 extents(math::InvalidExtents{});

    UTILITY_OMP(parallel for firstprivate(conv), shared(extents, treeNodes))
    for (std::size_t i = 0; i < treeNodes.size(); ++i) {
        const auto &treeNode(*treeNodes[i]);
        const auto &node(treeNode.node);

        // load geometry
        math::Extents2 e(math::InvalidExtents{});
        MeasureMesh loader(conv, e);
        input.loadGeometry(loader, node, treeNode.sharedResource);

        UTILITY_OMP(critical(slpk2obj_measureMesh))
        {
            math::update(extents, e.ll);
            math::update(extents, e.ur);
        }
    }

    return extents;
}

cv::Mat stream2mat(const roarchive::IStream::pointer &txStream)
{
    const auto buf(txStream->read());
    const auto image(cv::imdecode(buf, CV_LOAD_IMAGE_COLOR));

    if (!image.data) {
        LOGTHROW(err1, std::runtime_error)
            << "Cannot decode image from " << txStream->path() << ".";
    }
    return image;
}

inline math::Extents2 remap(const math::Size2 &size
                            , const slpk::Region &region)
{
    return {
        size.width * region.ll(0), size.height * region.ll(1)
        , size.width * region.ur(0), size.height *  region.ur(1)
    };
}

inline math::Point2d& denormalize(const math::Size2f &rsize, math::Point2d &tc)
{
    tc(0) *= rsize.width;
    tc(1) = (1.0 - tc(1)) * rsize.height;
    return tc;
}

void rebuild(slpk::SubMesh &submesh
             , const roarchive::IStream::pointer &txStream
             , const fs::path &texPath)
{
    const auto tx(stream2mat(txStream));

    const math::Size2 txSize(tx.cols, tx.rows);

    auto &mesh(submesh.mesh);

    std::vector<math::Extents2> regions;
    imgproc::tx::Patch::Rect::list uvRects;
    for (auto &region : submesh.regions) {
        regions.push_back(remap(txSize, region));
        uvRects.emplace_back(imgproc::tx::UvPatch(regions.back()));
    }

    // UV patch per region
    imgproc::tx::UvPatch::list uvPatches(submesh.regions.size());

    // create expanded patches
    std::vector<std::uint8_t> seen(mesh.tCoords.size(), false);
    const auto expand([&](imgproc::tx::UvPatch &uvPatch
                          , const math::Size2f &rsize, int index)
    {
        // skip mapped tc
        auto &iseen(seen[index]);
        if (iseen) { return; }

        uvPatch.update(denormalize(rsize, mesh.tCoords[index]));

        iseen = true;
    });

    for (const auto &face : mesh.faces) {
        const auto &region(regions[face.imageId]);
        const auto &rsize(math::size(region));
        auto &uvPatch(uvPatches[face.imageId]);

        expand(uvPatch, rsize, face.ta);
        expand(uvPatch, rsize, face.tb);
        expand(uvPatch, rsize, face.tc);
    }

    std::vector<imgproc::tx::Patch>
        patches(uvPatches.begin(), uvPatches.end());

    const auto size(imgproc::tx::pack(patches.begin(), patches.end()));

    // map texture coordinates to new texture

    seen.assign(mesh.tCoords.size(), false);
    const auto map([&](const imgproc::tx::Patch &patch, int index) -> void
    {
        // skip mapped tc
        auto &iseen(seen[index]);
        if (iseen) { return; }

        auto &tc(mesh.tCoords[index]);
        // map
        patch.map({}, tc);

        // and normalize back again
        tc(0) /= size.width;
        tc(1) /= size.height;
        tc(1) = 1.0 - tc(1);

        iseen = true;
    });

    for (auto &face : mesh.faces) {
        auto &patch(patches[face.imageId]);
        map(patch, face.ta);
        map(patch, face.tb);
        map(patch, face.tc);

        // reset image ID
        face.imageId = 0;
    }

    // generate new texture
    cv::Mat_<cv::Vec3b> otx(size.height, size.width, cv::Vec3b());

    const auto wrap([&](int pos, int origin, int size) -> int
    {
        auto mod(pos % size);
        if (mod < 0) { mod += size; }
        return origin + mod;
    });

    auto iuvRects(uvRects.begin());
    for (const auto &patch : patches) {
        const auto &uvRect(*iuvRects++);
        const auto &dstRect(patch.dst());
        const auto &srcRect(patch.src());
        const math::Point2i diff(uvRect.point - srcRect.point);

        // copy data
        for (int j(0), je(dstRect.size.height); j != je; ++j) {
            const auto jsrc(wrap(j - diff(1), uvRect.point(1)
                                 , uvRect.size.height));

            if ((jsrc < 0) || (jsrc >= txSize.height)) { continue; }

            for (int i(0), ie(dstRect.size.width); i != ie; ++i) {
                const auto isrc(wrap(i - diff(0), uvRect.point(0)
                                     , uvRect.size.width));

                if ((isrc < 0) || (isrc >= txSize.width)) { continue; }

                otx(dstRect.point(1) + j, dstRect.point(0) + i)
                    = tx.at<cv::Vec3b>(jsrc, isrc);
            }
        }
    }

    cv::imwrite(texPath.string(), otx
                , { cv::IMWRITE_JPEG_QUALITY, 85
                    , cv::IMWRITE_PNG_COMPRESSION, 9 });
}

void write(const slpk::Archive &input, fs::path &output
           , const geo::SrsDefinition &srs
           , const boost::optional<NodeIdSet> &pickedNodes)
{
    DeepCopyCsCovertor conv
        (input.sceneLayerInfo().spatialReference.srs(), srs);

    const auto tree(input.loadTree());

    // find extents in destination SRS to localize mesh
    const auto extents(measureMesh(tree, input, conv, pickedNodes));
    const auto center(math::center(extents));

    // collect nodes for OpenMP
    std::vector<const slpk::TreeNode*> treeNodes;
    for (const auto &item : tree.nodes) {
        if (notPicked(item.first, pickedNodes)) { continue; }
        treeNodes.push_back(&item.second);
    }

    UTILITY_OMP(parallel for firstprivate(conv), shared(treeNodes, center))
    for (std::size_t i = 0; i < treeNodes.size(); ++i) {
        const auto &treeNode(*treeNodes[i]);
        const auto &node(treeNode.node);

        LOG(info3) << "Converting <" << node.id << ">.";

        auto geometry(input.loadGeometry(node, treeNode.sharedResource));

        auto igd(node.geometryData.begin());
        int meshIndex(0);
        for (auto &submesh : geometry.submeshes) {
            auto &mesh(submesh.mesh);
            for (auto &v : mesh.vertices) {
                v = conv(v);
                v(0) -= center(0);
                v(1) -= center(1);
            }

            const fs::path path(output / (*igd++).href);
            const auto meshPath(utility::addExtension(path, ".obj"));
            create_directories(meshPath.parent_path());

            auto texture(input.texture(node, meshIndex));

            // detect extension
            const auto mtlPath(utility::addExtension(path, ".mtl"));

            fs::path texPath;

            if (submesh.regions.empty()) {
                // copy texture as-is
                texPath  = utility::addExtension
                    (path, imgproc::imageType
                     (*texture, texture->path()));
                copy(texture, texPath);
            } else {
                // texture atlas, need to repack/unpack texture
                texPath = utility::addExtension(path, ".jpg");
                rebuild(submesh, texture, texPath);
            }

            {
                // save mesh
                utility::ofstreambuf os(meshPath.string());
                os.precision(12);
                saveAsObj(mesh, os, mtlPath.filename().string());
                os.flush();
            }

            writeMtl(mtlPath, texPath.filename().string());
            ++meshIndex;
        }
    }
}

int Slpk2Obj::run()
{
    LOG(info4) << "Opening SLPK archive at " << input_ << ".";
    slpk::Archive archive(input_);
    LOG(info4) << "Generating textured meshes at " << output_ << ".";
    write(archive, output_, srs_, nodes_);
    return EXIT_SUCCESS;
}

} // namespace

int main(int argc, char *argv[])
{
    utility::unlimitedCoredump();
    return Slpk2Obj()(argc, argv);
}
