namespace se {
funcdef void MainCallback(int number);
funcdef void PlayerCallback(::jjPLAYER@ player, int number);
funcdef bool DrawingCallback(::jjPLAYER@ player, ::jjCANVAS@ canvas, const ::jjANIMATION@ anim);
funcdef void PacketCallback(::jjSTREAM& packet, int clientID);
funcdef ::jjSTREAM PacketConstructor();
shared enum WeaponTrait {
	weapon_no_trait                         = 0,
	weapon_deals_damage                     = 1 << 0,
	weapon_causes_splash_damage             = 1 << 1,
	weapon_may_harm_user                    = 1 << 2,
	weapon_may_heal_players                 = 1 << 3,
	weapon_increases_mobility               = 1 << 4,
	weapon_inflicts_status_condition        = 1 << 5,
	weapon_melts_ice                        = 1 << 6,
	weapon_freezes_objects                  = 1 << 7,
	weapon_fails_underwater                 = 1 << 8,
	weapon_fails_in_interiors               = 1 << 9,
	weapon_goes_through_thin_walls          = 1 << 10,
	weapon_goes_through_thick_walls         = 1 << 11,
	weapon_is_super_weapon                  = 1 << 12,
	weapon_is_effective_against_players     = 1 << 13,
	weapon_is_effective_against_sp_enemies  = 1 << 14,
	weapon_destroys_crates                  = 1 << 15,
	weapon_supports_mouse_aim               = 1 << 16,
	weapon_requires_mouse_use               = 1 << 17,
	weapon_works_in_mp                      = 1 << 18,
	weapon_works_in_sp                      = 1 << 19,
	weapon_works_in_coop                    = 1 << 20,
	weapon_works_in_splitscreen             = 1 << 21,
	weapon_works_online                     = 1 << 22,
	weapon_has_ammo_pickups                 = 1 << 23,
	weapon_has_ammo_crates                  = 1 << 24,
	weapon_has_powerup_monitors             = 1 << 25,
	weapon_has_distinct_powerup             = 1 << 26,
	weapon_requires_no_ammo_pickups         = 1 << 27,
	weapon_cannot_be_fired_by_key_press     = 1 << 28,
	weapon_requires_certain_events_in_level = 1 << 29,
	weapon_goes_through_walls               = weapon_goes_through_thin_walls | weapon_goes_through_thick_walls,
	weapon_is_effective_against_all_targets = weapon_is_effective_against_players | weapon_is_effective_against_sp_enemies | weapon_destroys_crates,
	weapon_works_in_all_modes               = weapon_works_in_mp | weapon_works_in_sp | weapon_works_in_coop | weapon_works_in_splitscreen | weapon_works_online,
	weapon_has_all_pickups                  = weapon_has_ammo_pickups | weapon_has_ammo_crates | weapon_has_powerup_monitors | weapon_has_distinct_powerup,
	weapon_default_traits                   = weapon_deals_damage | weapon_is_effective_against_all_targets | weapon_supports_mouse_aim |
	                                          weapon_works_in_all_modes | weapon_has_all_pickups
}
shared interface UserData {}
shared interface WeaponHook {
	void setUserData(uint number, UserData@ data);
	UserData@ getUserData(uint number) const;
	void setWeaponSprite(uint number, bool powerup, const ::jjANIMATION@ anim);
	void resetCallbacks(uint number);
	void setMainCallback(uint number, MainCallback@ callback);
	void setPlayerCallback(uint number, PlayerCallback@ callback);
	void setPlayerInputCallback(uint number, PlayerCallback@ callback);
	void setDrawingCallback(uint number, DrawingCallback@ callback);
	PacketConstructor@ addPacketCallback(PacketCallback@ callback);
	bool drawAmmo(::jjPLAYER@ player, ::jjCANVAS@ canvas);
	void processMain();
	void processPlayer(::jjPLAYER@ player);
	void processPlayerInput(::jjPLAYER@ player);
	bool processPacket(::jjSTREAM &in packet, int clientID);
}
shared interface WeaponInterface {
	::jjANIMSET@ loadAnims(::jjANIMSET@ animSet);
	::array<bool>@ loadSamples(const ::array<SOUND::Sample>& samples);
	uint getSampleCount() const;
	uint getTraits(bool powerup) const;
	uint getMaxDamage(bool powerup) const;
	bool setAsWeapon(uint number, WeaponHook@ weaponHook);
}
shared bool isValidWeapon(uint number) {
	return number != 0 && number <= WEAPON::GUN9;
}
shared uint getBasicBulletOfWeapon(uint number) {
	if (isValidWeapon(number)) {
		if (number < WEAPON::TNT)
			return OBJECT::BLASTERBULLET + number - WEAPON::BLASTER;
		if (number > WEAPON::TNT)
			return OBJECT::FIREBALLBULLET + number - WEAPON::GUN8;
		return OBJECT::TNT;
	}
	return 0;
}
shared uint getPoweredBulletOfWeapon(uint number) {
	if (isValidWeapon(number)) {
		if (number < WEAPON::TNT)
			return OBJECT::BLASTERBULLETPU + number - WEAPON::BLASTER;
		if (number > WEAPON::TNT)
			return OBJECT::FIREBALLBULLETPU + number - WEAPON::GUN8;
		return OBJECT::TNT;
	}
	return 0;
}
shared uint getAmmoPickupOfWeapon(uint number) {
	if (isValidWeapon(number) && number != WEAPON::BLASTER) {
		if (number == WEAPON::BOUNCER)
			return OBJECT::BOUNCERAMMO3;
		if (number == WEAPON::ICE)
			return OBJECT::ICEAMMO3;
		return OBJECT::SEEKERAMMO3 + number - WEAPON::SEEKER;
	}
	return 0;
}
shared uint getAmmoCrateOfWeapon(uint number) {
	if (number > WEAPON::BLASTER && number < WEAPON::TNT) {
		if (number == WEAPON::BOUNCER)
			return OBJECT::BOUNCERAMMO15;
		if (number == WEAPON::ICE)
			return OBJECT::ICEAMMO15;
		return OBJECT::SEEKERAMMO15 + number - WEAPON::SEEKER;
	}
	return 0;
}
shared uint getPowerupMonitorOfWeapon(uint number) {
	if (isValidWeapon(number)) {
		if (number < WEAPON::TNT)
			return OBJECT::BLASTERPOWERUP + number - WEAPON::BLASTER;
		return OBJECT::TNTPOWERUP + number - WEAPON::TNT;
	}
	return 0;
}
shared class DefaultPacketConstructor {
	private ::jjSTREAM m_result;
	DefaultPacketConstructor(const ::jjSTREAM &in result) {
		m_result = result;
	}
	::jjSTREAM opCall() const {
		return m_result;
	}
}
shared class DefaultWeaponHook : WeaponHook {
	private ::array<::array<const ::jjANIMATION@>> m_anims(9, ::array<const ::jjANIMATION@>(2));
	private ::array<UserData@> m_userData(9);
	private ::array<MainCallback@> m_mainCallbacks(9);
	private ::array<PlayerCallback@> m_playerCallbacks(9), m_playerInputCallbacks(9);
	private ::array<DrawingCallback@> m_drawingCallbacks(9);
	private ::array<PacketCallback@> m_packetCallbacks;
	private bool m_usePacketID;
	private uint8 m_packetID;
	DefaultWeaponHook() {
		m_usePacketID = false;
	}
	DefaultWeaponHook(uint8 packetID) {
		m_packetID = packetID;
		m_usePacketID = true;
	}
	void setUserData(uint number, UserData@ data) override {
		if (isValidWeapon(number))
			@m_userData[number - 1] = @data;
	}
	UserData@ getUserData(uint number) const override {
		return isValidWeapon(number) ? @m_userData[number - 1] : null;
	}
	void setWeaponSprite(uint number, bool powerup, const ::jjANIMATION@ anim) override {
		if (isValidWeapon(number))
			@m_anims[number - 1][powerup ? 1 : 0] = @anim;
	}
	void resetCallbacks(uint number) override {
		if (isValidWeapon(number)) {
			number--;
			@m_mainCallbacks[number] = null;
			@m_playerCallbacks[number] = null;
			@m_playerInputCallbacks[number] = null;
			@m_drawingCallbacks[number] = null;
		}
	}
	void setMainCallback(uint number, MainCallback@ callback) override {
		if (isValidWeapon(number))
			@m_mainCallbacks[number - 1] = @callback;
	}
	void setPlayerCallback(uint number, PlayerCallback@ callback) override {
		if (isValidWeapon(number))
			@m_playerCallbacks[number - 1] = @callback;
	}
	void setPlayerInputCallback(uint number, PlayerCallback@ callback) override {
		if (isValidWeapon(number))
			@m_playerInputCallbacks[number - 1] = @callback;
	}
	void setDrawingCallback(uint number, DrawingCallback@ callback) override {
		if (isValidWeapon(number))
			@m_drawingCallbacks[number - 1] = @callback;
	}
	PacketConstructor@ addPacketCallback(PacketCallback@ callback) override {
		::jjSTREAM packet;
		uint index = m_packetCallbacks.length();
		m_packetCallbacks.insertLast(@callback);
		if (m_usePacketID)
			packet.push(m_packetID);
		if (index >= 0x80) {
			packet.push(uint8(index >> 24) | 0x80);
			packet.push(uint8(index >> 16));
			packet.push(uint8(index >> 8));
		}
		packet.push(uint8(index));
		return @PacketConstructor(DefaultPacketConstructor(packet).opCall);
	}
	protected void drawAmmoSprite(::jjCANVAS@ canvas, int x, int y, const ::jjANIMATION@ anim) const {
		canvas.drawSpriteFromCurFrame(x, y, anim + (::jjGameTicks >> 2) % anim.frameCount);
	}
	bool drawAmmo(::jjPLAYER@ player, ::jjCANVAS@ canvas) override {
		if (player !is null) {
			uint number = player.currWeapon;
			if (isValidWeapon(number)) {
				const ::jjANIMATION@ anim = @m_anims[number - 1][player.powerup[number] ? 1 : 0];
				DrawingCallback@ callback = @m_drawingCallbacks[number - 1];
				if (callback !is null && callback(@player, @canvas, @anim))
					return true;
				if (anim !is null) {
					if (canvas !is null && !player.noFire && player.charCurr != CHAR::FROG && player.charCurr != CHAR::BIRD) {
						int x, y;
						STRING::Size font;
						::string text;
						const ::jjWEAPON@ weapon = ::jjWeapons[number];
						if (::jjSubscreenWidth > 400) {
							x = ::jjSubscreenWidth - 88;
							y = ::jjSubscreenHeight - 14;
							font = STRING::MEDIUM;
						} else {
							x = ::jjSubscreenWidth - 48;
							y = ::jjSubscreenHeight - 9;
							font = STRING::SMALL;
						}
						if (weapon.infinite || weapon.replacedByShield && player.shieldTime > 0) {
							text = "x^";
						} else {
							int ammo = player.ammo[number];
							text = "x" + (ammo > 0 ? ammo : 0);
						}
						drawAmmoSprite(@canvas, x, y, @anim);
						canvas.drawString(x + 8, y, text, font);
					}
					return true;
				}
			}
		}
		return false;
	}
	void processMain() override {
		for (int i = 0, size = m_mainCallbacks.length(); i < size; i++) {
			if (m_mainCallbacks[i] !is null)
				m_mainCallbacks[i](i + 1);
		}
	}
	void processPlayer(::jjPLAYER@ player) override {
		for (int i = 0, size = m_playerCallbacks.length(); i < size; i++) {
			if (m_playerCallbacks[i] !is null)
				m_playerCallbacks[i](@player, i + 1);
		}
	}
	void processPlayerInput(::jjPLAYER@ player) override {
		for (int i = 0, size = m_playerInputCallbacks.length(); i < size; i++) {
			if (m_playerInputCallbacks[i] !is null)
				m_playerInputCallbacks[i](@player, i + 1);
		}
	}
	bool processPacket(::jjSTREAM &in packet, int clientID) override {
		uint8 id, index;
		if ((!m_usePacketID || packet.pop(id) && id == m_packetID) && packet.pop(index)) {
			uint fullIndex;
			if (index & 0x80 != 0) {
				index &= ~0x80;
				uint8 u1, u2, u3;
				if (packet.pop(u1) && packet.pop(u2) && packet.pop(u3))
					fullIndex = uint(index) << 24 | uint(u1) << 16 | uint(u2) << 8 | u3;
				else
					fullIndex = m_packetCallbacks.length();
			} else {
				fullIndex = index;
			}
			if (fullIndex < m_packetCallbacks.length()) {
				PacketCallback@ callback = @m_packetCallbacks[fullIndex];
				if (callback !is null)
					callback(packet, clientID);
				return true;
			}
		}
		return false;
	}
}
shared class AmmoPickup : ::jjBEHAVIORINTERFACE {
	private const ::jjANIMATION@ m_basic, m_powered;
	private uint m_count;
	private SOUND::Sample m_sample;
	AmmoPickup(const ::jjANIMATION@ basic, const ::jjANIMATION@ powered, uint count = 3, SOUND::Sample sample = SOUND::COMMON_PICKUPW1) {
		@m_basic = @basic;
		@m_powered = @powered;
		m_count = count;
		m_sample = sample;
	}
	protected void draw(float x, float y, uint sprite, int direction, bool translucent, int playerID) const {
		::jjDrawSpriteFromCurFrame(x, y, sprite, direction, translucent ? SPRITE::TRANSLUCENT : SPRITE::NORMAL, 0, 4, 4, playerID);
	}
	protected float getY(const ::jjOBJ@ obj) const {
		int arg = (((obj.objectID << 3) + ::jjGameTicks) << (obj.yPos > ::jjWaterLevel ? 1 : 4)) + (int(obj.xPos) << 4);
		return obj.yPos + ::jjSin(arg) * 4.f;
	}
	void setAnimationBasic(const ::jjANIMATION@ basic) {
		@m_basic = @basic;
	}
	const ::jjANIMATION@ getAnimationBasic() const {
		return @m_basic;
	}
	void setAnimationPowered(const ::jjANIMATION@ powered) {
		@m_powered = @powered;
	}
	const ::jjANIMATION@ getAnimationPowered() const {
		return @m_powered;
	}
	void setCount(uint count) {
		m_count = count;
	}
	uint getCount() const {
		return m_count;
	}
	void setSample(SOUND::Sample sample) {
		m_sample = sample;
	}
	SOUND::Sample getSample() const {
		return m_sample;
	}
	void onBehave(::jjOBJ@ obj) override {
		if (obj.state == STATE::START)
			obj.scriptedCollisions = true;
		obj.behave(BEHAVIOR::PICKUP, false);
		for (int i = 0; i < ::jjLocalPlayerCount; i++) {
			const ::jjPLAYER@ player = @::jjLocalPlayers[i];
			const ::jjANIMATION@ anim = @(player.powerup[obj.var[3] + 1] ? @m_powered : @m_basic);
			uint sprite = anim + obj.frameID % anim.frameCount;
			draw(obj.xPos, getY(obj), sprite, obj.direction, ::jjAnimFrames[sprite].transparent, player.playerID);
		}
	}
	bool onObjectHit(::jjOBJ@ obj, ::jjOBJ@ bullet, ::jjPLAYER@ player, int) {
		if (bullet is null) {
			bool sp = ::jjGameMode == GAME::SP || ::jjGameMode == GAME::COOP;
			int type = obj.var[3] + 1;
			int multiplier = ::jjWeapons[type].multiplier;
			::jjWeapons[type].multiplier = 1;
			if (::jjAutoWeaponChange && player.ammo[type] == 0 && (player.shieldType == 0 || !::jjWeapons[player.currWeapon].replacedByShield) && m_count != 0)
				player.currWeapon = type;
			int maximum = ::jjWeapons[type].maximum;
			if (maximum == -1)
				maximum = sp ? 99 : 50;
			maximum *= multiplier;
			if (player.ammo[type] >= maximum) {
				player.ammo[type] = maximum;
			} else {
				player.ammo[type] = player.ammo[type] + m_count * multiplier;
				if (player.ammo[type] >= maximum)
					player.ammo[type] = maximum;
				obj.frameID = 0;
				obj.behavior = BEHAVIOR::EXPLOSION2;
				::jjSample(obj.xPos, obj.yPos, m_sample);
				if (sp && obj.points != 0) {
					player.score += obj.points;
					::jjPARTICLE@ part = ::jjAddParticle(PARTICLE::STRING);
					if (part !is null) {
						part.xPos = obj.xPos;
						part.yPos = obj.yPos;
						part.xSpeed = -0.5f - (jjRandom() & 0x3FFF) / 65536.f;
						part.ySpeed = -1.f - (jjRandom() & 0x7FFF) / 65536.f;
						part.string.text = ::formatInt(obj.points);
					}
					obj.points = 0;
				}
			}
			::jjWeapons[type].multiplier = multiplier;
			return true;
		}
		return false;
	}
}
}
