// custom.cpp --
// This is part of Metakit, see http://www.equi4.com/metakit.html

/** @file
 * Implementation of many custom viewer classes
 */

#include "header.h"

#include "custom.h"
#include "format.h"
/////////////////////////////////////////////////////////////////////////////

class c4_CustomHandler : public c4_Handler
{
    c4_CustomSeq *_seq;

public:
    c4_CustomHandler(const c4_Property &prop_, c4_CustomSeq *seq_);
    ~c4_CustomHandler() override;

    int ItemSize(int index_) override;
    const void *Get(int index_, int &length_) override;
    void Set(int index_, const c4_Bytes &buf_) override;

    void Insert(int index_, const c4_Bytes &buf_, int count_) override;
    void Remove(int index_, int count_) override;
};

/////////////////////////////////////////////////////////////////////////////

c4_CustomHandler::c4_CustomHandler(const c4_Property &prop_, c4_CustomSeq *seq_)
    : c4_Handler(prop_)
    , _seq(seq_)
{
    d4_assert(_seq != 0);
}

c4_CustomHandler::~c4_CustomHandler() = default;

int c4_CustomHandler::ItemSize(int index_)
{
    c4_Bytes &buf = _seq->Buffer();

    int colnum = _seq->PropIndex(Property().GetId());
    d4_assert(colnum >= 0);

    if (!_seq->DoGet(index_, colnum, buf)) {
        return 0;
    }

    return buf.Size();
}

const void *c4_CustomHandler::Get(int index_, int &length_)
{
    c4_Bytes &buf = _seq->Buffer();

    int colnum = _seq->PropIndex(Property().GetId());
    d4_assert(colnum >= 0);

    if (!_seq->DoGet(index_, colnum, buf)) {
        ClearBytes(buf);
    }

    length_ = buf.Size();
    return buf.Contents();
}

void c4_CustomHandler::Set(int index_, const c4_Bytes &buf_)
{
    int colnum = _seq->PropIndex(Property().GetId());
    d4_assert(colnum >= 0);

    _seq->DoSet(index_, colnum, buf_);
}

void c4_CustomHandler::Insert(int, const c4_Bytes &, int)
{
    d4_assert(0); //! not yet
}

void c4_CustomHandler::Remove(int, int)
{
    d4_assert(0); //! not yet
}

c4_Handler *c4_CustomSeq::CreateHandler(const c4_Property &prop_)
{
    return d4_new c4_CustomHandler(prop_, this);
}

/////////////////////////////////////////////////////////////////////////////

c4_CustomSeq::c4_CustomSeq(c4_CustomViewer *viewer_) : c4_HandlerSeq(nullptr)
    , _viewer
        (viewer_)
    , _inited(false)
{
    d4_assert(_viewer != 0);

    // set up handlers to match a template obtained from the viewer
    c4_View v = viewer_->GetTemplate();

    for (int i = 0; i < v.NumProperties(); ++i) {
        PropIndex(v.NthProperty(i));
    }

    _inited = true;
}

c4_CustomSeq::~c4_CustomSeq()
{
    delete _viewer;
}

int c4_CustomSeq::NumRows() const
{
    return _inited ? _viewer->GetSize() : 0;
}

bool c4_CustomSeq::RestrictSearch(c4_Cursor cursor_, int &pos_, int &count_)
{
    if (count_ > 0) {
        int n;
        int o = _viewer->Lookup(cursor_, n);
        // a -1 result means: "don't know, please scan all"
        if (o < 0) {
            return count_ > 0;
        }

        if (n > 0) {
            if (pos_ < o) {
                count_ -= o - pos_;
                pos_ = o;
            }

            if (pos_ + count_ > o + n) {
                count_ = o + n - pos_;
            }

            if (count_ > 0) {
                return true;
            }
        }
    }

    count_ = 0;
    return false;
}

void c4_CustomSeq::InsertAt(int p_, c4_Cursor c_, int n_)
{
    _viewer->InsertRows(p_, c_, n_);
}

void c4_CustomSeq::RemoveAt(int p_, int n_)
{
    _viewer->RemoveRows(p_, n_);
}

void c4_CustomSeq::Move(int, int)
{
    d4_assert(false); //! not yet
}

bool c4_CustomSeq::DoGet(int row_, int col_, c4_Bytes &buf_) const
{
    d4_assert(_inited);

    return _viewer->GetItem(row_, col_, buf_);
}

void c4_CustomSeq::DoSet(int row_, int col_, const c4_Bytes &buf_)
{
    d4_assert(_inited);

    d4_dbgdef(const bool f = ) _viewer->SetItem(row_, col_, buf_);
    d4_assert(f);
}

/////////////////////////////////////////////////////////////////////////////

/** @class c4_CustomViewer
 *
 *  Abstract base class for definition of custom views.
 *
 *  A custom view is a view which can be accessed like any other view, using
 *  row and property operations, but which is fully managed by a customized
 *  "viewer" class.  The viewer will eventually handle all requests for the
 *  view, such as defining its structure and size, as well as providing the
 *  actual data values when requested.
 *
 *  Custom views cannot propagate changes.
 *
 *  To implement a custom view, you must derive your viewer from this base
 *  class and define each of the virtual members.  Then create a new object
 *  of this type on the heap and pass it to the c4_View constructor.  Your
 *  viewer will automatically be destroyed when the last reference to its
 *  view goes away.  See the DBF2MK sample code for an example of a viewer.
 */

c4_CustomViewer::~c4_CustomViewer() = default;

/// Locate a row in this view, try to use native searches
int c4_CustomViewer::Lookup(c4_Cursor, int &count_)
{
    count_ = GetSize();
    return 0; // not implemented, return entire view range
}

/// Store one data item, supplied as a generic data value
bool c4_CustomViewer::SetItem(int, int, const c4_Bytes &)
{
    return false; // default is not modifiable
}

/// Insert one or more copies of a row (if possible)
bool c4_CustomViewer::InsertRows(int, c4_Cursor, int)
{
    return false; // default is not modifiable
}

/// Remove one or more rows (this is not always possible)
bool c4_CustomViewer::RemoveRows(int, int)
{
    return false; // default is not modifiable
}

/////////////////////////////////////////////////////////////////////////////

class c4_SliceViewer : public c4_CustomViewer
{
    c4_View _parent;
    int _first, _limit, _step;

public:
    c4_SliceViewer(c4_Sequence &seq_, int first_, int limit_, int step_);
    ~c4_SliceViewer() override;

    c4_View GetTemplate() override;
    int GetSize() override;
    bool GetItem(int row_, int col_, c4_Bytes &buf_) override;
    bool SetItem(int row_, int col_, const c4_Bytes &buf_) override;
    bool InsertRows(int pos_, c4_Cursor value_, int count_ = 1) override;
    bool RemoveRows(int pos_, int count_ = 1) override;
};

c4_SliceViewer::c4_SliceViewer(c4_Sequence &seq_, int first_, int limit_, int
                               step_) : _parent(&seq_)
    , _first(first_)
    , _limit(limit_)
    , _step(step_)
{
    d4_assert(_step != 0);
}

c4_SliceViewer::~c4_SliceViewer() = default;

c4_View c4_SliceViewer::GetTemplate()
{
    return _parent.Clone(); // could probably return _parent just as well
}

int c4_SliceViewer::GetSize()
{
    int n = _limit >= 0 ? _limit : _parent.GetSize();
    if (n < _first) {
        n = _first;
    }

    int k = _step < 0 ? -_step : _step;
    return (n - _first + k - 1) / k;
}

bool c4_SliceViewer::GetItem(int row_, int col_, c4_Bytes &buf_)
{
    row_ = _first + _step * (_step > 0 ? row_ : row_ - GetSize() + 1);

    return _parent.GetItem(row_, col_, buf_);
}

bool c4_SliceViewer::SetItem(int row_, int col_, const c4_Bytes &buf_)
{
    row_ = _first + _step * (_step > 0 ? row_ : row_ - GetSize() + 1);

    _parent.SetItem(row_, col_, buf_);
    return true;
}

bool c4_SliceViewer::InsertRows(int pos_, c4_Cursor value_, int count_)
{
    if (_step != 1) {
        return false;
    }

    pos_ = _first + _step * (_step > 0 ? pos_ : pos_ - GetSize() + 1);
    if (_limit >= 0) {
        _limit += count_;
    }

    _parent.InsertAt(pos_, *value_, count_);
    return true;
}

bool c4_SliceViewer::RemoveRows(int pos_, int count_)
{
    if (_step != 1) {
        return false;
    }

    pos_ = _first + _step * (_step > 0 ? pos_ : pos_ - GetSize() + 1);
    if (_limit >= 0) {
        _limit -= count_;
    }

    _parent.RemoveAt(pos_, count_);
    return true;
}

c4_CustomViewer *f4_CustSlice(c4_Sequence &seq_, int first_, int limit_, int
                              step_)
{
    return d4_new c4_SliceViewer(seq_, first_, limit_, step_);
}

/////////////////////////////////////////////////////////////////////////////

class c4_ProductViewer : public c4_CustomViewer
{
    c4_View _parent, _argView, _template;

public:
    c4_ProductViewer(c4_Sequence &seq_, const c4_View &view_);
    ~c4_ProductViewer() override;

    c4_View GetTemplate() override;
    int GetSize() override;
    bool GetItem(int row_, int col_, c4_Bytes &buf_) override;
};

c4_ProductViewer::c4_ProductViewer(c4_Sequence &seq_, const c4_View &view_)
    : _parent(&seq_)
    , _argView(view_)
    , _template(_parent.Clone())
{
    for (int i = 0; i < _argView.NumProperties(); ++i) {
        _template.AddProperty(_argView.NthProperty(i));
    }
}

c4_ProductViewer::~c4_ProductViewer() = default;

c4_View c4_ProductViewer::GetTemplate()
{
    return _template;
}

int c4_ProductViewer::GetSize()
{
    return _parent.GetSize() * _argView.GetSize();
}

bool c4_ProductViewer::GetItem(int row_, int col_, c4_Bytes &buf_)
{
    c4_View v = _parent;

    if (col_ < v.NumProperties()) {
        row_ /= _argView.GetSize();
    } else {
        v = _argView;
        row_ %= _argView.GetSize();
        col_ = v.FindProperty(_template.NthProperty(col_).GetId());

        d4_assert(col_ >= 0);
    }

    return v.GetItem(row_, col_, buf_);
}

c4_CustomViewer *f4_CustProduct(c4_Sequence &seq_, const c4_View &view_)
{
    return d4_new c4_ProductViewer(seq_, view_);
}

/////////////////////////////////////////////////////////////////////////////

class c4_RemapWithViewer : public c4_CustomViewer
{
    c4_View _parent, _argView;

public:
    c4_RemapWithViewer(c4_Sequence &seq_, const c4_View &view_);
    ~c4_RemapWithViewer() override;

    c4_View GetTemplate() override;
    int GetSize() override;
    bool GetItem(int row_, int col_, c4_Bytes &buf_) override;
    bool SetItem(int row_, int col_, const c4_Bytes &buf_) override;
};

c4_RemapWithViewer::c4_RemapWithViewer(c4_Sequence &seq_, const c4_View &view_)
    : _parent(&seq_)
    , _argView(view_)
{
}

c4_RemapWithViewer::~c4_RemapWithViewer() = default;

c4_View c4_RemapWithViewer::GetTemplate()
{
    return _parent.Clone(); // could probably return _parent just as well
}

int c4_RemapWithViewer::GetSize()
{
    return _argView.GetSize();
}

bool c4_RemapWithViewer::GetItem(int row_, int col_, c4_Bytes &buf_)
{
    const c4_Property &map = _argView.NthProperty(0);
    d4_assert(map.Type() == 'I');

    row_ = ((const c4_IntProp &)map)(_argView[row_]);

    return _parent.GetItem(row_, col_, buf_);
}

bool c4_RemapWithViewer::SetItem(int row_, int col_, const c4_Bytes &buf_)
{
    const c4_Property &map = _argView.NthProperty(0);
    d4_assert(map.Type() == 'I');

    row_ = ((const c4_IntProp &)map)(_argView[row_]);

    _parent.SetItem(row_, col_, buf_);
    return true;
}

c4_CustomViewer *f4_CustRemapWith(c4_Sequence &seq_, const c4_View &view_)
{
    return d4_new c4_RemapWithViewer(seq_, view_);
}

/////////////////////////////////////////////////////////////////////////////

class c4_PairViewer : public c4_CustomViewer
{
    c4_View _parent, _argView, _template;

public:
    c4_PairViewer(c4_Sequence &seq_, const c4_View &view_);
    ~c4_PairViewer() override;

    c4_View GetTemplate() override;
    int GetSize() override;
    bool GetItem(int row_, int col_, c4_Bytes &buf_) override;
    bool SetItem(int row_, int col_, const c4_Bytes &buf_) override;
    bool InsertRows(int pos_, c4_Cursor value_, int count_ = 1) override;
    bool RemoveRows(int pos_, int count_ = 1) override;
};

c4_PairViewer::c4_PairViewer(c4_Sequence &seq_, const c4_View &view_) : _parent
        (&seq_)
    , _argView(view_)
    , _template(_parent.Clone())
{
    for (int i = 0; i < _argView.NumProperties(); ++i) {
        _template.AddProperty(_argView.NthProperty(i));
    }
}

c4_PairViewer::~c4_PairViewer() = default;

c4_View c4_PairViewer::GetTemplate()
{
    return _template;
}

int c4_PairViewer::GetSize()
{
    return _parent.GetSize();
}

bool c4_PairViewer::GetItem(int row_, int col_, c4_Bytes &buf_)
{
    c4_View v = _parent;

    if (col_ >= v.NumProperties()) {
        v = _argView;
        col_ = v.FindProperty(_template.NthProperty(col_).GetId());
        d4_assert(col_ >= 0);
    }

    return v.GetItem(row_, col_, buf_);
}

bool c4_PairViewer::SetItem(int row_, int col_, const c4_Bytes &buf_)
{
    c4_View v = _parent;

    if (col_ >= v.NumProperties()) {
        v = _argView;
        col_ = v.FindProperty(_template.NthProperty(col_).GetId());
        d4_assert(col_ >= 0);
    }

    v.SetItem(row_, col_, buf_);
    return true;
}

bool c4_PairViewer::InsertRows(int pos_, c4_Cursor value_, int count_)
{
    _parent.InsertAt(pos_, *value_, count_);
    _argView.InsertAt(pos_, *value_, count_);
    return true;
}

bool c4_PairViewer::RemoveRows(int pos_, int count_)
{
    _parent.RemoveAt(pos_, count_);
    _argView.RemoveAt(pos_, count_);
    return true;
}

c4_CustomViewer *f4_CustPair(c4_Sequence &seq_, const c4_View &view_)
{
    return d4_new c4_PairViewer(seq_, view_);
}

/////////////////////////////////////////////////////////////////////////////

class c4_ConcatViewer : public c4_CustomViewer
{
    c4_View _parent, _argView;

public:
    c4_ConcatViewer(c4_Sequence &seq_, const c4_View &view_);
    ~c4_ConcatViewer() override;

    c4_View GetTemplate() override;
    int GetSize() override;
    bool GetItem(int row_, int col_, c4_Bytes &buf_) override;
    bool SetItem(int row_, int col_, const c4_Bytes &buf_) override;
};

c4_ConcatViewer::c4_ConcatViewer(c4_Sequence &seq_, const c4_View &view_)
    : _parent(&seq_)
    , _argView(view_)
{
}

c4_ConcatViewer::~c4_ConcatViewer() = default;

c4_View c4_ConcatViewer::GetTemplate()
{
    return _parent.Clone(); // could probably return _parent just as well
}

int c4_ConcatViewer::GetSize()
{
    return _parent.GetSize() + _argView.GetSize();
}

bool c4_ConcatViewer::GetItem(int row_, int col_, c4_Bytes &buf_)
{
    c4_View v = _parent;

    if (row_ >= _parent.GetSize()) {
        v = _argView;
        row_ -= _parent.GetSize();
        col_ = v.FindProperty(_parent.NthProperty(col_).GetId());

        if (col_ < 0) {
            return false;
        }
    }

    return v.GetItem(row_, col_, buf_);
}

bool c4_ConcatViewer::SetItem(int row_, int col_, const c4_Bytes &buf_)
{
    c4_View v = _parent;

    if (row_ >= _parent.GetSize()) {
        v = _argView;
        row_ -= _parent.GetSize();
        col_ = v.FindProperty(_parent.NthProperty(col_).GetId());
        d4_assert(col_ >= 0);
    }

    v.SetItem(row_, col_, buf_);
    return true;
}

c4_CustomViewer *f4_CustConcat(c4_Sequence &seq_, const c4_View &view_)
{
    return d4_new c4_ConcatViewer(seq_, view_);
}

/////////////////////////////////////////////////////////////////////////////

class c4_RenameViewer : public c4_CustomViewer
{
    c4_View _parent, _template;

public:
    c4_RenameViewer(c4_Sequence &seq_, const c4_Property &old_, const
                    c4_Property &new_);
    ~c4_RenameViewer() override;

    c4_View GetTemplate() override;
    int GetSize() override;
    bool GetItem(int row_, int col_, c4_Bytes &buf_) override;
    bool SetItem(int row_, int col_, const c4_Bytes &buf_) override;
    //virtual bool InsertRows(int pos_, c4_Cursor value_, int count_=1);
    //virtual bool RemoveRows(int pos_, int count_=1);
};

c4_RenameViewer::c4_RenameViewer(c4_Sequence &seq_, const c4_Property &old_, const c4_Property &new_) : _parent(&seq_)
{
    for (int i = 0; i < _parent.NumProperties(); ++i) {
        const c4_Property &prop = _parent.NthProperty(i);
        _template.AddProperty(prop.GetId() == old_.GetId() ? new_ : prop);
    }
}

c4_RenameViewer::~c4_RenameViewer() = default;

c4_View c4_RenameViewer::GetTemplate()
{
    return _template;
}

int c4_RenameViewer::GetSize()
{
    return _parent.GetSize();
}

bool c4_RenameViewer::GetItem(int row_, int col_, c4_Bytes &buf_)
{
    return _parent.GetItem(row_, col_, buf_);
}

bool c4_RenameViewer::SetItem(int row_, int col_, const c4_Bytes &buf_)
{
    _parent.SetItem(row_, col_, buf_);
    return true;
}

c4_CustomViewer *f4_CustRename(c4_Sequence &seq_, const c4_Property &old_, const c4_Property &new_)
{
    return d4_new c4_RenameViewer(seq_, old_, new_);
}

/////////////////////////////////////////////////////////////////////////////

class c4_GroupByViewer : public c4_CustomViewer
{
    c4_View _parent, _keys, _sorted, _temp;
    c4_Property _result;
    c4_DWordArray _map;

    int ScanTransitions(int lo_, int hi_, t4_byte *flags_, const c4_View
                        &match_) const;

public:
    c4_GroupByViewer(c4_Sequence &seq_, const c4_View &keys_, const c4_Property
                     &result_);
    ~c4_GroupByViewer() override;

    c4_View GetTemplate() override;
    int GetSize() override;
    bool GetItem(int row_, int col_, c4_Bytes &buf_) override;
};

c4_GroupByViewer::c4_GroupByViewer(c4_Sequence &seq_, const c4_View &keys_, const c4_Property &result_) : _parent(&seq_)
    , _keys(keys_)
    , _result(result_)
{
    _sorted = _parent.SortOn(_keys);
    int n = _sorted.GetSize();

    c4_Bytes temp;
    t4_byte *buf = temp.SetBufferClear(n);

    int groups = 0;
    if (n > 0) {
        ++buf[0]; // the first entry is always a transition
        groups = 1 + ScanTransitions(1, n, buf, _sorted.Project(_keys));
    }

    // set up a map pointing to each transition
    _map.SetSize(groups + 1);
    int j = 0;

    for (int i = 0; i < n; ++i) {
        if (buf[i]) {
            _map.SetAt(j++, i);
        }
    }

    // also append an entry to point just past the end
    _map.SetAt(j, n);

    d4_assert(_map.GetAt(0) == 0);
    d4_assert(j == groups);
}

c4_GroupByViewer::~c4_GroupByViewer() = default;

int c4_GroupByViewer::ScanTransitions(int lo_, int hi_, t4_byte *flags_, const
                                      c4_View &match_) const
{
    d4_assert(lo_ > 0);

    int m = hi_ - lo_;
    d4_assert(m >= 0);

    // done if nothing left or if entire range is identical
    if (m == 0 || match_[lo_ - 1] == match_[hi_ - 1]) {
        return 0;
    }

    // range has a transition, done if it is exactly of size one
    if (m == 1) {
        ++(flags_[lo_]);
        return 1;
    }

    // use binary splitting if the range has enough entries
    if (m >= 5) {
        return ScanTransitions(lo_, lo_ + m / 2, flags_, match_) + ScanTransitions
                   (lo_ + m / 2, hi_, flags_, match_);
    }

    // else use a normal linear scan
    int n = 0;

    for (int i = lo_; i < hi_; ++i) {
        if (match_[i] != match_[i - 1]) {
            ++(flags_[i]);
            ++n;
        }
    }

    return n;
}

c4_View c4_GroupByViewer::GetTemplate()
{
    c4_View v = _keys.Clone();
    v.AddProperty(_result);

    return v;
}

int c4_GroupByViewer::GetSize()
{
    d4_assert(_map.GetSize() > 0);

    return _map.GetSize() - 1;
}

bool c4_GroupByViewer::GetItem(int row_, int col_, c4_Bytes &buf_)
{
    if (col_ < _keys.NumProperties()) {
        return _sorted.GetItem(_map.GetAt(row_), col_, buf_);
    }

    d4_assert(col_ == _keys.NumProperties());

    t4_i32 count;
    switch (_result.Type()) {
    case 'I':
        count = _map.GetAt(row_ + 1) - _map.GetAt(row_);
        buf_ = c4_Bytes(&count, sizeof count, true);
        break;
    case 'V':
        _temp = _sorted.Slice(_map.GetAt(row_), _map.GetAt(row_ + 1))
                .ProjectWithout(_keys);
        buf_ = c4_Bytes(&_temp, sizeof _temp, true);
        break;
    default:
        d4_assert(0);
    }

    return true;
}

c4_CustomViewer *f4_CustGroupBy(c4_Sequence &seq_, const c4_View &template_, const c4_Property &result_)
{
    return d4_new c4_GroupByViewer(seq_, template_, result_);
}

/////////////////////////////////////////////////////////////////////////////

class c4_JoinPropViewer : public c4_CustomViewer
{
    c4_View _parent, _template;
    c4_ViewProp _sub;
    int _subPos, _subWidth;
    c4_DWordArray _base, _offset;

public:
    c4_JoinPropViewer(c4_Sequence &seq_, const c4_ViewProp &sub_, bool outer_);
    ~c4_JoinPropViewer() override;

    c4_View GetTemplate() override;
    int GetSize() override;
    bool GetItem(int row_, int col_, c4_Bytes &buf_) override;
};

c4_JoinPropViewer::c4_JoinPropViewer(c4_Sequence &seq_, const c4_ViewProp &sub_, bool outer_) : _parent(&seq_)
    , _sub(sub_)
    , _subPos(_parent.FindProperty
                  (sub_.GetId()))
    , _subWidth(0)
{
    d4_assert(_subPos >= 0);

    for (int k = 0; k < _parent.NumProperties(); ++k) {
        if (k != _subPos) {
            _template.AddProperty(_parent.NthProperty(k));
        } else
        // if there are no rows, then this join does very little anyway
        //! OOPS: if this is an unattached view, then the subviews can differ
        if (_parent.GetSize() > 0) {
            c4_View view = sub_(_parent[0]);
            for (int l = 0; l < view.NumProperties(); ++l) {
                _template.AddProperty(view.NthProperty(l));
                ++_subWidth;
            }
        }
    }

    _base.SetSize(0, 5);
    _offset.SetSize(0, 5);

    for (int i = 0; i < _parent.GetSize(); ++i) {
        c4_View v = _sub(_parent[i]);

        int n = v.GetSize();
        if (n == 0 && outer_) {
            _base.Add(i);
            _offset.Add(~(t4_i32)0); // special null entry for outer joins
        } else {
            for (int j = 0; j < n; ++j) {
                _base.Add(i);
                _offset.Add(j);
            }
        }
    }
}

c4_JoinPropViewer::~c4_JoinPropViewer() = default;

c4_View c4_JoinPropViewer::GetTemplate()
{
    return _template;
}

int c4_JoinPropViewer::GetSize()
{
    return _base.GetSize();
}

bool c4_JoinPropViewer::GetItem(int row_, int col_, c4_Bytes &buf_)
{
    c4_View v = _parent;
    int r = _base.GetAt(row_);

    if (col_ >= _subPos) {
        if (col_ >= _subPos + _subWidth) {
            col_ -= _subWidth - 1;
        } else {
            v = _sub(_parent[r]);
            r = _offset.GetAt(row_);
            if (r < 0) {
                return false;
            }
            // if this is a null row in an outer join

            col_ = v.FindProperty(_template.NthProperty(col_).GetId());
            if (col_ < 0) {
                return false;
            }
            // if subview doesn't have all properties
        }
    }

    return v.GetItem(r, col_, buf_);
}

c4_CustomViewer *f4_CustJoinProp(c4_Sequence &seq_, const c4_ViewProp &sub_, bool outer_)
{
    return d4_new c4_JoinPropViewer(seq_, sub_, outer_);
}

/////////////////////////////////////////////////////////////////////////////

class c4_JoinViewer : public c4_CustomViewer
{
    c4_View _parent, _argView, _template;
    c4_DWordArray _base, _offset;

public:
    c4_JoinViewer(c4_Sequence &seq_, const c4_View &keys_, const c4_View &view_, bool outer_);
    ~c4_JoinViewer() override;

    c4_View GetTemplate() override;
    int GetSize() override;
    bool GetItem(int row_, int col_, c4_Bytes &buf_) override;
};

c4_JoinViewer::c4_JoinViewer(c4_Sequence &seq_, const c4_View &keys_, const
                             c4_View &view_, bool outer_) : _parent(&seq_)
    , _argView(view_.SortOn(keys_))
{
    // why not in GetTemplate, since we don't need to know this...
    _template = _parent.Clone();
    for (int l = 0; l < _argView.NumProperties(); ++l) {
        _template.AddProperty(_argView.NthProperty(l));
    }

    c4_View sorted = _parent.SortOn(keys_).Project(keys_);
    c4_View temp = _argView.Project(keys_);

    _base.SetSize(0, 5);
    _offset.SetSize(0, 5);

    int j = 0;
    int n = 0;

    for (int i = 0; i < sorted.GetSize(); ++i) {
        int orig = _parent.GetIndexOf(sorted[i]);
        d4_assert(orig >= 0);

        if (i > 0 && sorted[i] == sorted[i - 1]) {
            // if last key was same, repeat the same join
            int last = _offset.GetSize() - n;
            for (int k = 0; k < n; ++k) {
                _base.Add(orig);
                _offset.Add(_offset.GetAt(last + k));
            }
        } else {
            // no, this is a new combination
            bool match = false;

            // advance until the temp view entry is >= this sorted entry
            while (j < temp.GetSize()) {
                if (sorted[i] <= temp[j]) {
                    match = sorted[i] == temp[j];
                    break;
                } else {
                    ++j;
                }
            }

            n = 0;

            if (match) {
                do {
                    _base.Add(orig);
                    _offset.Add(j);
                    ++n;
                } while (++j < temp.GetSize() && temp[j] == temp[j - 1]);
            } else if (outer_) {
                // no match, add an entry anyway if this is an outer join
                _base.Add(orig);
                _offset.Add(~(t4_i32)0); // special null entry
                ++n;
            }
        }
    }
}

c4_JoinViewer::~c4_JoinViewer() = default;

c4_View c4_JoinViewer::GetTemplate()
{
    return _template;
}

int c4_JoinViewer::GetSize()
{
    return _base.GetSize();
}

bool c4_JoinViewer::GetItem(int row_, int col_, c4_Bytes &buf_)
{
    c4_View v = _parent;
    int r = _base.GetAt(row_);

    if (col_ >= v.NumProperties()) {
        v = _argView;
        r = _offset.GetAt(row_);
        if (r < 0) {
            return false;
        }
        // if this is a null row in an outer join

        col_ = v.FindProperty(_template.NthProperty(col_).GetId());
        if (col_ < 0) {
            return false;
        }
        // if second view doesn't have all properties
    }

    return v.GetItem(r, col_, buf_);
}

#if 0
bool c4_JoinViewer::GetItem(int row_, int col_, c4_Bytes &buf_)
{
    c4_View v = _parent;

    int o = 0;
    int r = _offset.GetAt(row_);

    if (r < 0) {
        o = ~r;
        if (o == 0) {
            return false;
        }
        // if this is a null row in an outer join
        r -= o;
    }

    if (col_ >= v.NumProperties()) {
        v = _argView;
        r = _o;

        col_ = v.FindProperty(_template.NthProperty(col_));
        if (col_ < 0) {
            return false;
        }
        // if second view doesn't have all properties
    }

    return v.GetItem(r, col_, buf_);
}

#endif

c4_CustomViewer *f4_CustJoin(c4_Sequence &seq_, const c4_View &keys_, const
                             c4_View &view_, bool outer_)
{
    return d4_new c4_JoinViewer(seq_, keys_, view_, outer_);
}

/////////////////////////////////////////////////////////////////////////////
