JanitorPeep.cs
namespace NoChillquarium;

/// <summary>
/// Janitor cameo — only after a real beat, never on a timer.
/// Tim &amp; Eric energy: too close, too specific, legally unclear.
/// </summary>
public static class JanitorPeep
{
	const float MinGap = 155f;
	const float VisibleSeconds = 4.8f;
	const float OutSeconds = 0.4f;
	const float SettleSeconds = 1.6f; // wait out banners / overlay close
	const float IdleAfkSeconds = 280f; // one "you left" peep after a long sit

	static readonly Random _rng = new();
	static float _cooldown;
	static float _visibleLeft;
	static float _outLeft;
	static float _quietTime;
	static float _settle;
	static int _lastFishCount = -1;
	static string _line = "";
	static string _pending;
	static int _version;
	static int _lastLine = -1;
	static bool _saidIdle;

	public static int Version => _version;
	public static bool IsVisible => _visibleLeft > 0f || _outLeft > 0f;
	public static bool Leaving => _visibleLeft <= 0f && _outLeft > 0f;
	public static string Line => _line ?? "";

	public static void ResetForNewGame()
	{
		_cooldown = 50f; // not on top of his own intro gift
		_visibleLeft = 0f;
		_outLeft = 0f;
		_quietTime = 0f;
		_settle = 0f;
		_lastFishCount = -1;
		_line = "";
		_pending = null;
		_lastLine = -1;
		_saidIdle = false;
		_version++;
	}

	public static void Clear()
	{
		_visibleLeft = 0f;
		_outLeft = 0f;
		_line = "";
		_pending = null;
		_version++;
	}

	/// <summary>Force a cameo even on cooldown (player-initiated crime).</summary>
	public static void ForceShow( string beat )
	{
		if ( string.IsNullOrEmpty( beat ) || !Unlocked )
			return;
		_pending = null;
		Fire( beat );
	}

	/// <summary>Swap his line if he's already in the window, else pop him.</summary>
	public static void SayNow( string beat )
	{
		if ( string.IsNullOrEmpty( beat ) || !Unlocked )
			return;
		if ( IsVisible )
		{
			_line = PickLine( beat );
			_visibleLeft = Math.Max( _visibleLeft, 4.4f );
			_outLeft = 0f;
			_version++;
			return;
		}
		ForceShow( beat );
	}

	/// <summary>Call from gameplay beats. Harmless if he's not invited yet.</summary>
	public static void On( string beat )
	{
		if ( string.IsNullOrEmpty( beat ) )
			return;
		if ( !Unlocked )
			return;
		if ( IsVisible )
			return;
		if ( _cooldown > 0f || !CanShow() || _settle < SettleSeconds )
		{
			// Keep the juicier beat; don't stack a pile.
			if ( string.IsNullOrEmpty( _pending ) || IsJuicier( beat, _pending ) )
				_pending = beat;
			return;
		}

		Fire( beat );
	}

	public static void Tick( float dt )
	{
		if ( dt <= 0f )
			return;

		if ( _outLeft > 0f )
		{
			_outLeft -= dt;
			if ( _outLeft <= 0f )
			{
				_outLeft = 0f;
				_line = "";
				_version++;
			}
			return;
		}

		if ( _visibleLeft > 0f )
		{
			_visibleLeft -= dt;
			if ( _visibleLeft <= 0f )
			{
				_visibleLeft = 0f;
				_outLeft = OutSeconds;
				_version++;
			}
			return;
		}

		if ( _cooldown > 0f )
			_cooldown -= dt;

		if ( !Unlocked )
			return;

		if ( !CanShow() )
		{
			_quietTime = 0f;
			_settle = 0f;
			return;
		}

		_settle += dt;

		// Empty-tank beat — only when they just cleared the glass.
		var n = TankSim.TotalFishCount;
		if ( _lastFishCount > 0 && n == 0 )
			On( "empty" );
		_lastFishCount = n;

		// One AFK visit per session, after a long quiet stretch.
		_quietTime += dt;
		if ( !_saidIdle && _quietTime >= IdleAfkSeconds && _cooldown <= 0f )
		{
			_saidIdle = true;
			On( "idle" );
		}

		if ( _cooldown <= 0f && _settle >= SettleSeconds && !string.IsNullOrEmpty( _pending ) )
		{
			var beat = _pending;
			_pending = null;
			Fire( beat );
		}
	}

	static bool Unlocked =>
		GrandmaGifts.HasTank
		&& (GrandmaGifts.HasReceived( "janitor_fusion" ) || StorySystem.HasCombinedOnce);

	static bool CanShow()
	{
		if ( !TankSim.IsActive || !GrandmaGifts.HasTank )
			return false;
		if ( GrandmaGifts.IsBusy || GrandmaGifts.IsLookingAround )
			return false;
		if ( StorySystem.CardOpen )
			return false;
		if ( BattleSystem.IsOpen || LotRunSystem.IsOpen )
			return false;
		if ( TrainingSystem.TrainOpen || TrainingSystem.InjectOpen || MethRush.Open )
			return false;
		if ( FishFusion.PickingSecond )
			return false;
		if ( !string.IsNullOrEmpty( TankSim.MenuFocusId ) )
			return false;
		return true;
	}

	static bool IsJuicier( string a, string b )
	{
		static int Rank( string s ) => s switch
		{
			"boom" or "combine" or "caffeine" or "pipes" => 6,
			"dead" or "grandma_meth" or "tin" => 5,
			"flush" or "fightwin" or "fightloss" or "ransack" or "spike" => 4,
			"meth" or "rare" or "empty" or "kick" => 3,
			"sell" or "poke" => 2,
			_ => 1
		};
		return Rank( a ) >= Rank( b );
	}

	static void Fire( string beat )
	{
		_line = PickLine( beat );
		_visibleLeft = VisibleSeconds;
		_outLeft = 0f;
		_cooldown = MinGap;
		_quietTime = 0f;
		_settle = 0f;
		// No ding — the face is the joke. A notice SFX made him feel like a toaster.
		_version++;
	}

	static string PickLine( string beat )
	{
		var pool = LinesFor( beat );
		if ( pool.Length == 0 )
			pool = LinesFor( "stare" );
		var i = _rng.Next( pool.Length );
		if ( pool.Length > 1 && i == _lastLine )
			i = (i + 1) % pool.Length;
		_lastLine = i;
		return pool[i];
	}

	static string[] LinesFor( string beat ) => beat switch
	{
		"combine" => new[]
		{
			"Brad's garage only reads monsters. I don't pry. I suggest. The suggestion has teeth. Closet teeth.",
			"Two became one. That's marriage. That's a smoothie. I'm clapping in the closet. Don't come in. I'm not even supposed to be on this shift.",
			"The seam is beautiful. I have a binder for seams. Tab C. If you open Tab C we have to talk.",
			"They said yes. I asked them. With my mouth against the glass. That's consent in some counties.",
		},
		"boom" => new[]
		{
			"Pop. I clapped. In the closet. Alone. It's a lifestyle. HR knows. HR is me.",
			"Indoor fireworks. I wrote that down. Under Hobbies. Then Legal. Then I ate the paper.",
			"The wet went away. That's comedy. That's also a report I will not file. I filed it in my mouth.",
			"I rate that a strong medium. The splatter had confidence. I have a laminated scorecard. It's damp.",
		},
		"sell" => new[]
		{
			"You sold him. I had a name for him. In a folder. In a drawer. The drawer is me.",
			"Cash for wet. Very yard sale. Very American. I brought coupons. They expired during Clinton.",
			"Goodbye, little employee. HR will miss you. I am HR. I am also the exit interview. Sit down.",
			"I watched him grow. From a chair you can't see. It's a great chair. It has a cup holder for looking.",
		},
		"fightwin" => new[]
		{
			"Brad cried into a receipt. I laminated it. For the fridge. For later. For reasons I will not discuss in this window.",
			"Winning has a smell. I catalog smells. This one's a Tuesday. Tuesday is my favorite employee.",
			"The garage lost. I mopped the loss. It was sticky. Like pride. Like jam. Like a feeling.",
		},
		"fightloss" => new[]
		{
			"Losing is a texture. I lick textures. Professionally. Don't put that on the form. Put it under Other.",
			"Brad did a little dance. I did a bigger dance. In the vents. The vents have a disco ball now.",
			"They'll be okay. Or they won't. Both are content. I love content. I am content. I am watching.",
		},
		"flush" => new[]
		{
			"The pipes said thank you. Personally. They asked me to pass that along. I live with them. Rent is staring.",
			"Down. Down. Down. That's a direction I respect. I have maps. Wet maps. The maps have my face on them.",
			"I waved at the drain. It waved back. That's our thing. Don't make it weird. I already did. Twice.",
			"Adventure. In the toilet. That's a sentence. I framed that sentence. The bathroom is a gallery now. Suggested donation: staring.",
		},
		"empty" => new[]
		{
			"Nobody home. I like nobody home. I brought a folding chair. And a sandwich. And a second chair. For the sandwich.",
			"Empty glass. That's a canvas. That's a mirror. That's my lunch break. I'm clocked in. Don't tell.",
			"All gone. I whispered goodbye individually. I have a list. The list has stars. The stars have names.",
		},
		"dead" => new[]
		{
			"He's gone. I took attendance. I put a sticker next to his name. The sticker is a sad face. I drew it with my finger.",
			"Death is a spill. I mop spills. I mop feelings. I mop the idea of you. It's fine. It's a job.",
			"I said a prayer. It was mostly vowels. The pipes sang backup. We nailed the ending.",
			"I was going to say hi. Then he did the leaving thing. Rude. Iconic. I took notes.",
		},
		"rare" => new[]
		{
			"Shiny. I would keep that in a sock. A special sock. The sock has a name. The name is legally my own.",
			"That's a collector piece. I collect. I collect looking. I'm winning. The trophy is this window.",
			"Ooooh colors. My favorite channel. After the static. After the other static. After the channel that's just me.",
		},
		"meth" => new[]
		{
			"Science. Illegal science. My favorite block. I brought popcorn. The popcorn is watching too. It signed a waiver.",
			"Wrong water, right snack. I'm not a cop. I'm a vibe. The vibe has a mop. The mop has a warrant.",
			"I will not tell Grandma. I will tell the pipes. The pipes are worse. The pipes keep notes.",
		},
		"caffeine" => new[]
		{
			"No thank you. I'm already illegal. It's Folgers. Folgers is a controlled substance in my bloodstream. I have a note from a gas station.",
			"I don't do that. I do cups. I do cups until the cups do me. The cups filed a complaint. I drank the complaint.",
			"That's a no from Caffeine. Caffeine is my manager. Caffeine said if I touch that we have to redo the whole closet.",
			"I already have a drug. It's called 4am. 4am is a person. 4am lives in the break room. We share a mug. The mug has teeth.",
			"Refuse. REFUSE. That's the bit. The bit is I'm vibrating at a legal frequency. Your powder is off-key. I have perfect pitch for crimes.",
			"I laminated a decline. It's still warm. Don't put it near the Folgers. The Folgers gets jealous. The Folgers has a knife.",
		},
		"poke" => new[]
		{
			"You touched the bit. The bit touched back. That's a handshake. That's a marriage in two counties.",
			"Hi. Still unpaid. Still a window. Still your problem. I brought a second face for if this one gets tired.",
			"Don't mind the tap. The tap is how I clock in. The clock is a suggestion. I ate the suggestion.",
		},
		"kick" => new[]
		{
			"You kicked my favorite rectangle. I have a form for that. The form is wet. The wet is labeled Assault.",
			"Glass has feelings. I interviewed the glass. The glass said ouch in a professional way.",
			"That's a workplace incident. I am the workplace. I am also the incident. Sit down. Don't sit down. Hover.",
		},
		"tin" => new[]
		{
			"You robbed a funeral. That's a genre. I collect genres. This one goes under Family, then Legal, then Mouth.",
			"Grandpa's tin. I knew that tin. We had a moment. The moment was staring. You ruined staring.",
			"I will not tell. I will mime it at bingo. Bingo is a courtroom. The courtroom has cookies.",
		},
		"pipes" => new[]
		{
			"THE PIPES ARE DOING HOMEWORK. I live with them. Rent is staring. Rent just grew teeth.",
			"You dosed my roommates. My roommates are plumbing. They have a group chat. I am the group chat.",
			"That went down. Down is my zip code. I have to sleep there. I have to mop there. I have to file a vibe.",
		},
		"spike" => new[]
		{
			"I saw the handshake over the bucket. I have it on a napkin. The napkin is evidence. The evidence is damp.",
			"Brad's fish are studying. I respect academia. I do not respect Brad. I laminated that opinion.",
			"Pre-game in a garage. That's sports. That's a felony. That's my favorite channel after the static.",
		},
		"ransack" => new[]
		{
			"You emptied a sock drawer. That's archaeology. I brought a brush. The brush is a mop. The mop is me.",
			"Brad will laminate this. He laminates grief. I already reserved a fridge magnet. It's sticky. Like pride.",
			"Theft with a folding chair audience. I was the chair. I was also the audience. I clapped in the vents.",
		},
		"grandma_meth" => new[]
		{
			"You gave Grandma a bag. I wrote that down. Then I ate the paper. Then the paper wrote me up.",
			"The soup is grounded. I heard the soup. The soup has a lawyer. The lawyer asked for my statement. I mopped it.",
			"That's a family crime. Those are my favorite. I have a scrapbook. The scrapbook is damp. The damp is labeled Love.",
		},
		"idle" => new[]
		{
			"You left. I stayed. That's our dynamic. I'm not even supposed to be here today. I made a graph. The graph is wet. The wet is labeled Us.",
			"Still here. Still unpaid. Still a bit. The bit is staring. Pause me. I'm buffering. That's a lifestyle.",
			"I counted the bubbles. Twice. They were different both times. That's God. I shook His hand. Wet handshake. He didn't tip.",
		},
		_ => new[]
		{
			"Don't mind me. The blinds are a suggestion. The wall is a suggestion. I am a suggestion. Accept me. Decline in writing.",
			"I don't pry. I hover. Hovering is a skill. I have a certificate. I printed it. I laminated the printer. The printer screamed.",
			"Mmm. Mmm. That's my whole thought. You can have it. I'm done using it. It's still warm. Don't microwave it.",
		}
	};
}