/*
===============================================================================

  FILE:  field_point14.hpp

  CONTENTS:

  PROGRAMMERS:

    martin.isenburg@rapidlasso.com  -  http://rapidlasso.com

  COPYRIGHT:

    (c) 2007-2014, martin isenburg, rapidlasso - tools to catch reality

    This is free software; you can redistribute and/or modify it under the
    terms of the Apache Public License 2.0 published by the Apache Software
    Foundation. See the COPYING file for more information.

    This software is distributed WITHOUT ANY WARRANTY and without even the
    implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

  CHANGE HISTORY:

===============================================================================
*/

#include <cmath>
#include <cstdint>
#include "../las.hpp"

namespace lazperf
{
namespace detail
{

namespace {
    const uint8_t number_return_map_6ctx[16][16] = {
        {  0,  1,  2,  3,  4,  5,  3,  4,  4,  5,  5,  5,  5,  5,  5,  5 },
        {  1,  0,  1,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3,  3 },
        {  2,  1,  2,  4,  4,  4,  4,  4,  4,  4,  4,  3,  3,  3,  3,  3 },
        {  3,  3,  4,  5,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4 },
        {  4,  3,  4,  4,  5,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4 },
        {  5,  3,  4,  4,  4,  5,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4 },
        {  3,  3,  4,  4,  4,  4,  5,  4,  4,  4,  4,  4,  4,  4,  4,  4 },
        {  4,  3,  4,  4,  4,  4,  4,  5,  4,  4,  4,  4,  4,  4,  4,  4 },
        {  4,  3,  4,  4,  4,  4,  4,  4,  5,  4,  4,  4,  4,  4,  4,  4 },
        {  5,  3,  4,  4,  4,  4,  4,  4,  4,  5,  4,  4,  4,  4,  4,  4 },
        {  5,  3,  4,  4,  4,  4,  4,  4,  4,  4,  5,  4,  4,  4,  4,  4 },
        {  5,  3,  3,  4,  4,  4,  4,  4,  4,  4,  4,  5,  5,  4,  4,  4 },
        {  5,  3,  3,  4,  4,  4,  4,  4,  4,  4,  4,  5,  5,  5,  4,  4 },
        {  5,  3,  3,  4,  4,  4,  4,  4,  4,  4,  4,  4,  5,  5,  5,  4 },
        {  5,  3,  3,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  5,  5,  5 },
        {  5,  3,  3,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  4,  5,  5 }
    };
    const uint8_t number_return_level_8ctx[16][16] =
    {
        {  0,  1,  2,  3,  4,  5,  6,  7,  7,  7,  7,  7,  7,  7,  7,  7 },
        {  1,  0,  1,  2,  3,  4,  5,  6,  7,  7,  7,  7,  7,  7,  7,  7 },
        {  2,  1,  0,  1,  2,  3,  4,  5,  6,  7,  7,  7,  7,  7,  7,  7 },
        {  3,  2,  1,  0,  1,  2,  3,  4,  5,  6,  7,  7,  7,  7,  7,  7 },
        {  4,  3,  2,  1,  0,  1,  2,  3,  4,  5,  6,  7,  7,  7,  7,  7 },
        {  5,  4,  3,  2,  1,  0,  1,  2,  3,  4,  5,  6,  7,  7,  7,  7 },
        {  6,  5,  4,  3,  2,  1,  0,  1,  2,  3,  4,  5,  6,  7,  7,  7 },
        {  7,  6,  5,  4,  3,  2,  1,  0,  1,  2,  3,  4,  5,  6,  7,  7 },
        {  7,  7,  6,  5,  4,  3,  2,  1,  0,  1,  2,  3,  4,  5,  6,  7 },
        {  7,  7,  7,  6,  5,  4,  3,  2,  1,  0,  1,  2,  3,  4,  5,  6 },
        {  7,  7,  7,  7,  6,  5,  4,  3,  2,  1,  0,  1,  2,  3,  4,  5 },
        {  7,  7,  7,  7,  7,  6,  5,  4,  3,  2,  1,  0,  1,  2,  3,  4 },
        {  7,  7,  7,  7,  7,  7,  6,  5,  4,  3,  2,  1,  0,  1,  2,  3 },
        {  7,  7,  7,  7,  7,  7,  7,  6,  5,  4,  3,  2,  1,  0,  1,  2 },
        {  7,  7,  7,  7,  7,  7,  7,  7,  6,  5,  4,  3,  2,  1,  0,  1 },
        {  7,  7,  7,  7,  7,  7,  7,  7,  7,  6,  5,  4,  3,  2,  1,  0 }
    };

    const int GpstimeMulti = 500;
    const int GpstimeMultiMinus = -10;
    const int GpstimeMultiCodeFull = 511;
} // unnamed namespace

Point14Base::Point14Base() : last_channel_(-1)
{
    //ABELL - For debug.
    chan_ctxs_[0].ctx_num_ = 0;
    chan_ctxs_[1].ctx_num_ = 1;
    chan_ctxs_[2].ctx_num_ = 2;
    chan_ctxs_[3].ctx_num_ = 3;
}

// COMPRESSOR

void Point14Compressor::writeSizes()
{
    xy_enc_.done();
    z_enc_.done();
    class_enc_.done();
    flags_enc_.done();
    intensity_enc_.done();
    scan_angle_enc_.done();
    user_data_enc_.done();
    point_source_id_enc_.done();
    gpstime_enc_.done();

    stream_ << xy_enc_.num_encoded();
    stream_ << z_enc_.num_encoded();
    stream_ << class_enc_.num_encoded();
    stream_ << flags_enc_.num_encoded();
    stream_ << intensity_enc_.num_encoded();
    stream_ << scan_angle_enc_.num_encoded();
    stream_ << user_data_enc_.num_encoded();
    stream_ << point_source_id_enc_.num_encoded();
    stream_ << gpstime_enc_.num_encoded();
}

void Point14Compressor::writeData()
{
#ifdef PRINT_DEBUG
    std::cerr << "XY        : " <<
        utils::sum(xy_enc_.encoded_bytes(), xy_enc_.num_encoded()) << "\n";
    std::cerr << "Z         : " <<
        utils::sum(z_enc_.encoded_bytes(), z_enc_.num_encoded()) << "\n";
    std::cerr << "Class     : " <<
        utils::sum(class_enc_.encoded_bytes(), class_enc_.num_encoded()) << "\n";
    std::cerr << "Flags     : " <<
        utils::sum(flags_enc_.encoded_bytes(), flags_enc_.num_encoded()) << "\n";
    std::cerr << "Intensity : " <<
        utils::sum(intensity_enc_.encoded_bytes(), intensity_enc_.num_encoded()) << "\n";
    std::cerr << "Scan angle: " <<
        utils::sum(scan_angle_enc_.encoded_bytes(), scan_angle_enc_.num_encoded()) << "\n";
    std::cerr << "User data : " <<
        utils::sum(user_data_enc_.encoded_bytes(), user_data_enc_.num_encoded()) << "\n";
    std::cerr << "Point src : " <<
        utils::sum(point_source_id_enc_.encoded_bytes(),
            point_source_id_enc_.num_encoded()) << "\n";
    std::cerr << "GPS time  : " <<
        utils::sum(gpstime_enc_.encoded_bytes(), gpstime_enc_.num_encoded()) << "\n";
#endif

    stream_.putBytes(xy_enc_.encoded_bytes(), xy_enc_.num_encoded());
    stream_.putBytes(z_enc_.encoded_bytes(), z_enc_.num_encoded());
    if (class_enc_.num_encoded())
        stream_.putBytes(class_enc_.encoded_bytes(), class_enc_.num_encoded());
    if (flags_enc_.num_encoded())
        stream_.putBytes(flags_enc_.encoded_bytes(), flags_enc_.num_encoded());
    if (intensity_enc_.num_encoded())
        stream_.putBytes(intensity_enc_.encoded_bytes(), intensity_enc_.num_encoded());
    if (scan_angle_enc_.num_encoded())
        stream_.putBytes(scan_angle_enc_.encoded_bytes(), scan_angle_enc_.num_encoded());
    if (user_data_enc_.num_encoded())
        stream_.putBytes(user_data_enc_.encoded_bytes(), user_data_enc_.num_encoded());
    if (point_source_id_enc_.num_encoded())
        stream_.putBytes(point_source_id_enc_.encoded_bytes(), point_source_id_enc_.num_encoded());
    if (gpstime_enc_.num_encoded())
        stream_.putBytes(gpstime_enc_.encoded_bytes(), gpstime_enc_.num_encoded());
}

const char *Point14Compressor::compress(const char *buf, int& scArg)
{
    const las::point14 point(buf);

    // We choose the model based on the scanner channel. In the laszip code, this
    // is called the context.
    int sc = point.scannerChannel();
    assert (sc >= 0 && sc <= 3);

    // don't have the first data for this channel yet, just push it to our have
    // last stuff and move on
    if (last_channel_ == -1)
    {
        ChannelCtx& c = chan_ctxs_[sc];
        stream_.putBytes((const unsigned char*)buf, sizeof(las::point14));
        c.last_ = point;
        c.have_last_ = true;
        c.last_gpstime_[0] = point.gpsTime();
        last_channel_ = sc;

        for (auto& z : c.last_z_)
            z = point.z();
        for (auto& last_intensity : c.last_intensity_)
            last_intensity = point.intensity();
        //ABELL - See note at end of function.
        scArg = sc;

        return buf + sizeof(las::point14);
    }

    // prev is the context for the previous point.
    ChannelCtx& prev = chan_ctxs_[last_channel_];

    // There are 8 contexts for the change bits based on the return number,
    // number of returns and a GPS time change. Calculate that context number.
    // Called 'lpr' in the laszip code.
    int change_stream =
        (prev.last_.returnNum() == 1) |                                 // bit 0
        ((prev.last_.returnNum() >= prev.last_.numReturns()) << 1) |    // bit 1
        (prev.gps_time_change_ << 2);                                   // bit 2

    // c is the context for this point.
    ChannelCtx& c = chan_ctxs_[sc];
    // old is the same as c unless we've switched channels and don't have a previous
    // for this channel, in which case we use the last channel's context.
    // In other words, we prefer the last point in the same channel as this point
    // if possible when computing the changes below.

    //ABELL - If we don't use the same context when actually storing the diffs (rather than
    //  the diff flags), it seems we could be mis-encoding data.
    ChannelCtx& old = c.have_last_ ? c : prev;

    //ABELL - The following comparison yields false if one of the values is NaN and
    //  true if both of the values are NaN. This probably wasn't what was intended but
    //  I don't think it really matters as long as the decoding is in sync. Care should
    //  be taken if a change is made.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wfloat-equal"
    bool gps_time_change = (point.gpsTime() != old.last_.gpsTime());
#pragma GCC diagnostic pop
    bool point_source_change = (point.pointSourceID() != old.last_.pointSourceID());
    bool scan_angle_change = (point.scanAngle() != old.last_.scanAngle());
    uint32_t last_n = old.last_.numReturns();
    uint32_t last_r = old.last_.returnNum();
    uint32_t n = point.numReturns();
    uint32_t r = point.returnNum();
    bool returnNumIncrements = (r == ((last_r + 1) % 16));
    bool returnNumDecrements = (r == ((last_r + 15) % 16));
    bool returnMiscChange = ((r != last_r) && !returnNumIncrements && !returnNumDecrements);

    int changed_values =
        ((returnNumIncrements || returnMiscChange) << 0) |
        ((returnNumDecrements || returnMiscChange) << 1) |
        ((n != last_n) << 2) |
        (scan_angle_change << 3) |
        (gps_time_change << 4) |
        (point_source_change << 5) |
        ((sc != last_channel_) << 6);

    xy_enc_.encodeSymbol(prev.changed_values_model_[change_stream], changed_values);

    if (sc > last_channel_)
        xy_enc_.encodeSymbol(prev.scanner_channel_model_, sc - last_channel_ - 1);
    else if (sc < last_channel_)
        xy_enc_.encodeSymbol(prev.scanner_channel_model_, sc - last_channel_ - 1 + 4);

    // If we haven't initialized the current context, do so.
    if (!c.have_last_)
    {
        c.have_last_ = true;
        c.last_ = prev.last_;
        for (auto& z : c.last_z_)
            z = prev.last_.z();
        for (auto& intensity : c.last_intensity_)
            intensity = prev.last_.intensity();
        c.last_gpstime_[0] = prev.last_.gpsTime();
    }

    if (n != last_n)
        xy_enc_.encodeSymbol(c.nr_model_[last_n], n);

    if (returnMiscChange)
    {
        if (gps_time_change)
            xy_enc_.encodeSymbol(c.rn_model_[last_r], r);
        else
        {
            //ABELL - I believe this is broken for the case when diff == -15, as
            //  it results in a symbol of -1, which is coerced into an unsigned value.
            int diff = r - last_r;
            if (diff > 1)
                xy_enc_.encodeSymbol(c.rn_gps_same_model_, diff - 2);
            else
                xy_enc_.encodeSymbol(c.rn_gps_same_model_, diff - 2 + 16);
        }
    }

    // X and Y
    {
        uint32_t ctx = (number_return_map_6ctx[n][r] << 1) | (uint32_t) gps_time_change;
        int32_t median = c.last_x_diff_median5_[ctx].get();
        int32_t diff = point.x() - c.last_.x();
        c.dx_compr_.compress(xy_enc_, median, diff, point.numReturns() == 1);
        c.last_x_diff_median5_[ctx].add(diff);

        // Max of 20, low bit cleared to allow for numReturns change.
        uint32_t kbits = (std::min)(c.dx_compr_.getK(), 20U) & ~1;
        median = c.last_y_diff_median5_[ctx].get();
        diff = point.y() - c.last_.y();
        c.dy_compr_.compress(xy_enc_, median, diff, kbits | (n == 1));
        c.last_y_diff_median5_[ctx].add(diff);
    }

    // Z
    {
        uint32_t kbits = (c.dx_compr_.getK() + c.dy_compr_.getK()) / 2;
        kbits = (std::min)(kbits, 18U) & ~1;
        uint32_t ctx = number_return_level_8ctx[n][r];
        c.z_compr_.compress(z_enc_, c.last_z_[ctx], point.z(), kbits | (n == 1));
        c.last_z_[ctx] = point.z();
    }

    // Classification
    {
        int32_t ctx =
            // This bit is supposed to represent an only return.
            ((r == 1) && (r >= n)) |
            // Class 0 - 31, shifted.
            ((c.last_.classification() & 0x1F) << 1);

        if (point.classification() != c.last_.classification())
            class_enc_.makeValid();
        class_enc_.encodeSymbol(c.class_model_[ctx], point.classification());
    }

    // Flags
    {
        // This nonsense is to pack the flags, since we've already written the scanner
        // channel that's normally part of the flag byte.
        uint32_t flags =
            point.classFlags() |
            (point.scanDirFlag() << 4) |
            (point.eofFlag() << 5);
        uint32_t last_flags =
            c.last_.classFlags() |
            (c.last_.scanDirFlag() << 4) |
            (c.last_.eofFlag() << 5);

        if (flags != last_flags)
            flags_enc_.makeValid();;
        flags_enc_.encodeSymbol(c.flag_model_[last_flags], flags);
    }

    // Intensity
    {
        int32_t ctx =
            (int32_t)gps_time_change |
            ((r >= n) << 1) |
            ((r == 1) << 2);

        if (point.intensity() != c.last_.intensity())
            intensity_enc_.makeValid();
        c.intensity_compr_.compress(intensity_enc_,
                c.last_intensity_[ctx], point.intensity(), ctx >> 1);
        c.last_intensity_[ctx] = point.intensity();
    }

    // Scan angle
    {
        if (point.scanAngle() != c.last_.scanAngle())
        {
            scan_angle_enc_.makeValid();
            c.scan_angle_compr_.compress(scan_angle_enc_, c.last_.scanAngle(),
                    point.scanAngle(), gps_time_change);
        }
    }

    // User data
    {
        int32_t ctx = c.last_.userData() / 4;
        if (point.userData() != c.last_.userData())
            user_data_enc_.makeValid();
        user_data_enc_.encodeSymbol(c.user_data_model_[ctx], point.userData());
    }

    // Point Source ID
    {
        if (point_source_change)
        {
            point_source_id_enc_.makeValid();
            c.point_source_id_compr_.compress(point_source_id_enc_,
                    c.last_.pointSourceID(), point.pointSourceID(), 0);
        }
    }

    // GPS Time
    if (gps_time_change)
        encodeGpsTime(point, c);

//ABELL - There is a bug in laszip where the context does not get set unless there is a *change*
//  in context. This means that if the context is non-zero and never changes, it will
//  always be zero. This test maintains that behavior.
    if (sc != last_channel_)
        scArg = sc;
    last_channel_ = sc;
    c.gps_time_change_ = gps_time_change;
    c.last_ = point;
    return buf + sizeof(las::point14);
}

void Point14Compressor::encodeGpsTime(const las::point14& point, ChannelCtx& c)
{
    auto findSeq = [&c](double gpstime, int start, int32_t& diff)
    {
        for (int i = start; i < 4; ++i)
        {
            int testseq = (c.last_gps_seq_ + i) & 0x3;
            int64_t diff64 = utils::d2i(gpstime) - utils::d2i(c.last_gpstime_[testseq]);
            diff = (int32_t)diff64;
            if (diff64 == diff)
                return i;
        }
        return -1;
    };

    gpstime_enc_.makeValid();

    loop:
    if (c.last_gpstime_diff_[c.last_gps_seq_] == 0)
    {
        int32_t diff;  // Difference between current time and last time in the sequence,
        // as 32 bit int.
        int idx = findSeq(point.gpsTime(), 0, diff);

        if (idx == 0)
        {
            gpstime_enc_.encodeSymbol(c.gpstime_0diff_model_, 0);
            c.gpstime_compr_.compress(gpstime_enc_, 0, diff, 0);
            c.last_gpstime_diff_[c.last_gps_seq_] = diff;
            c.multi_extreme_counter_[c.last_gps_seq_] = 0;
        }
        else if (idx > 0)
        {
            gpstime_enc_.encodeSymbol(c.gpstime_0diff_model_, idx + 1);
            c.last_gps_seq_ = (c.last_gps_seq_ + idx) & 3;
            goto loop;
        }
        else
        {
            gpstime_enc_.encodeSymbol(c.gpstime_0diff_model_, 1);
            c.gpstime_compr_.compress(gpstime_enc_,
                    utils::d2u(c.last_gpstime_[c.last_gps_seq_]) >> 32,
                    (int32_t)(utils::d2u(point.gpsTime()) >> 32), 8);
            gpstime_enc_.writeInt((uint32_t)(utils::d2u(point.gpsTime())));
            c.last_gps_seq_ = c.next_gps_seq_ = (c.next_gps_seq_ + 1) & 3;
            c.last_gpstime_diff_[c.last_gps_seq_] = 0;
            c.multi_extreme_counter_[c.last_gps_seq_] = 0;
        }
        c.last_gpstime_[c.last_gps_seq_] = point.gpsTime();
    }
    else  // Last diff nonzero.
    {
        int64_t diff64 = utils::d2i(point.gpsTime()) - utils::d2i(c.last_gpstime_[c.last_gps_seq_]);
        int32_t diff = (int32_t)diff64;

        // 32 bit difference.
        if (diff64 == diff)
        {
            // Compute multiplier between current and last int difference.
            int32_t multi =
                (int32_t)std::round(diff / (float)c.last_gpstime_diff_[c.last_gps_seq_]);
            if (multi > 0 && multi < GpstimeMulti)
            {
                int tag;
                if (multi == 1)
                    tag = 1;  // The case for regular spaced pulses.
                else if (multi > 1 && multi < 10)
                    tag = 2;
                else
                    tag = 3;
                gpstime_enc_.encodeSymbol(c.gpstime_multi_model_, multi);
                c.gpstime_compr_.compress(gpstime_enc_,
                        multi * c.last_gpstime_diff_[c.last_gps_seq_], diff, tag);
                if (tag == 1)
                    c.multi_extreme_counter_[c.last_gps_seq_] = 0;
            }
            else if (multi >= GpstimeMulti)
            {
                gpstime_enc_.encodeSymbol(c.gpstime_multi_model_, GpstimeMulti);
                c.gpstime_compr_.compress(gpstime_enc_,
                        GpstimeMulti * c.last_gpstime_diff_[c.last_gps_seq_], diff, 4);
                c.multi_extreme_counter_[c.last_gps_seq_]++;
                if (c.multi_extreme_counter_[c.last_gps_seq_] > 3)
                {
                    c.multi_extreme_counter_[c.last_gps_seq_] = 0;
                    c.last_gpstime_diff_[c.last_gps_seq_] = diff;
                }
            }
            else if (multi < 0 && multi > GpstimeMultiMinus)
            {
                gpstime_enc_.encodeSymbol(c.gpstime_multi_model_, GpstimeMulti - multi);
                c.gpstime_compr_.compress(gpstime_enc_,
                        multi * c.last_gpstime_diff_[c.last_gps_seq_], diff, 5);
            }
            else if (multi <= GpstimeMultiMinus)
            {
                gpstime_enc_.encodeSymbol(c.gpstime_multi_model_,
                        GpstimeMulti - GpstimeMultiMinus);
                c.gpstime_compr_.compress(gpstime_enc_,
                        GpstimeMultiMinus * c.last_gpstime_diff_[c.last_gps_seq_], diff, 6);
                c.multi_extreme_counter_[c.last_gps_seq_]++;
                if (c.multi_extreme_counter_[c.last_gps_seq_] > 3)
                {
                    c.multi_extreme_counter_[c.last_gps_seq_] = 0;
                    c.last_gpstime_diff_[c.last_gps_seq_] = diff;
                }
            }
            else if (multi == 0)
            {
                gpstime_enc_.encodeSymbol(c.gpstime_multi_model_, 0);
                c.gpstime_compr_.compress(gpstime_enc_, 0, diff, 7);
                c.multi_extreme_counter_[c.last_gps_seq_]++;
                if (c.multi_extreme_counter_[c.last_gps_seq_] > 3)
                {
                    c.multi_extreme_counter_[c.last_gps_seq_] = 0;
                    c.last_gpstime_diff_[c.last_gps_seq_] = diff;
                }
            }
        }
        // Large difference
        else
        {
            int32_t diff;
            int idx = findSeq(point.gpsTime(), 1, diff);
            if (idx > 0)
            {
                gpstime_enc_.encodeSymbol(c.gpstime_multi_model_,
                        GpstimeMultiCodeFull + idx);
                c.last_gps_seq_ = (c.last_gps_seq_ + idx) & 3;
                goto loop;
            }
            gpstime_enc_.encodeSymbol(c.gpstime_multi_model_, GpstimeMultiCodeFull);
            c.gpstime_compr_.compress(gpstime_enc_,
                    int32_t(utils::d2u(c.last_gpstime_[c.last_gps_seq_]) >> 32),
                    int32_t(utils::d2u(point.gpsTime()) >> 32), 8);
            gpstime_enc_.writeInt((uint32_t)(utils::d2u(point.gpsTime())));
            c.next_gps_seq_ = c.last_gps_seq_ = (c.next_gps_seq_ + 1) & 3;
            c.last_gpstime_diff_[c.last_gps_seq_] = 0;
            c.multi_extreme_counter_[c.last_gps_seq_] = 0;
        }
        c.last_gpstime_[c.last_gps_seq_] = point.gpsTime();
    }
}

// DECOMPRESSOR

void Point14Decompressor::dumpSums()
{
    if (sumChange.count() == 0)
        return;

    std::cout << "Change   : " << sumChange.value() << "\n";
    std::cout << "Return   : " << sumReturn.value() << "\n";
    std::cout << "X        : " << sumX.value() << "\n";
    std::cout << "Y        : " << sumY.value() << "\n";
    std::cout << "Z        : " << sumZ.value() << "\n";
    std::cout << "Class    : " << sumClass.value() << "\n";
    std::cout << "Flags    : " << sumFlags.value() << "\n";
    std::cout << "Intensity: " << sumIntensity.value() << "\n";
    std::cout << "Scan angl: " << sumScanAngle.value() << "\n";
    std::cout << "User data: " << sumUserData.value() << "\n";
    std::cout << "Point src: " << sumPointSourceId.value() << "\n";
    std::cout << "GPS time : " << sumGpsTime.value() << "\n";
}

void Point14Decompressor::readSizes()
{
    uint32_t xy_cnt;
    uint32_t z_cnt;
    uint32_t class_cnt;
    uint32_t flags_cnt;
    uint32_t intensity_cnt;
    uint32_t scan_angle_cnt;
    uint32_t user_data_cnt;
    uint32_t point_source_id_cnt;
    uint32_t gpstime_cnt;

    stream_ >> xy_cnt;
    stream_ >> z_cnt;
    stream_ >> class_cnt;
    stream_ >> flags_cnt;
    stream_ >> intensity_cnt;
    stream_ >> scan_angle_cnt;
    stream_ >> user_data_cnt;
    stream_ >> point_source_id_cnt;
    stream_ >> gpstime_cnt;

    sizes_.push_back(xy_cnt);
    sizes_.push_back(z_cnt);
    sizes_.push_back(class_cnt);
    sizes_.push_back(flags_cnt);
    sizes_.push_back(intensity_cnt);
    sizes_.push_back(scan_angle_cnt);
    sizes_.push_back(user_data_cnt);
    sizes_.push_back(point_source_id_cnt);
    sizes_.push_back(gpstime_cnt);
}

void Point14Decompressor::readData()
{
    auto cnt = sizes_.begin();

    // Copy data and read the init bytes.
    xy_dec_.initStream(stream_, *cnt++);
    z_dec_.initStream(stream_, *cnt++);
    class_dec_.initStream(stream_, *cnt++);
    flags_dec_.initStream(stream_, *cnt++);
    intensity_dec_.initStream(stream_, *cnt++);
    scan_angle_dec_.initStream(stream_, *cnt++);
    user_data_dec_.initStream(stream_, *cnt++);
    point_source_id_dec_.initStream(stream_, *cnt++);
    gpstime_dec_.initStream(stream_, *cnt++);
    sizes_.clear();
}

char *Point14Decompressor::decompress(char *buf, int& scArg)
{
    // This is weird, but the first point, stored raw, is written *before* the point
    // count.
    // First point.
    if (last_channel_ == -1)
    {
        stream_.getBytes((unsigned char *)buf, sizeof(las::point14));
        las::point14 point(buf);

        scArg = point.scannerChannel();
        ChannelCtx& c = chan_ctxs_[scArg];
        c.last_ = point;
        c.have_last_ = true;
        c.last_gpstime_[0] = point.gpsTime();
        last_channel_ = scArg;

        for (auto& z : c.last_z_)
            z = point.z();
        for (auto& last_intensity : c.last_intensity_)
            last_intensity = point.intensity();

        return buf + sizeof(las::point14);
    }
    ChannelCtx& prev = chan_ctxs_[last_channel_];

    // There are 8 streams for the change bits based on the return number,
    // number of returns and a GPS time change. Calculate that stream number.
    // Called 'lpr' in the laszip code.
    int change_stream =
        (prev.last_.returnNum() == 1) |                                 // bit 0
        ((prev.last_.returnNum() >= prev.last_.numReturns()) << 1) |    // bit 1
        (prev.gps_time_change_ << 2);                                   // bit 2

    int32_t changed_values = xy_dec_.decodeSymbol(prev.changed_values_model_[change_stream]);
    LAZDEBUG(sumChange.add(changed_values));

    bool scanner_channel_changed = (changed_values >> 6) & 1;
    bool point_source_changed = (changed_values >> 5) & 1;
    bool gps_time_changed = (changed_values >> 4) & 1;
    bool scan_angle_changed = (changed_values >> 3) & 1;
    bool nr_changes = (changed_values >> 2) & 1;
    bool rn_minus = (changed_values >> 1) & 1;
    bool rn_plus = (changed_values >> 0) & 1;
    bool rn_increments = rn_plus && !rn_minus;
    bool rn_decrements = rn_minus && !rn_plus;
    bool rn_misc_change = rn_plus && rn_minus;

    int sc = prev.last_.scannerChannel();
    if (scanner_channel_changed)
    {
        uint32_t diff = xy_dec_.decodeSymbol(prev.scanner_channel_model_);
        sc = (sc + diff + 1) % 4;
        last_channel_ = sc;
        scArg = sc;
    }

    ChannelCtx& c = chan_ctxs_[sc];
    if (!c.have_last_)
    {
        c.have_last_ = true;
        c.last_ = prev.last_;
        for (auto& z : c.last_z_)
            z = prev.last_.z();
        for (auto& intensity : c.last_intensity_)
            intensity = prev.last_.intensity();
        c.last_gpstime_[0] = prev.last_.gpsTime();
    }
    c.last_.setScannerChannel(sc);

    uint32_t n = c.last_.numReturns();
    uint32_t r = c.last_.returnNum();
    if (nr_changes)
        n = xy_dec_.decodeSymbol(c.nr_model_[c.last_.numReturns()]);
    c.last_.setNumReturns(n);

    if (rn_increments)
        r = (r + 1) % 16;
    else if (rn_decrements)
        r = ((r + 15) % 16);
    else if (rn_misc_change)
    {
        if (gps_time_changed)
            r = xy_dec_.decodeSymbol(c.rn_model_[r]);
        else
            r = (r + xy_dec_.decodeSymbol(c.rn_gps_same_model_) + 2) % 16;
    }
    c.last_.setReturnNum(r);
    LAZDEBUG(sumReturn.add(n));
    LAZDEBUG(sumReturn.add(r));

    uint32_t ctx = (number_return_map_6ctx[n][r] << 1) | (uint32_t) gps_time_changed;
    // X
    {
        int32_t median = c.last_x_diff_median5_[ctx].get();
        int32_t diff = c.dx_decomp_.decompress(xy_dec_, median, n == 1);
        c.last_.setX(c.last_.x() + diff);
        c.last_x_diff_median5_[ctx].add(diff);
        LAZDEBUG(sumX.add(c.last_.x()));
    }

    // Y
    {
        uint32_t kbits = (std::min)(c.dx_decomp_.getK(), 20U) & ~1;
        int32_t median = c.last_y_diff_median5_[ctx].get();
        int32_t diff = c.dy_decomp_.decompress(xy_dec_, median, (n == 1) | kbits);
        c.last_.setY(c.last_.y() + diff);
        c.last_y_diff_median5_[ctx].add(diff);
        LAZDEBUG(sumY.add(c.last_.y()));
    }

    // Z
    if (z_dec_.valid())
    {
        uint32_t kbits = (c.dx_decomp_.getK() + c.dy_decomp_.getK()) / 2;
        kbits = (std::min)(kbits, 18U) & ~1;
        uint32_t ctx = number_return_level_8ctx[n][r];
        int32_t z = c.z_decomp_.decompress(z_dec_, c.last_z_[ctx], (n == 1) | kbits);
        c.last_.setZ(z);
        c.last_z_[ctx] = z;
    }
    LAZDEBUG(sumZ.add(c.last_.z()));

    // Classification
    if (class_dec_.valid())
    {
        int32_t ctx = ((r == 1 && r >= n) | ((c.last_.classification() & 0x1F) << 1));
        c.last_.setClassification(class_dec_.decodeSymbol(c.class_model_[ctx]));
    }
    LAZDEBUG(sumClass.add(c.last_.classification()));

    // Flags
    auto mergeFlags = [](const las::point14& p)
    {
        uint32_t flags = p.classFlags() | (p.scanDirFlag() << 4) | (p.eofFlag() << 5);
        return flags;
    };
    if (flags_dec_.valid())
    {
        uint32_t flags = flags_dec_.decodeSymbol(c.flag_model_[mergeFlags(c.last_)]);
        c.last_.setEofFlag((flags >> 5) & 1);
        c.last_.setScanDirFlag((flags >> 4) & 1);
        c.last_.setClassFlags(flags & 0x0F);
        LAZDEBUG(sumFlags.add(flags));
    }
    else
        LAZDEBUG(sumFlags.add(mergeFlags(c.last_)));


    // Intensity
    if (intensity_dec_.valid())
    {
        int32_t ctx = (int32_t) gps_time_changed | ((r >= n) << 1) | ((r == 1) << 2);

        uint16_t intensity = c.intensity_decomp_.decompress(intensity_dec_,
                c.last_intensity_[ctx], ctx >> 1);
        c.last_intensity_[ctx] = intensity;
        c.last_.setIntensity(intensity);
    }
    LAZDEBUG(sumIntensity.add(c.last_.intensity()));

    // Scan angle
    if (scan_angle_changed)
    {
        c.last_.setScanAngle(c.scan_angle_decomp_.decompress(scan_angle_dec_,
            c.last_.scanAngle(), gps_time_changed));
    }
    LAZDEBUG(sumScanAngle.add(c.last_.scanAngle()));

    // User data
    if (user_data_dec_.valid())
    {
        int32_t ctx = c.last_.userData() / 4;
        c.last_.setUserData(user_data_dec_.decodeSymbol(c.user_data_model_[ctx]));
    }
    LAZDEBUG(sumUserData.add(c.last_.userData()));

    // Point source ID
    if (point_source_changed)
    {
        c.last_.setPointSourceID(c.point_source_id_decomp_.decompress(
            point_source_id_dec_, c.last_.pointSourceID(), 0));
    }
    LAZDEBUG(sumPointSourceId.add(c.last_.pointSourceID()));

    if (gps_time_changed)
        decodeGpsTime(c);
    LAZDEBUG(sumGpsTime.add(c.last_.gpsTime()));
    c.gps_time_change_ = gps_time_changed;
    las::point14 *point = reinterpret_cast<las::point14 *>(buf);
    *point = c.last_;
    return buf + sizeof(las::point14);
}

void Point14Decompressor::decodeGpsTime(ChannelCtx& c)
{
    loop:
    if (c.last_gpstime_diff_[c.last_gps_seq_] == 0)
    {
        int32_t multi = gpstime_dec_.decodeSymbol(c.gpstime_0diff_model_);
        if (multi == 0)
        {
            int32_t sym = c.gpstime_decomp_.decompress(gpstime_dec_, 0, 0);

            c.last_gpstime_diff_[c.last_gps_seq_] = sym;
            int64_t lasttime = utils::d2i(c.last_gpstime_[c.last_gps_seq_]) + sym;
            c.last_gpstime_[c.last_gps_seq_] = utils::i2d(lasttime);
            c.multi_extreme_counter_[c.last_gps_seq_] = 0;
        }
        else if (multi == 1)
        {
            c.next_gps_seq_ = (c.next_gps_seq_ + 1) & 3;
            double lasttime = c.last_gpstime_[c.last_gps_seq_];
            int32_t sym = c.gpstime_decomp_.decompress(gpstime_dec_,
                    (int32_t)(utils::d2u(lasttime) >> 32), 8);
            c.last_gpstime_[c.next_gps_seq_] =
                utils::u2d(((uint64_t)sym << 32) | gpstime_dec_.readInt());
            c.last_gps_seq_ = c.next_gps_seq_;
            c.last_gpstime_diff_[c.last_gps_seq_] = 0;
            c.multi_extreme_counter_[c.last_gps_seq_] = 0;
        }
        else
        {
            c.last_gps_seq_ = (c.last_gps_seq_ + multi - 1) & 3;
            goto loop;
        }
    }
    else
    {
        int32_t multi = gpstime_dec_.decodeSymbol(c.gpstime_multi_model_);
        int32_t gpstime_diff;
        if (multi == 1)
        {
            int32_t sym = c.gpstime_decomp_.decompress(gpstime_dec_,
                    c.last_gpstime_diff_[c.last_gps_seq_], 1);
            c.last_gpstime_[c.last_gps_seq_] =
                utils::i2d((int64_t)sym + utils::d2u(c.last_gpstime_[c.last_gps_seq_]));
            c.multi_extreme_counter_[c.last_gps_seq_] = 0;
        }
        else if (multi < GpstimeMultiCodeFull)
        {
            if (multi == 0)
            {
                gpstime_diff = c.gpstime_decomp_.decompress(gpstime_dec_, 0, 7);
                c.multi_extreme_counter_[c.last_gps_seq_]++;
                if (c.multi_extreme_counter_[c.last_gps_seq_] > 3)
                {
                    c.multi_extreme_counter_[c.last_gps_seq_] = 0;
                    c.last_gpstime_diff_[c.last_gps_seq_] = gpstime_diff;
                }
            }
            else if (multi < GpstimeMulti)
            {
                int tag;
                if (multi < 10)
                    tag = 2;
                else
                    tag = 3;
                gpstime_diff = c.gpstime_decomp_.decompress(gpstime_dec_,
                        multi * c.last_gpstime_diff_[c.last_gps_seq_], tag);
            }
            else if (multi == GpstimeMulti)
            {
                gpstime_diff = c.gpstime_decomp_.decompress(gpstime_dec_,
                        GpstimeMulti * c.last_gpstime_diff_[c.last_gps_seq_], 4);
                c.multi_extreme_counter_[c.last_gps_seq_]++;
                if (c.multi_extreme_counter_[c.last_gps_seq_] > 3)
                {
                    c.multi_extreme_counter_[c.last_gps_seq_] = 0;
                    c.last_gpstime_diff_[c.last_gps_seq_] = gpstime_diff;
                }
            }
            else
            {
                multi = GpstimeMulti - multi;
                if (multi > GpstimeMultiMinus)
                {
                    gpstime_diff = c.gpstime_decomp_.decompress(gpstime_dec_,
                            multi * c.last_gpstime_diff_[c.last_gps_seq_], 5);
                }
                else
                {
                    gpstime_diff = c.gpstime_decomp_.decompress(gpstime_dec_,
                            GpstimeMultiMinus * c.last_gpstime_diff_[c.last_gps_seq_], 6);
                    c.multi_extreme_counter_[c.last_gps_seq_]++;
                    if (c.multi_extreme_counter_[c.last_gps_seq_] > 3)
                    {
                        c.multi_extreme_counter_[c.last_gps_seq_] = 0;
                        c.last_gpstime_diff_[c.last_gps_seq_] = gpstime_diff;
                    }
                }
            }
            int64_t lasttime = utils::d2i(c.last_gpstime_[c.last_gps_seq_]);
            c.last_gpstime_[c.last_gps_seq_] = utils::i2d(lasttime + gpstime_diff);
        }
        else if (multi == GpstimeMultiCodeFull)
        {
            c.next_gps_seq_ = (c.next_gps_seq_ + 1) & 3;

            uint64_t lasttime = utils::d2u(c.last_gpstime_[c.last_gps_seq_]);
            int32_t sym = c.gpstime_decomp_.decompress(gpstime_dec_,
                    (int32_t)(lasttime >> 32), 8);
            c.last_gpstime_[c.next_gps_seq_] =
                utils::u2d(((uint64_t)(sym) << 32) | gpstime_dec_.readInt());
            c.last_gps_seq_ = c.next_gps_seq_;
            c.last_gpstime_diff_[c.last_gps_seq_] = 0;
            c.multi_extreme_counter_[c.last_gps_seq_] = 0;
        }
        else if (multi >= GpstimeMultiCodeFull)
        {
            c.last_gps_seq_ = (c.last_gps_seq_ + multi - GpstimeMultiCodeFull) & 3;
            goto loop;
        }
    }
    c.last_.setGpsTime(c.last_gpstime_[c.last_gps_seq_]);
}

} // namespace detail
} // namespace lazperf
