/******************************************************************************
 *
 * Project:  netCDF read/write Driver
 * Purpose:  GDAL bindings over netCDF library.
 * Author:   Winor Chen <wchen329 at wisc.edu>
 *
 ******************************************************************************
 * Copyright (c) 2019, Winor Chen <wchen329 at wisc.edu>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/
#include "netcdfsgwriterutil.h"
#include "netcdfdataset.h"

namespace nccfdriver
{
SGeometry_Feature::SGeometry_Feature(OGRFeature &ft)
{
    this->hasInteriorRing = false;
    const OGRGeometry *geom = ft.GetGeometryRef();

    if (geom == nullptr)
    {
        throw SGWriter_Exception_EmptyGeometry();
    }

    OGRwkbGeometryType ogwkt = geom->getGeometryType();
    this->type = OGRtoRaw(ogwkt);

    if (this->type == POINT)
    {
        // Set total node count (1)
        this->total_point_count = 1;

        // Also single part geometry (1)
        this->total_part_count = 1;

        // One part per part
        ppart_node_count.push_back(1);
    }

    else if (this->type == MULTIPOINT)
    {
        const auto mp = geom->toMultiPoint();

        // Set total node count
        this->total_point_count = mp->getNumGeometries();

        // The amount of nodes is also the amount of parts
        for (size_t pc = 0; pc < total_point_count; pc++)
        {
            ppart_node_count.push_back(1);
        }

        // total part count == total node count
        this->total_part_count = this->total_point_count;
    }

    else if (this->type == LINE)
    {
        const auto line = geom->toLineString();
        // to do: check for std::bad_cast somewhere?

        // Get node count
        this->total_point_count = line->getNumPoints();

        // Single line: 1 part count == node count
        this->ppart_node_count.push_back(line->getNumPoints());

        // One part
        this->total_part_count = 1;
    }

    else if (this->type == MULTILINE)
    {
        const auto mls = geom->toMultiLineString();
        this->total_point_count = 0;
        this->total_part_count = mls->getNumGeometries();

        // Take each geometry, just add up the corresponding parts

        for (const auto ls : *mls)
        {
            int pt_count = ls->getNumPoints();

            this->ppart_node_count.push_back(pt_count);
            this->total_point_count += pt_count;
        }
    }

    else if (this->type == POLYGON)
    {
        const auto poly = geom->toPolygon();

        this->total_point_count = 0;
        this->total_part_count = 0;

        // Get node count
        // First count exterior ring
        const auto exterior_ring = poly->getExteriorRing();
        if (exterior_ring == nullptr)
        {
            throw SGWriter_Exception_EmptyGeometry();
        }

        size_t outer_ring_ct = exterior_ring->getNumPoints();

        this->total_point_count += outer_ring_ct;
        this->ppart_node_count.push_back(outer_ring_ct);
        this->total_part_count++;

        // Then count all from the interior rings
        // While doing the following
        // Get per part node count (per part RING count)
        // Get part count (in this case it's the amount of RINGS)

        for (int iRingCt = 0; iRingCt < poly->getNumInteriorRings(); iRingCt++)
        {
            this->hasInteriorRing = true;
            const auto iring = poly->getInteriorRing(iRingCt);
            if (iring == nullptr)
            {
                throw SGWriter_Exception_RingOOB();
            }

            this->total_point_count += iring->getNumPoints();
            this->ppart_node_count.push_back(iring->getNumPoints());
            this->total_part_count++;
        }
    }

    else if (this->type == MULTIPOLYGON)
    {
        const auto poMP = geom->toMultiPolygon();

        this->total_point_count = 0;
        this->total_part_count = 0;

        for (const auto poly : *poMP)
        {
            const auto exterior_ring = poly->getExteriorRing();
            if (exterior_ring == nullptr)
            {
                throw SGWriter_Exception_EmptyGeometry();
            }

            size_t outer_ring_ct = exterior_ring->getNumPoints();

            this->total_point_count += outer_ring_ct;
            this->ppart_node_count.push_back(outer_ring_ct);
            this->total_part_count++;
            this->part_at_ind_interior.push_back(false);

            // Then count all from the interior rings
            // While doing the following
            // Get per part node count (per part RING count)
            // Get part count (in this case it's the amount of RINGS)

            for (int iRingCt = 0; iRingCt < poly->getNumInteriorRings();
                 iRingCt++)
            {
                const auto iring = poly->getInteriorRing(iRingCt);
                if (iring == nullptr)
                {
                    throw SGWriter_Exception_RingOOB();
                }

                this->hasInteriorRing = true;
                this->total_point_count += iring->getNumPoints();
                this->ppart_node_count.push_back(iring->getNumPoints());
                this->total_part_count++;
                this->part_at_ind_interior.push_back(true);
            }
        }
    }

    else
    {
        throw SG_Exception_BadFeature();
    }

    this->geometry_ref = geom;
}

inline void WBuffer::addCount(unsigned long long memuse)
{
    this->used_mem += memuse;
}

inline void WBuffer::subCount(unsigned long long memfree)
{
    /* detect underflow cases-
     * better not subtract more than we already counted...
     */
    CPLAssert(this->used_mem >= memfree);

    this->used_mem -= memfree;
}

void ncLayer_SG_Metadata::initializeNewContainer(int containerVID)
{
    this->containerVar_realID = containerVID;

    netCDFVID &ncdf = this->vDataset;
    geom_t geo = this->writableType;

    // Define some virtual dimensions, and some virtual variables
    char container_name[NC_MAX_CHAR + 1] = {0};
    char node_coord_names[NC_MAX_CHAR + 1] = {0};

    // Set default values
    pnc_varID = INVALID_VAR_ID;
    pnc_dimID = INVALID_DIM_ID;
    intring_varID = INVALID_VAR_ID;

    int err_code;
    err_code = nc_inq_varname(ncID, containerVar_realID, container_name);
    NCDF_ERR(err_code);
    if (err_code != NC_NOERR)
    {
        throw SGWriter_Exception_NCInqFailure("new layer", "geometry container",
                                              "var name of");
    }

    this->containerVarName = std::string(container_name);

    // Node Coordinates - Dim
    std::string nodecoord_name =
        containerVarName + "_" + std::string(CF_SG_NODE_COORDINATES);

    node_coordinates_dimID = ncdf.nc_def_vdim(nodecoord_name.c_str(), 1);

    // Node Coordinates - Variable Names
    err_code = nc_get_att_text(ncID, containerVar_realID,
                               CF_SG_NODE_COORDINATES, node_coord_names);
    NCDF_ERR(err_code);
    if (err_code != NC_NOERR)
    {
        throw SGWriter_Exception_NCInqFailure(
            containerVarName.c_str(), CF_SG_NODE_COORDINATES, "varName");
    }

    // Node Count
    if (geo != POINT)
    {
        std::string nodecount_name =
            containerVarName + "_" + std::string(CF_SG_NODE_COUNT);
        node_count_dimID = ncdf.nc_def_vdim(nodecount_name.c_str(), 1);
        node_count_varID = ncdf.nc_def_vvar(nodecount_name.c_str(), NC_INT, 1,
                                            &node_count_dimID);
    }

    // Do the same for part node count, if it exists
    char pnc_name[NC_MAX_CHAR + 1] = {0};
    err_code = nc_get_att_text(ncID, containerVar_realID, CF_SG_PART_NODE_COUNT,
                               pnc_name);

    if (err_code == NC_NOERR)
    {
        pnc_dimID = ncdf.nc_def_vdim(pnc_name, 1);
        pnc_varID = ncdf.nc_def_vvar(pnc_name, NC_INT, 1, &pnc_dimID);

        char ir_name[NC_MAX_CHAR + 1] = {0};
        nc_get_att_text(ncID, containerVar_realID, CF_SG_INTERIOR_RING,
                        ir_name);

        // For interior ring too (for POLYGON and MULTIPOLYGON); there's always
        // an assumption that interior rings really do exist until the very end
        // in which case it's known whether or not that assumption was true or
        // false (if false, this (and PNC attribute for Polygons) will just be
        // deleted)
        if (this->writableType == POLYGON || this->writableType == MULTIPOLYGON)
        {
            intring_varID = ncdf.nc_def_vvar(ir_name, NC_INT, 1, &pnc_dimID);
        }
    }

    // Node coordinates Var Definitions
    int new_varID;
    CPLStringList aosNcoord(CSLTokenizeString2(node_coord_names, " ", 0));

    if (aosNcoord.size() < 2)
        throw SGWriter_Exception();

    // first it's X
    new_varID =
        ncdf.nc_def_vvar(aosNcoord[0], NC_DOUBLE, 1, &node_coordinates_dimID);
    ncdf.nc_put_vatt_text(new_varID, CF_AXIS, CF_SG_X_AXIS);

    this->node_coordinates_varIDs.push_back(new_varID);

    // second it's Y
    new_varID =
        ncdf.nc_def_vvar(aosNcoord[1], NC_DOUBLE, 1, &node_coordinates_dimID);
    ncdf.nc_put_vatt_text(new_varID, CF_AXIS, CF_SG_Y_AXIS);

    this->node_coordinates_varIDs.push_back(new_varID);

    // (and perhaps) third it's Z
    if (aosNcoord.size() > 2)
    {
        new_varID = ncdf.nc_def_vvar(aosNcoord[2], NC_DOUBLE, 1,
                                     &node_coordinates_dimID);
        ncdf.nc_put_vatt_text(new_varID, CF_AXIS, CF_SG_Z_AXIS);

        this->node_coordinates_varIDs.push_back(new_varID);
    }
}

ncLayer_SG_Metadata::ncLayer_SG_Metadata(int &i_ncID, geom_t geo,
                                         netCDFVID &ncdf, OGR_NCScribe &ncs)
    : ncID(i_ncID), vDataset(ncdf), ncb(ncs), writableType(geo)
{
}

const OGRPoint &SGeometry_Feature::getPoint(size_t part_no,
                                            int point_index) const
{
    if (this->type == POINT)
    {
        // Point case: always return the single point regardless of any thing

        const OGRPoint *as_p_ref = geometry_ref->toPoint();
        return *as_p_ref;
    }

    if (this->type == MULTIPOINT)
    {
        const OGRMultiPoint *as_mp_ref = geometry_ref->toMultiPoint();
        int part_ind = static_cast<int>(part_no);
        const OGRPoint *pt = as_mp_ref->getGeometryRef(part_ind);
        return *pt;
    }

    if (this->type == LINE)
    {
        const OGRLineString *as_line_ref = geometry_ref->toLineString();
        as_line_ref->getPoint(point_index, &pt_buffer);
    }

    if (this->type == MULTILINE)
    {
        const OGRMultiLineString *as_mline_ref =
            geometry_ref->toMultiLineString();
        CPLAssert(as_mline_ref);
        int part_ind = static_cast<int>(part_no);
        const OGRLineString *lstring = as_mline_ref->getGeometryRef(part_ind);
        CPLAssert(lstring);
        lstring->getPoint(point_index, &pt_buffer);
    }

    if (this->type == POLYGON)
    {
        const OGRPolygon *as_polygon_ref = geometry_ref->toPolygon();
        int ring_ind = static_cast<int>(part_no);

        if (part_no == 0)
        {
            as_polygon_ref->getExteriorRing()->getPoint(point_index,
                                                        &pt_buffer);
        }

        else
        {
            as_polygon_ref->getInteriorRing(ring_ind - 1)
                ->getPoint(point_index, &pt_buffer);
        }
    }

    if (this->type == MULTIPOLYGON)
    {
        const OGRMultiPolygon *as_mpolygon_ref = geometry_ref->toMultiPolygon();
        int polygon_num = 0;
        int ring_number = 0;
        int pno_itr = static_cast<int>(part_no);

        // Find the right polygon, and the right ring number
        for (int pind = 0; pind < as_mpolygon_ref->getNumGeometries(); pind++)
        {
            const OGRPolygon *itr_poly = as_mpolygon_ref->getGeometryRef(pind);
            if (pno_itr < (itr_poly->getNumInteriorRings() +
                           1))  // + 1 is counting the EXTERIOR ring
            {
                ring_number = static_cast<int>(pno_itr);
                break;
            }

            else
            {
                pno_itr -= (itr_poly->getNumInteriorRings() + 1);
                polygon_num++;
            }
        }

        const OGRPolygon *key_polygon =
            as_mpolygon_ref->getGeometryRef(polygon_num);

        if (ring_number == 0)
        {
            key_polygon->getExteriorRing()->getPoint(point_index, &pt_buffer);
        }

        else
        {
            key_polygon->getInteriorRing(ring_number - 1)
                ->getPoint(point_index, &pt_buffer);
        }
    }

    return pt_buffer;
}

void ncLayer_SG_Metadata::writeSGeometryFeature(SGeometry_Feature &ft)
{
    if (ft.getType() == NONE)
    {
        throw SG_Exception_BadFeature();
    }

    // Write each point from each part in node coordinates
    for (size_t part_no = 0; part_no < ft.getTotalPartCount(); part_no++)
    {
        if (this->writableType == POLYGON || this->writableType == MULTIPOLYGON)
        {
            int interior_ring_fl = 1;

            if (this->writableType == POLYGON)
            {
                interior_ring_fl = part_no == 0 ? 0 : 1;
            }

            else if (this->writableType == MULTIPOLYGON)
            {
                if (ft.IsPartAtIndInteriorRing(part_no))
                {
                    interior_ring_fl = 1;
                }

                else
                {
                    interior_ring_fl = 0;
                }
            }

            if (interior_ring_fl)
            {
                this->interiorRingDetected = true;
            }

            ncb.enqueue_transaction(MTPtr(new OGR_SGFS_NC_Int_Transaction(
                intring_varID, interior_ring_fl)));
        }

        if (this->writableType == POLYGON || this->writableType == MULTILINE ||
            this->writableType == MULTIPOLYGON)
        {
            int pnc_writable =
                static_cast<int>(ft.getPerPartNodeCount()[part_no]);
            ncb.enqueue_transaction(MTPtr(
                new OGR_SGFS_NC_Int_Transaction(pnc_varID, pnc_writable)));
            this->next_write_pos_pnc++;
        }

        for (size_t pt_ind = 0; pt_ind < ft.getPerPartNodeCount()[part_no];
             pt_ind++)
        {
            int pt_ind_int = static_cast<int>(pt_ind);
            const OGRPoint &write_pt = ft.getPoint(part_no, pt_ind_int);

            // Write each node coordinate
            double x = write_pt.getX();
            ncb.enqueue_transaction(MTPtr(new OGR_SGFS_NC_Double_Transaction(
                node_coordinates_varIDs[0], x)));

            double y = write_pt.getY();
            ncb.enqueue_transaction(MTPtr(new OGR_SGFS_NC_Double_Transaction(
                node_coordinates_varIDs[1], y)));

            if (this->node_coordinates_varIDs.size() > 2)
            {
                double z = write_pt.getZ();
                ncb.enqueue_transaction(
                    MTPtr(new OGR_SGFS_NC_Double_Transaction(
                        node_coordinates_varIDs[2], z)));
            }
        }

        this->next_write_pos_node_coord += ft.getPerPartNodeCount()[part_no];
    }

    // Append node counts from the end, if not a POINT
    if (this->writableType != POINT)
    {
        int ncount_add = static_cast<int>(ft.getTotalNodeCount());
        ncb.enqueue_transaction(MTPtr(
            new OGR_SGFS_NC_Int_Transaction(node_count_varID, ncount_add)));
        this->next_write_pos_node_count++;

        // Special case: The "empty" MultiPolygon type
        // MultiPolygon part_node_counts are counted in terms of "rings" not
        // parts contrary to the name so an empty multipolygon with no rings
        // will slip past the regular part_node_count placement In essence this
        // is probably taken as "if there are no rings" then "there are also no
        // points"
        if (ft.getTotalPartCount() == 0 && this->writableType == MULTIPOLYGON &&
            (ft.getType() == POLYGON || ft.getType() == MULTIPOLYGON))
        {
            ncb.enqueue_transaction(
                MTPtr(new OGR_SGFS_NC_Int_Transaction(pnc_varID, 0)));
            this->next_write_pos_pnc++;
        }
    }
}

static std::string sgwe_msg_builder(const char *layer_name,
                                    const char *failure_name,
                                    const char *failure_type,
                                    const char *special_msg)
{
    return std::string("[") + std::string(layer_name) + std::string("] ") +
           std::string(failure_type) + std::string(" ") +
           std::string(failure_name) + std::string(" ") +
           std::string(special_msg);
}

// Exception related definitions
SGWriter_Exception_NCWriteFailure::SGWriter_Exception_NCWriteFailure(
    const char *layer_name, const char *failure_name, const char *failure_type)
    : msg(sgwe_msg_builder(layer_name, failure_name, failure_type,
                           "could not be written to (write failure)."))
{
}

SGWriter_Exception_NCInqFailure::SGWriter_Exception_NCInqFailure(
    const char *layer_name, const char *failure_name, const char *failure_type)
    : msg(sgwe_msg_builder(
          layer_name, failure_name, failure_type,
          "could not be read from (property inquiry failure)."))
{
}

SGWriter_Exception_NCDefFailure::SGWriter_Exception_NCDefFailure(
    const char *layer_name, const char *failure_name, const char *failure_type)
    : msg(sgwe_msg_builder(
          layer_name, failure_name, failure_type,
          "could not be defined in the dataset (definition failure)."))
{
}

// OGR_NCScribe
void OGR_NCScribe::enqueue_transaction(MTPtr transactionAdd)
{
    if (transactionAdd.get() == nullptr)
    {
        return;
    }

    // See if the variable name is already being written to
    if (this->varMaxInds.count(transactionAdd->getVarId()) > 0)
    {
        size_t varWriteLength = this->varMaxInds[transactionAdd->getVarId()];
        varWriteLength++;
        this->varMaxInds[transactionAdd->getVarId()] = varWriteLength;
    }

    else
    {
        // Otherwise, just add it to the list of variable names being written to
        std::pair<int, size_t> entry(transactionAdd->getVarId(), 1);
        this->varMaxInds.insert(entry);
    }

    // Add sizes to memory count
    this->buf.addCount(sizeof(transactionAdd));   // account for pointer
    this->buf.addCount(transactionAdd->count());  // account for pointee

    // Finally push the transaction in
    this->transactionQueue.push(MTPtr(transactionAdd.release()));
}

void OGR_NCScribe::commit_transaction()
{
    wl.startRead();

    NCWMap writerMap;
    std::vector<int> varV;

    MTPtr t;
    t = this->pop();

    while (t.get() != nullptr)
    {
        int varId = t->getVarId();
        size_t writeInd;

        // First, find where to write. If doesn't exist, write to index 0
        if (this->varWriteInds.count(varId) > 0)
        {
            writeInd = this->varWriteInds[varId];
        }

        else
        {
            std::pair<int, size_t> insertable(varId, 0);
            this->varWriteInds.insert(insertable);
            writeInd = 0;
        }

        // Then write
        // Subtract sizes from memory count
        this->buf.subCount(sizeof(t));   // account for pointer
        this->buf.subCount(t->count());  // account for pointee

        try
        {
            // If variable length type, for now, continue using old committing
            // scheme Maybe some future work: optimize this in the similar
            // manner to other types However, CHAR and STRING have huge copying
            // overhead and are more complicated to memory manage correctly
            if (t->getType() == NC_CHAR || t->getType() == NC_STRING ||
                singleDatumMode)
            {
                t->commit(ncvd, writeInd);
            }

            // Other types: Use a more optimized approach
            else
            {
                int wvid = t->getVarId();
                size_t numEntries = this->varMaxInds.at(wvid);
                nc_type ncw = t->getType();
                size_t curWrInd = this->varWriteInds.at(wvid);

                // If entry doesn't exist in map, then add it
                switch (ncw)
                {
                    case NC_BYTE:
                    {
                        NCWMapAllocIfNeeded<signed char>(wvid, writerMap,
                                                         numEntries, varV);
                        auto byte_trn =
                            cpl::down_cast<OGR_SGFS_NC_Byte_Transaction *>(
                                t.get());
                        NCWMapWriteAndCommit<signed char>(
                            wvid, writerMap, curWrInd, numEntries,
                            byte_trn->getData(), this->ncvd);
                        break;
                    }
                    case NC_SHORT:
                    {
                        NCWMapAllocIfNeeded<short>(wvid, writerMap, numEntries,
                                                   varV);
                        auto short_trn =
                            cpl::down_cast<OGR_SGFS_NC_Short_Transaction *>(
                                t.get());
                        NCWMapWriteAndCommit<short>(
                            wvid, writerMap, curWrInd, numEntries,
                            short_trn->getData(), this->ncvd);
                        break;
                    }
                    case NC_INT:
                    {
                        NCWMapAllocIfNeeded<int>(wvid, writerMap, numEntries,
                                                 varV);
                        auto int_trn =
                            cpl::down_cast<OGR_SGFS_NC_Int_Transaction *>(
                                t.get());
                        NCWMapWriteAndCommit<int>(
                            wvid, writerMap, curWrInd, numEntries,
                            int_trn->getData(), this->ncvd);
                        break;
                    }
                    case NC_FLOAT:
                    {
                        NCWMapAllocIfNeeded<float>(wvid, writerMap, numEntries,
                                                   varV);
                        auto float_trn =
                            cpl::down_cast<OGR_SGFS_NC_Float_Transaction *>(
                                t.get());
                        NCWMapWriteAndCommit<float>(
                            wvid, writerMap, curWrInd, numEntries,
                            float_trn->getData(), this->ncvd);
                        break;
                    }
                    case NC_DOUBLE:
                    {
                        NCWMapAllocIfNeeded<double>(wvid, writerMap, numEntries,
                                                    varV);
                        auto double_trn =
                            cpl::down_cast<OGR_SGFS_NC_Double_Transaction *>(
                                t.get());
                        NCWMapWriteAndCommit<double>(
                            wvid, writerMap, curWrInd, numEntries,
                            double_trn->getData(), this->ncvd);
                        break;
                    }
#ifdef NETCDF_HAS_NC4
                    case NC_UINT:
                    {
                        NCWMapAllocIfNeeded<unsigned>(wvid, writerMap,
                                                      numEntries, varV);
                        auto uint_trn =
                            cpl::down_cast<OGR_SGFS_NC_UInt_Transaction *>(
                                t.get());
                        NCWMapWriteAndCommit<unsigned>(
                            wvid, writerMap, curWrInd, numEntries,
                            uint_trn->getData(), this->ncvd);
                        break;
                    }
                    case NC_UINT64:
                    {
                        NCWMapAllocIfNeeded<unsigned long long>(
                            wvid, writerMap, numEntries, varV);
                        auto uint64_trn =
                            cpl::down_cast<OGR_SGFS_NC_UInt64_Transaction *>(
                                t.get());
                        NCWMapWriteAndCommit<unsigned long long>(
                            wvid, writerMap, curWrInd, numEntries,
                            uint64_trn->getData(), this->ncvd);
                        break;
                    }
                    case NC_INT64:
                    {
                        NCWMapAllocIfNeeded<long long>(wvid, writerMap,
                                                       numEntries, varV);
                        auto int64_trn =
                            cpl::down_cast<OGR_SGFS_NC_Int64_Transaction *>(
                                t.get());
                        NCWMapWriteAndCommit<long long>(
                            wvid, writerMap, curWrInd, numEntries,
                            int64_trn->getData(), this->ncvd);
                        break;
                    }
                    case NC_UBYTE:
                    {
                        NCWMapAllocIfNeeded<unsigned char>(wvid, writerMap,
                                                           numEntries, varV);
                        auto ubyte_trn =
                            cpl::down_cast<OGR_SGFS_NC_UByte_Transaction *>(
                                t.get());
                        NCWMapWriteAndCommit<unsigned char>(
                            wvid, writerMap, curWrInd, numEntries,
                            ubyte_trn->getData(), this->ncvd);
                        break;
                    }
                    case NC_USHORT:
                    {
                        NCWMapAllocIfNeeded<unsigned short>(wvid, writerMap,
                                                            numEntries, varV);
                        auto ushort_trn =
                            cpl::down_cast<OGR_SGFS_NC_UShort_Transaction *>(
                                t.get());
                        NCWMapWriteAndCommit<unsigned short>(
                            wvid, writerMap, curWrInd, numEntries,
                            ushort_trn->getData(), this->ncvd);
                        break;
                    }
#endif
                    default:
                    {
                        break;
                    }
                }
            }
        }
        catch (SG_Exception &sge)
        {
            CPLError(CE_Failure, CPLE_FileIO, "%s", sge.get_err_msg());
        }

        // increment index
        this->varWriteInds[varId]++;
        t = this->pop();
    }

    // Clean up afterwards, potential miswrites
    for (size_t vcount = 0; vcount < varV.size(); vcount++)
    {
        int cleanid = varV[vcount];

        if (writerMap.count(cleanid) > 0)
        {
            CPLFree(writerMap.at(cleanid));
            CPLError(CE_Failure, CPLE_FileIO,
                     "Transaction corruption detected. The target variable "
                     "will most likely be missing data.");
        }
    }
}

MTPtr OGR_NCScribe::pop()
{
    // Buffered changes are the earliest, so commit those first
    MTPtr m = this->wl.pop();
    if (m.get() != nullptr)
    {
        // add it to the buffer count
        this->buf.addCount(sizeof(m));
        this->buf.addCount(m->count());
        return m;
    }

    else if (!transactionQueue.empty())
    {
        OGR_SGFS_Transaction *value =
            this->transactionQueue.front()
                .release();  // due to delete copy A.K.A uniqueness of
                             // unique_ptr
        this->transactionQueue.pop();

        return MTPtr(value);
    }
    else
    {
        return MTPtr();
    }
}

void OGR_NCScribe::log_transaction()
{
    if (wl.logIsNull())
        wl.startLog();

    while (!transactionQueue.empty())
    {
        // coverity[var_deref_model]
        wl.push(MTPtr(transactionQueue.front().release()));
        this->transactionQueue.pop();
    }
    this->buf.reset();
}

// WBufferManager
bool WBufferManager::isOverQuota()
{
    unsigned long long sum = 0;
    for (size_t s = 0; s < bufs.size(); s++)
    {
        WBuffer &b = *(bufs[s]);
        sum += b.getUsage();
    }

    return sum > this->buffer_soft_limit;
}

// Transactions
void OGR_SGFS_NC_Char_Transaction::appendToLog(VSILFILE *f)
{
    int vid = OGR_SGFS_Transaction::getVarId();
    int type = NC_CHAR;
    int8_t OP = 0;
    size_t DATA_SIZE = char_rep.length();

    VSIFWriteL(&vid, sizeof(int), 1, f);           // write varID data
    VSIFWriteL(&type, sizeof(int), 1, f);          // write NC type
    VSIFWriteL(&OP, sizeof(int8_t), 1, f);         // write "OP" flag
    VSIFWriteL(&DATA_SIZE, sizeof(size_t), 1, f);  // write length
    VSIFWriteL(char_rep.c_str(), sizeof(char), DATA_SIZE, f);  // write data
}

#ifdef NETCDF_HAS_NC4
void OGR_SGFS_NC_String_Transaction::appendToLog(VSILFILE *f)
{
    int vid = OGR_SGFS_Transaction::getVarId();
    int type = NC_STRING;
    size_t DATA_SIZE = char_rep.length();

    VSIFWriteL(&vid, sizeof(int), 1, f);           // write varID data
    VSIFWriteL(&type, sizeof(int), 1, f);          // write NC type
    VSIFWriteL(&DATA_SIZE, sizeof(size_t), 1, f);  // write length
    VSIFWriteL(char_rep.c_str(), sizeof(char), DATA_SIZE, f);  // write data
}
#endif

void OGR_SGFS_NC_CharA_Transaction::appendToLog(VSILFILE *f)
{
    int vid = OGR_SGFS_Transaction::getVarId();
    int type = NC_CHAR;
    int8_t OP = 1;
    size_t DATA_SIZE = char_rep.length();

    VSIFWriteL(&vid, sizeof(int), 1, f);           // write varID data
    VSIFWriteL(&type, sizeof(int), 1, f);          // write NC type
    VSIFWriteL(&OP, sizeof(int8_t), 1, f);         // write "OP" flag
    VSIFWriteL(&DATA_SIZE, sizeof(size_t), 1, f);  // write length
    VSIFWriteL(char_rep.c_str(), sizeof(char), DATA_SIZE, f);  // write data
}

// WTransactionLog
WTransactionLog::WTransactionLog(const std::string &logName) : wlogName(logName)
{
}

void WTransactionLog::startLog()
{
    log = VSIFOpenL(wlogName.c_str(), "w");
}

void WTransactionLog::startRead()
{
    if (log == nullptr)
        return;

    VSIFCloseL(this->log);
    this->log = VSIFOpenL(wlogName.c_str(), "r");
}

void WTransactionLog::push(MTPtr t)
{
    t->appendToLog(this->log);
}

MTPtr WTransactionLog::pop()
{
    if (log == nullptr)
        return MTPtr(nullptr);

    int varId;
    nc_type ntype;
    size_t itemsread;
    itemsread = VSIFReadL(&varId, sizeof(int), 1, log);
    itemsread &= VSIFReadL(&ntype, sizeof(nc_type), 1, log);

    // If one of the two reads failed, then return nullptr
    if (!itemsread)
        return MTPtr(nullptr);

    // If not, continue on and parse additional fields
    switch (ntype)
    {
        // NC-3 Primitives
        case NC_BYTE:
            return genericLogDataRead<OGR_SGFS_NC_Byte_Transaction,
                                      signed char>(varId, log);
        case NC_SHORT:
            return genericLogDataRead<OGR_SGFS_NC_Short_Transaction, short>(
                varId, log);
        case NC_INT:
            return genericLogDataRead<OGR_SGFS_NC_Int_Transaction, int>(varId,
                                                                        log);
        case NC_FLOAT:
            return genericLogDataRead<OGR_SGFS_NC_Float_Transaction, float>(
                varId, log);
        case NC_DOUBLE:
            return genericLogDataRead<OGR_SGFS_NC_Double_Transaction, double>(
                varId, log);
#ifdef NETCDF_HAS_NC4
        case NC_UBYTE:
            return genericLogDataRead<OGR_SGFS_NC_UByte_Transaction,
                                      unsigned char>(varId, log);
        case NC_USHORT:
            return genericLogDataRead<OGR_SGFS_NC_UShort_Transaction,
                                      unsigned short>(varId, log);
        case NC_UINT:
            return genericLogDataRead<OGR_SGFS_NC_UInt_Transaction,
                                      unsigned int>(varId, log);
        case NC_INT64:
            return genericLogDataRead<OGR_SGFS_NC_Int64_Transaction, long long>(
                varId, log);
        case NC_UINT64:
            return genericLogDataRead<OGR_SGFS_NC_UInt64_Transaction,
                                      unsigned long long>(varId, log);
#endif
        case NC_CHAR:
        {
            size_t readcheck;  // 0 means at least one read 0 bytes

            // Check what type of OP is requested
            int8_t op = 0;
            readcheck = VSIFReadL(&op, sizeof(int8_t), 1, log);
            if (!readcheck)
                return MTPtr();  // read failure

            size_t strsize;

            // get how much data to read
            readcheck = VSIFReadL(&strsize, sizeof(size_t), 1, log);
            if (!readcheck)
                return MTPtr();  // read failure

            std::string data;
            data.resize(strsize);

            // read that data and return it
            readcheck = VSIFReadL(&data[0], sizeof(char), strsize, log);
            if (!readcheck)
                return MTPtr();  // read failure

            // case: its a standard CHAR op
            if (!op)
            {
                return MTPtr(new OGR_SGFS_NC_Char_Transaction(
                    varId, &data[0]));  // data is copied so okay!
            }

            // case: its a CHARA op, additional processing
            else
            {
                return MTPtr(
                    new OGR_SGFS_NC_CharA_Transaction(varId, &data[0]));
            }
        }

#ifdef NETCDF_HAS_NC4
        case NC_STRING:
        {
            size_t readcheck;  // 0 means at least one read 0 bytes

            size_t strsize;

            // get how much data to read
            readcheck = VSIFReadL(&strsize, sizeof(size_t), 1, log);

            if (!readcheck)
                return MTPtr();  // read failure

            std::string data;
            data.resize(strsize);

            // read that data and return it
            readcheck = VSIFReadL(&data[0], sizeof(char), strsize, log);

            if (!readcheck)
                return MTPtr();  // read failure

            return MTPtr(new OGR_SGFS_NC_String_Transaction(
                varId, &data[0]));  // data is copied so okay!
        }
#endif

        default:
            // Unsupported type
            return MTPtr();
    }
}

WTransactionLog::~WTransactionLog()
{
    if (log != nullptr)
    {
        VSIFCloseL(log);
        VSIUnlink(this->wlogName.c_str());
    }
}

// Helper function definitions
int write_Geometry_Container(
    int ncID, const std::string &name, geom_t geometry_type,
    const std::vector<std::string> &node_coordinate_names)
{

    int write_var_id;
    int err_code;

    // Define geometry container variable
    err_code =
        nc_def_var(ncID, name.c_str(), NC_FLOAT, 0, nullptr, &write_var_id);
    // todo: exception handling of err_code
    NCDF_ERR(err_code);
    if (err_code != NC_NOERR)
    {
        throw SGWriter_Exception_NCDefFailure(name.c_str(),
                                              "geometry_container", "variable");
    }

    /* Geometry Type Attribute
     * -
     */

    // Next, go on to add attributes needed for each geometry type
    std::string geometry_str =
        (geometry_type == POINT || geometry_type == MULTIPOINT)
            ? CF_SG_TYPE_POINT
        : (geometry_type == LINE || geometry_type == MULTILINE)
            ? CF_SG_TYPE_LINE
        : (geometry_type == POLYGON || geometry_type == MULTIPOLYGON)
            ? CF_SG_TYPE_POLY
            : "";  // obviously an error condition...

    if (geometry_str == "")
    {
        throw SG_Exception_BadFeature();
    }

    // Add the geometry type attribute
    err_code = nc_put_att_text(ncID, write_var_id, CF_SG_GEOMETRY_TYPE,
                               geometry_str.size(), geometry_str.c_str());
    NCDF_ERR(err_code);
    if (err_code != NC_NOERR)
    {
        throw SGWriter_Exception_NCWriteFailure(
            name.c_str(), CF_SG_GEOMETRY_TYPE,
            "attribute in geometry_container");
    }

    /* Node Coordinates Attribute
     * -
     */
    std::string ncoords_atr_str = "";

    for (size_t itr = 0; itr < node_coordinate_names.size(); itr++)
    {
        ncoords_atr_str += node_coordinate_names[itr];
        if (itr < node_coordinate_names.size() - 1)
        {
            ncoords_atr_str += " ";
        }
    }

    err_code = nc_put_att_text(ncID, write_var_id, CF_SG_NODE_COORDINATES,
                               ncoords_atr_str.size(), ncoords_atr_str.c_str());

    NCDF_ERR(err_code);
    if (err_code != NC_NOERR)
    {
        throw SGWriter_Exception_NCWriteFailure(
            name.c_str(), CF_SG_NODE_COORDINATES,
            "attribute in geometry_container");
    }
    // The previous two attributes are all that are required from POINT

    /* Node_Count Attribute
     * (not needed for POINT)
     */
    if (geometry_type != POINT)
    {
        std::string nodecount_atr_str = name + "_node_count";

        err_code = nc_put_att_text(ncID, write_var_id, CF_SG_NODE_COUNT,
                                   nodecount_atr_str.size(),
                                   nodecount_atr_str.c_str());
        NCDF_ERR(err_code);
        if (err_code != NC_NOERR)
        {
            throw SGWriter_Exception_NCWriteFailure(
                name.c_str(), CF_SG_NODE_COUNT,
                "attribute in geometry_container");
        }
    }

    /* Part_Node_Count Attribute
     * (only needed for MULTILINE, MULTIPOLYGON, and (potentially) POLYGON)
     */
    if (geometry_type == MULTILINE || geometry_type == MULTIPOLYGON ||
        geometry_type == POLYGON)
    {
        std::string pnc_atr_str = name + "_part_node_count";

        err_code = nc_put_att_text(ncID, write_var_id, CF_SG_PART_NODE_COUNT,
                                   pnc_atr_str.size(), pnc_atr_str.c_str());

        NCDF_ERR(err_code);
        if (err_code != NC_NOERR)
        {
            throw SGWriter_Exception_NCWriteFailure(
                name.c_str(), CF_SG_PART_NODE_COUNT,
                "attribute in geometry_container");
        }
    }

    /* Interior Ring Attribute
     * (only needed potentially for MULTIPOLYGON and POLYGON)
     */

    if (geometry_type == MULTIPOLYGON || geometry_type == POLYGON)
    {
        std::string ir_atr_str = name + "_interior_ring";

        err_code = nc_put_att_text(ncID, write_var_id, CF_SG_INTERIOR_RING,
                                   ir_atr_str.size(), ir_atr_str.c_str());
        NCDF_ERR(err_code);
        if (err_code != NC_NOERR)
        {
            throw nccfdriver::SGWriter_Exception_NCWriteFailure(
                name.c_str(), CF_SG_INTERIOR_RING,
                "attribute in geometry_container");
        }
    }

    return write_var_id;
}
}  // namespace nccfdriver
