#pragma once
// Pure core of the Design View feature — mirror of bank_model: mode registry,
// GUID-keyed membership, folder-tree-aware visibility, park/restore planner,
// and JSON round-trip. Folder structure is an INPUT (the D2 shell reads
// REAPER's I_FOLDERDEPTH); this model never fetches or stores REAPER's live
// tree. See src/core/view/CLAUDE.md for the settled invariants.

#include <cstdint>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <vector>

#include "core/view/fx_offline.h"
#include "core/view/solo_cache.h"

namespace reasampler {

// Stable seed-mode ids. Arrange is the default home for untagged leaves.
inline constexpr const char* kArrangeModeId = "arrange";
inline constexpr const char* kDesignModeId  = "design";

// A display "stance" the user adopts. Modes are ordered by `ordinal` for tab order.
struct Mode {
    std::string id;           // stable, persisted; never reused for a different mode
    std::string displayName;
    int         ordinal = 0;  // tab order

    bool operator==(const Mode& o) const;
};

// Ordered registry of modes. Arrange + Design are seeded on construction; ids
// are unique, adding a duplicate id is rejected.
class ModeRegistry {
public:
    ModeRegistry();  // seeds Arrange (ordinal 0) + Design (ordinal 1)

    bool add(const Mode& mode);

    const Mode* query(const std::string& id) const;

    bool contains(const std::string& id) const { return query(id) != nullptr; }

    const std::vector<Mode>& all() const { return modes_; }

    std::size_t size() const { return modes_.size(); }

    bool operator==(const ModeRegistry& o) const { return modes_ == o.modes_; }

    // Empty registry (no seed modes) for deserialization, so the parsed
    // Arrange/Design don't collide with the default ctor's seeded ones.
    static ModeRegistry makeEmpty() { return ModeRegistry(EmptyTag{}); }

private:
    struct EmptyTag {};
    explicit ModeRegistry(EmptyTag) {}  // no seed

    std::vector<Mode> modes_;  // kept sorted by ordinal, then insertion
};

// The membership record for one tagged leaf track, keyed externally by GUID.
struct Membership {
    std::set<std::string> modeIds;   // the mode(s) this leaf opted into
    bool                  showBoth = false;  // pinned visible + running in every mode

    bool operator==(const Membership& o) const {
        return modeIds == o.modeIds && showBoth == o.showBoth;
    }
};

// Item-level (fixed-lane) lane ownership. Mode operations touch only managed
// lanes; manual lanes are the user's own comping lanes and stay untouched —
// the fixed-lane analog of never-touch-mute/solo. Lane identity is an opaque
// key the shell supplies; this model bakes in no I_FIXEDLANE ordinal assumption.

// One lane's ownership: managed by a specific mode, or manual (user-minted).
struct LaneOwnership {
    std::optional<std::string> managedMode;  // set ⇒ managed by this mode; unset ⇒ manual

    bool isManaged() const { return managedMode.has_value(); }
    bool isManual()  const { return !managedMode.has_value(); }

    bool operator==(const LaneOwnership& o) const { return managedMode == o.managedMode; }
};

// A lane's composite key: (track GUID, opaque lane key).
struct LaneRef {
    std::string trackGuid;
    std::string laneKey;   // opaque, shell-supplied; not assumed to be a stable ordinal

    bool operator<(const LaneRef& o) const {
        if (trackGuid != o.trackGuid) return trackGuid < o.trackGuid;
        return laneKey < o.laneKey;
    }
    bool operator==(const LaneRef& o) const {
        return trackGuid == o.trackGuid && laneKey == o.laneKey;
    }
};

// (track GUID, lane key) -> ownership, GUID-keyed and portable. A lane ABSENT
// from the index is treated as manual by default (never minted by the tool),
// so the managed-only guarantee holds even before the index is populated.
class LaneOwnershipIndex {
public:
    bool setManaged(const std::string& trackGuid, const std::string& laneKey,
                    const std::string& modeId);

    bool setManual(const std::string& trackGuid, const std::string& laneKey);

    // Removes the lane entirely (⇒ manual-by-default again). Returns true if present.
    bool remove(const std::string& trackGuid, const std::string& laneKey);

    const LaneOwnership* query(const std::string& trackGuid, const std::string& laneKey) const;

    // Load-bearing predicate the toggle planner gates on: absent ⇒ not managed.
    bool isManaged(const std::string& trackGuid, const std::string& laneKey) const {
        const LaneOwnership* o = query(trackGuid, laneKey);
        return o && o->isManaged();
    }

    const std::map<LaneRef, LaneOwnership>& all() const { return entries_; }

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

    bool operator==(const LaneOwnershipIndex& o) const { return entries_ == o.entries_; }

private:
    std::map<LaneRef, LaneOwnership> entries_;  // (guid, laneKey) -> ownership
};

// C_LANEPLAYS value for a managed lane under the given active mode: the lane
// plays exclusively iff its owning mode is active, else silent+hidden. Callers
// must only pass MANAGED lanes; manual lanes never reach this decision.
inline constexpr int kLanePlaysExclusive = 1;  // C_LANEPLAYS: plays exclusively
inline constexpr int kLaneSilent         = 0;  // C_LANEPLAYS: does not play (hidden+silent)
int laneModeState(const std::string& managedMode, const std::string& activeMode);

// GUID-keyed membership index. Untagged GUIDs are absent and belong to Arrange.
class MembershipIndex {
public:
    // Tags `guid` into `modeId`, replacing any prior mode set. Returns false if
    // guid or modeId is empty.
    bool tag(const std::string& guid, const std::string& modeId);

    // Removes `guid` entirely (returns it to the Arrange default).
    bool untag(const std::string& guid);

    // Sets the show-both flag; creates an untagged (Arrange-default) entry if
    // `guid` had none, so show-both alone is representable.
    bool setShowBoth(const std::string& guid, bool showBoth);

    // Installs a complete membership record verbatim, replacing any existing
    // entry. Used by deserialization to rebuild a trusted entry without tag()'s
    // single-mode clobbering.
    bool restore(const std::string& guid, const Membership& membership);

    const Membership* query(const std::string& guid) const;

    bool isShowBoth(const std::string& guid) const {
        const Membership* m = query(guid);
        return m && m->showBoth;
    }

    // The mode ids `guid` belongs to. Empty for an untagged guid (⇒ Arrange).
    std::set<std::string> modesOf(const std::string& guid) const;

    const std::map<std::string, Membership>& all() const { return entries_; }

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

    bool operator==(const MembershipIndex& o) const { return entries_ == o.entries_; }

private:
    std::map<std::string, Membership> entries_;  // guid -> membership
};

// Folder tree: an INPUT the shell rebuilds from I_FOLDERDEPTH each call, never
// stored here. A parent is visible in a mode if it belongs by its own
// membership or any descendant leaf does, and is never parked. The master
// track is implicit (always visible, untouched) and is not a node here.
struct FolderNode {
    std::string guid;
    std::string parentGuid;  // empty ⇒ top-level (child of master / project root)
    bool        isParent = false;  // true if this node has descendant tracks (a folder)
};

// Arrange-view order; parentGuid links each node to its immediate parent folder.
struct FolderTree {
    std::vector<FolderNode> nodes;
};

// The prior value of every tool-driven flag on one track, captured BEFORE
// parking — restore's source of truth. Ints, not bools, so a snapshot
// faithfully round-trips whatever REAPER reported (defensive against
// non-0/1 values).
struct TrackSnapshot {
    int showInTcp   = 0;   // B_SHOWINTCP prior value
    int showInMixer = 0;   // B_SHOWINMIXER prior value
    int mainSend    = 0;   // B_MAINSEND  prior value
    int fxEnable    = 0;   // I_FXEN      prior value

    // Prior per-FX offline state in capture-time slot order, keyed per fxKeying:
    // by the FX's own identity (live capture), or by position (a snapshot lifted
    // from a project saved before identity was recorded).
    std::vector<FxOfflineState> fxOffline;
    FxKeying                    fxKeying = FxKeying::Identity;

    bool operator==(const TrackSnapshot& o) const {
        return showInTcp == o.showInTcp && showInMixer == o.showInMixer &&
               mainSend == o.mainSend && fxEnable == o.fxEnable &&
               fxOffline == o.fxOffline && fxKeying == o.fxKeying;
    }
};

// Which scalar flag a TrackFlagOp drives. FX-offline is carried separately (it
// is per-slot, variable length) — see TrackParkPlan::fxOffline.
enum class Flag {
    ShowInTcp,    // B_SHOWINTCP
    ShowInMixer,  // B_SHOWINMIXER
    MainSend,     // B_MAINSEND
    FxEnable,     // I_FXEN
};

// One scalar-flag write the shell must apply: SetMediaTrackInfo_Value(guid, flag, value).
struct TrackFlagOp {
    std::string guid;
    Flag        flag = Flag::ShowInTcp;
    int         value = 0;

    bool operator==(const TrackFlagOp& o) const {
        return guid == o.guid && flag == o.flag && value == o.value;
    }
};

// One managed-lane play/show write the shell must apply (translated into
// C_LANEPLAYS / I_FIXEDLANE / B_FIXEDLANE_HIDDEN). Emitted for MANAGED lanes
// only — never a manual lane; enforced in planToggle and mirrored by
// lanesTouchedByToggle.
struct LanePlayOp {
    std::string trackGuid;
    std::string laneKey;   // opaque, shell-supplied
    int         lanePlays = kLaneSilent;

    bool operator==(const LanePlayOp& o) const {
        return trackGuid == o.trackGuid && laneKey == o.laneKey && lanePlays == o.lanePlays;
    }
};

// The complete set of operations to park one inactive leaf, or restore one
// leaf. Park uses fixed zeros; restore uses a snapshot's values. Park's
// fxOffline ops are slot-keyed (every live slot goes offline); restore's carry
// the snapshot's keying and are resolved against the live chain by
// resolveFxRestore before any write.
struct TrackPlan {
    std::vector<TrackFlagOp>  flags;
    std::vector<FxOfflineOp>  fxOffline;
};

// The plan for a toggle to a target mode. Parents and show-both leaves never
// appear (derived-visible, never parked — see visibleTracks). Untagged
// leaves DO appear: an untagged leaf is an Arrange member, so it parks in
// every non-Arrange mode and restores in Arrange.
struct TogglePlan {
    std::vector<TrackPlan> park;     // inactive leaves -> parked (fixed zeros)
    std::vector<TrackPlan> restore;  // active leaves returning -> snapshot values

    // Per managed lane, the C_LANEPLAYS state for the target mode. Managed
    // lanes only. Empty when no fixed lanes exist, so a lane-free project
    // produces an identical plan to before fixed-lane support.
    std::vector<LanePlayOp> lanes;
};

// Owns the mode registry, membership index, active mode, and durable
// per-track snapshots (kept while parked so a save-while-parked project
// restores correctly). Visibility and the toggle plan are computed against a
// supplied FolderTree — the tree is never stored.
class ViewModeModel {
public:
    ViewModeModel();  // Arrange + Design seeded; active mode = Arrange

    ModeRegistry&           modes()       { return modes_; }
    const ModeRegistry&     modes() const { return modes_; }
    MembershipIndex&        membership()       { return membership_; }
    const MembershipIndex&  membership() const { return membership_; }
    LaneOwnershipIndex&        lanes()       { return lanes_; }
    const LaneOwnershipIndex&  lanes() const { return lanes_; }
    view::SoloCache&           soloCache()       { return soloCache_; }
    const view::SoloCache&     soloCache() const { return soloCache_; }

    const std::string& activeModeId() const { return activeModeId_; }
    // Returns false (no change) if the id is not registered.
    bool setActiveMode(const std::string& modeId);

    // The shell calls store before it parks a track, so restore survives a save.
    void storeSnapshot(const std::string& guid, const TrackSnapshot& snap);
    void clearSnapshot(const std::string& guid);
    const TrackSnapshot* snapshot(const std::string& guid) const;
    const std::map<std::string, TrackSnapshot>& snapshots() const { return snapshots_; }

    // Drops every snapshot whose GUID is NOT in `liveGuids`, and prunes the solo
    // cache the same way (see SoloCache::reconcile). Returns the count of
    // SNAPSHOTS removed — the solo cache's own count is available from it directly.
    //
    // Snapshots are pruned, membership is not: a parked track's snapshot is
    // dead weight once the track is deleted (can never restore; a reused GUID
    // would drive an incorrect restore). Membership survives because REAPER's
    // undo of a track delete restores the SAME GUID — dropping the tag on
    // delete would lose it on undo. A never-restored track leaves only a
    // dormant membership entry, which is a fine trade against losing tags on
    // undo. Folder restructure is self-healing (tree rebuilt every toggle) and
    // is not what this handles.
    std::size_t reconcile(const std::set<std::string>& liveGuids);

    // A leaf belongs if tagged into modeId, show-both, or untagged with modeId
    // == Arrange. No parent derivation here — see visibleTracks for that.
    bool leafBelongsToMode(const std::string& guid, const std::string& modeId) const;

    // Tree-aware visible set: active leaves, show-both leaves, and every
    // parent that belongs to the mode itself or has a visible descendant.
    // Untagged nodes count as Arrange. Stale tree GUIDs are tolerated; the
    // master is not represented (always visible, untouched).
    std::set<std::string> visibleTracks(const FolderTree& tree,
                                        const std::string& modeId) const;

    // Enumerates every leaf in `tree`; a leaf inactive in `targetMode` is
    // parked (fixed zeros), one becoming active with a stored snapshot is
    // restored from it. Parents and show-both leaves are never parked.
    // Untagged leaves are Arrange members and park/restore accordingly. Tree
    // membership is the enumeration source, so stale membership GUIDs absent
    // from the tree are ignored.
    //
    // Park plans here carry an empty fxOffline vector — the D2 shell expands
    // per-FX offline writes via TrackFX_GetCount (not available to the pure model).
    TogglePlan planToggle(const FolderTree& tree, const std::string& targetMode) const;

    // Managed lanes only, from the ownership index — the set a toggle may
    // drive. Independent of the folder tree (lane ownership isn't a tree
    // property); the target mode decides each lane's play VALUE, not the set.
    std::set<LaneRef> lanesTouchedByToggle() const;

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

    std::string serialize() const;

    // std::nullopt on malformed input. deserialize(serialize(x)) == x on success.
    static std::optional<ViewModeModel> deserialize(const std::string& json);

private:
    ModeRegistry    modes_;
    MembershipIndex membership_;
    LaneOwnershipIndex lanes_;                            // (guid, laneKey) -> ownership
    std::string     activeModeId_;                       // always a registered id
    std::map<std::string, TrackSnapshot> snapshots_;     // guid -> pre-park snapshot
    view::SoloCache soloCache_;                          // modeId -> guid -> raw I_SOLO
};

// Fixed-zero park plan for one leaf, offlining `fxCount` slots.
TrackPlan makeParkPlan(const std::string& guid, int fxCount);

// Restore plan for one leaf from its snapshot — every flag to its captured
// value, never a default.
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap);

// Auto-tag decision: new content takes the active mode at creation; the
// Wave-2 shell diffs GUIDs on the panel timer and asks this what to tag.
// Pre-existing content never reaches here.
//
// Manual-lane exemption: an item landing on a MANUAL lane is off-limits.
//
// Adoption (strand fix): a new item on a track that already carries
// pre-existing content adopts that content's single mode rather than blindly
// taking the active mode — otherwise the track would go multi-mode, get
// lane-split, and strand the pre-existing (previously visible) items on a
// silenced lane with no user intent. Falls back to the active mode only when
// the track has no pre-existing managed-eligible content, or that content
// already spans multiple modes (an existing deliberate split).

// One new item the shell detected this poll.
struct NewItem {
    std::string guid;
    bool        onManualLane = false;  // true ⇒ EXEMPT from auto-tag
    // Distinct modes the pre-existing (not-new-this-tick) content on this
    // item's track resolves to. Empty ⇒ take active mode. Exactly one ⇒
    // adopt it. More than one ⇒ already a deliberate split, take active mode.
    std::set<std::string> trackModes;
};

// One membership write: tag `guid` into `modeId`.
struct AutoTag {
    std::string guid;
    std::string modeId;

    bool operator==(const AutoTag& o) const { return guid == o.guid && modeId == o.modeId; }
};

// Every new track is tagged to `activeMode`. Every new item is tagged unless
// exempt (manual lane); its target is the adopted single mode of its track's
// pre-existing content, else `activeMode`. Empty `activeMode` yields no tags.
// Empty GUIDs are skipped. Mutates nothing.
std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackGuids,
                                       const std::vector<NewItem>& newItems,
                                       const std::string& activeMode);

// Item-level mode-move decision (bindable "Move selected items -> mode"
// actions): which selected items to retag, and to what. Manual-lane items
// (shell-reported `onManualLane`) are exempt — never retagged, never re-laned,
// upholding the managed-lanes-only invariant under an explicit user action too.

// One selected item the shell reports for the retag decision.
struct RetagItem {
    std::string guid;
    bool        onManualLane = false;  // true ⇒ EXEMPT
};

// One membership op: `untag` removes the item (Arrange default); otherwise
// tags it into `modeId`.
struct ItemRetagOp {
    std::string guid;
    bool        untag = false;   // true ⇒ untag; false ⇒ tag into modeId
    std::string modeId;          // the target mode when !untag (empty when untag)

    bool operator==(const ItemRetagOp& o) const {
        return guid == o.guid && untag == o.untag && modeId == o.modeId;
    }
};

// Empty `targetMode` means untag (Move -> Arrange and Untag collapse to the
// same act, mirroring the track-level doUntag). Manual-lane and empty-GUID
// items are skipped. Mutates nothing.
std::vector<ItemRetagOp> planItemRetag(const std::vector<RetagItem>& selected,
                                       const std::string& targetMode);

// Lane-minting decision (D2 Wave 3): once a track is visible in more than one
// mode while carrying its own media, whole-track parking can no longer keep
// stances separate, so it drops to fixed lanes — one managed lane per
// involved mode, each item assigned to its mode's lane.
//
// "Visible in more than one mode" has two independent triggers, either
// splits the track: (a) the track's own items span >= 2 modes, or (b) the
// track is a content-bearing folder derived-visible in >= 2 modes
// (visibleTracks) even though its own item is single-mode — the folder case
// a naive own-item-span check would miss.
//
// Show-both tracks are skipped outright (never force-split — the point of
// show-both is staying audible everywhere). Manual-lane items are exempt.
// Lanes are minted LAZILY — only for modes the track's own items actually
// occupy, never an empty reserved lane for a merely-derived-visible mode;
// confinement still holds because an absent lane never plays.
//
// Idempotent: re-reporting an already-split track yields the same mints and
// assignments, so re-running detection does not thrash the project or undo
// history.

// One item the shell reports for the minting decision.
struct LaneItem {
    std::string guid;
    std::string modeId;                // the mode this item's content belongs to
    bool        onManualLane = false;  // true ⇒ EXEMPT (user's hand-managed lane)
};

// One track the shell reports: its GUID plus the items on it.
struct LaneTrack {
    std::string trackGuid;
    std::vector<LaneItem> items;
};

// One item→lane assignment the shell must apply (I_FIXEDLANE = the lane the
// durable key `laneKey` currently occupies). Only managed-eligible items appear.
struct LaneAssign {
    std::string itemGuid;
    std::string trackGuid;
    std::string laneKey;   // durable managed-lane key (laneNameForMode(modeId))

    bool operator==(const LaneAssign& o) const {
        return itemGuid == o.itemGuid && trackGuid == o.trackGuid && laneKey == o.laneKey;
    }
};

// One managed lane the shell must mint: its durable key (== the P_LANENAME to
// stamp) and the mode that owns it (an ownership-index write).
struct LaneMint {
    std::string trackGuid;
    std::string laneKey;   // == laneNameForMode(modeId); the P_LANENAME to stamp
    std::string modeId;    // the owning mode (ownership-index managed-for-mode write)

    bool operator==(const LaneMint& o) const {
        return trackGuid == o.trackGuid && laneKey == o.laneKey && modeId == o.modeId;
    }
};

// The complete plan; empty when no track needs splitting (D1 behavior
// unchanged). The shell wraps application in one Undo block (visible
// structural mutation).
struct LaneMintPlan {
    // Tracks to switch into fixed-lane mode (I_FREEMODE=2, I_NUMFIXEDLANES >=
    // laneCount). Idempotent — an already-split track still appears, but the
    // shell's ensure is then a no-op.
    struct TrackSplit {
        std::string trackGuid;
        int         laneCount = 0;  // number of managed lanes this track needs
    };
    std::vector<TrackSplit>  splits;
    std::vector<LaneMint>    mints;    // managed lanes to mint (name + ownership write)
    std::vector<LaneAssign>  assigns;  // item→managed-lane assignments

    bool empty() const {
        return splits.empty() && mints.empty() && assigns.empty();
    }
};

// `model` supplies membership + show-both state; `tree` supplies folder
// structure for the derived-visibility trigger. Items with an empty GUID or
// modeId are skipped (defensive). Mutates nothing.
LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
                             const std::vector<LaneTrack>& tracks);

// Next mode id in ordinal order, cycling past `currentModeId` and wrapping
// after the last. Empty registry -> "". currentModeId not present -> the
// first mode's id. Free function (not a model member) so it is testable
// against a bare ModeRegistry.
std::string nextModeId(const ModeRegistry& modes, const std::string& currentModeId);

} // namespace reasampler
