#pragma once
// slot_map — the gap-preserving display-position carrier for ONE bank: plain
// interchangeable slots, not fixed/addressable ones. A slot is a display position a
// sample id occupies; the map is sample id -> slot (>= 0). Gaps are first-class (a
// bank may have slot 1 occupied with slot 0 empty). At most one id per slot, at
// most one slot per id.
//
// Position lives HERE, not on Sample: a copy of one sample into two banks may sit
// at different slots, so position is a per-bank display concern owned by the
// bank's membership. bank_model / Sample stay untouched.

#include <cstddef>
#include <string>
#include <utility>
#include <vector>

namespace reasampler::model {

class SlotMap {
public:
    // The slot an id occupies, or -1 if the id is not mapped. O(N).
    int slotOf(const std::string& id) const;

    // The id occupying `slot`, or "" if the slot is empty. O(N).
    std::string idAt(int slot) const;

    // The highest occupied slot, or -1 when the map is empty. Defines the append
    // frontier and (with trailing-empty trim) the content extent.
    int maxSlot() const;

    // Ids in ASCENDING slot order (the deterministic display order). Empty slots
    // produce no entry — the caller iterates occupants; sparse layout is a draw
    // concern that reads slotOf/idAt, not this list.
    std::vector<std::string> orderedIds() const;

    // Places `id` at the next free slot after the last occupied one (append). If the
    // id is already mapped it is first removed (leaving its old slot empty), then
    // appended — an append never fills an earlier gap. No-op guard: empty id ignored.
    void append(const std::string& id);

    // Drops `id`'s mapping, LEAVING ITS SLOT EMPTY (no re-pack) so every other id
    // keeps its position. Returns true if the id was mapped.
    bool remove(const std::string& id);

    // Moves `id` to `targetSlot`, gap-preserving:
    //   * target slot EMPTY  -> `id` moves there; its old slot is left empty.
    //   * target slot OCCUPIED -> insert-before-and-shift: `id` takes targetSlot and
    //     every occupant at slot >= targetSlot (except `id` itself) shifts up by one,
    //     preserving their relative order and never colliding. Matches file-manager
    //     reorder. Interior gaps between shifted occupants are preserved as-is
    //     (shift is +1 on each occupant, so the gap structure above the target is kept).
    //   * negative targetSlot is clamped to 0.
    // Returns false (no mutation) if `id` is not mapped. Deterministic.
    bool reorder(const std::string& id, int targetSlot);

    // Rebuilds the map densely from `ids` in the given order (slot i = ids[i]),
    // dropping any prior state. The migration path: a bank with no persisted slot
    // data is seeded from its BankModel insertion order, densely packed (no gaps),
    // so it is visually identical on first load. Empty/duplicate ids skipped.
    void resetDense(const std::vector<std::string>& ids);

    // Drops any mapping whose id is NOT in `liveIds` (a stale marker whose sample left
    // the index) and appends any live id that has NO mapping yet (a sample the index
    // gained out-of-band). Slots of surviving ids are untouched (gaps preserved). Keeps
    // the map consistent with the bank's membership without a re-pack. Deterministic:
    // orphan appends follow `liveIds` order.
    void reconcile(const std::vector<std::string>& liveIds);

    bool empty() const { return entries_.empty(); }
    std::size_t size() const { return entries_.size(); }

    bool operator==(const SlotMap& o) const;

    // JSON fragment (an array of {id, slot} objects, ascending slot). Emitted as the
    // bank envelope's "slots" member by BankBook::serialize; parsed back by its parser.
    // Round-trips losslessly with the rest of the bank.
    std::string serialize() const;

    // Builds a map from explicit (id, slot) pairs parsed from persisted JSON. Enforces
    // the map invariants defensively against a hand-edited blob: a duplicate id keeps
    // its FIRST occurrence; a slot already taken by a kept id drops the later pair
    // (never double-occupies); an empty id or negative slot is dropped. The result is
    // sorted ascending by slot. reconcile() against live membership runs afterward, so
    // a lossy repair here degrades gracefully rather than corrupting lookup.
    static SlotMap fromEntries(const std::vector<std::pair<std::string, int>>& pairs);

private:
    struct Entry {
        std::string id;
        int         slot = 0;
        bool operator==(const Entry& o) const { return id == o.id && slot == o.slot; }
    };
    std::vector<Entry> entries_;  // kept sorted ascending by slot (invariant)

    void sortBySlot();
};

}  // namespace reasampler::model
