import { ObservablePoint } from "../geometries/observablePoint.ts";
import { Rect } from "./../geometries/rectangle.ts";
import { releaseAllPointerEvents } from "./../input/input.ts";
import { colorPool } from "./../math/color.ts";
import { clamp } from "./../math/math.ts";
import { Matrix3d } from "../math/matrix3d.ts";
import { ObservableVector3d } from "../math/observableVector3d.ts";
import { vector3dPool } from "../math/vector3d.ts";
import { boundsPool } from "./../physics/bounds.ts";
import Body from "./../physics/builtin/body.js";
import pool from "../system/legacy_pool.js";

/**
 * additional import for TypeScript
 * @import {Matrix3d} from "../math/matrix3d.ts";
 * @import Entity from "./entity/entity.js";
 * @import Container from "./container.js";
 * @import {Line} from "./../geometries/line.ts";
 * @import {Ellipse} from "./../geometries/ellipse.ts";
 * @import {Polygon} from "../geometries/polygon.ts";
 * @import {RoundRect} from "./../geometries/roundrect.ts";
 * @import Application from "./../application/application.ts";
 * @import CanvasRenderer from "./../video/canvas/canvas_renderer.js";
 * @import WebGLRenderer from "./../video/webgl/webgl_renderer.js";
 * @import ResponseObject from "./../physics/response.js";
 **/

/**
 * A base class for renderable objects.
 * @category Game Objects
 */
export default class Renderable extends Rect {
	/**
	 * @param {number} x - position of the renderable object (accessible through inherited pos.x property)
	 * @param {number} y - position of the renderable object (accessible through inherited pos.y property)
	 * @param {number} width - object width
	 * @param {number} height - object height
	 */
	constructor(x, y, width, height) {
		// parent constructor
		super(x, y, width, height);

		/**
		 * Position of the Renderable relative to its parent container
		 * @public
		 * @type {ObservableVector3d}
		 */
		this.pos = new ObservableVector3d(x, y, 0, () => {
			this.updateBounds();
			this.isDirty = true;
		});

		/**
		 * The anchor point is used for attachment behavior, and/or when applying transformations.<br>
		 * The coordinate system places the origin at the top left corner of the frame (0, 0) and (1, 1) means the bottom-right corner<br>
		 * <img src="images/anchor_point.png"/><br>
		 * a Renderable's anchor point defaults to (0.5,0.5), which corresponds to the center position.<br>
		 * <br>
		 * <i><b>Note:</b> Object created through Tiled will have their anchorPoint set to (0, 0) to match Tiled Level editor implementation.
		 * To specify a value through Tiled, use a json expression like `json:{"x":0.5,"y":0.5}`. </i>
		 * @type {ObservablePoint}
		 * @default <0.5,0.5>
		 */
		this.anchorPoint = new ObservablePoint(0.5, 0.5, () => {
			this.updateBounds();
			this.isDirty = true;
		});

		if (typeof this.currentTransform === "undefined") {
			/**
			 * the renderable transformation matrix (4x4).
			 * For standard 2D use, only the 2D components are used (rotate around Z, scale X/Y, translate X/Y).
			 * For 3D use (e.g. Mesh), the full 4x4 matrix supports rotation around any axis,
			 * 3D translation, and perspective projection.
			 * Use the `rotate()`, `scale()`, and `translate()` methods rather than modifying this directly.
			 * @type {Matrix3d}
			 */
			this.currentTransform = new Matrix3d();
		}
		this.currentTransform.identity();

		/**
		 * the renderable physics body — the handle returned by the
		 * active {@link PhysicsAdapter}'s `addBody` (or constructed
		 * imperatively via `new Body(...)`). Typed as the portable
		 * {@link PhysicsBody} interface; cast to the adapter-specific
		 * concrete type (`MatterAdapter.Body`, `BuiltinAdapter.Body`, or
		 * the legacy {@link Body} class) to reach native fields.
		 * @type {PhysicsBody}
		 * @example
		 *  // define a new Player Class
		 *  class PlayerEntity extends me.Sprite {
		 *      // constructor
		 *      constructor(x, y, settings) {
		 *          // call the parent constructor
		 *          super(x, y , settings);
		 *
		 *          // define a basic walking animation
		 *          this.addAnimation("walk",  [...]);
		 *          // define a standing animation (using the first frame)
		 *          this.addAnimation("stand",  [...]);
		 *          // set the standing animation as default
		 *          this.setCurrentAnimation("stand");
		 *
		 *          // add a physic body
		 *          this.body = new me.Body(this);
		 *          // add a default collision shape
		 *          this.body.addShape(new me.Rect(0, 0, this.width, this.height));
		 *          // configure max speed, friction, and initial force to be applied
		 *          this.body.setMaxVelocity(3, 15);
		 *          this.body.setFriction(0.4, 0);
		 *          this.body.force.set(3, 0);
		 *          this.isKinematic = false;
		 *
		 *          // set the display to follow our position on both axis
		 *          app.viewport.follow(this.pos, app.viewport.AXIS.BOTH);
		 *      }
		 *
		 *      ...
		 *
		 * }
		 */
		this.body = undefined;

		/**
		 * Declarative body definition consumed by the active
		 * {@link PhysicsAdapter} when this renderable is added to a
		 * container. **Adapter API only** — leave `undefined` if you
		 * build a body imperatively via `this.body = new Body(...)`.
		 *
		 * When set, the parent container forwards it to
		 * `world.adapter.addBody(this, this.bodyDef)`, which constructs
		 * the underlying physics body (matter, builtin SAT, …) and
		 * assigns the engine-portable wrapper to `this.body`. This is
		 * the engine-portable path: the same `bodyDef` produces an
		 * equivalent body under any adapter.
		 *
		 * Typical fields: `type` (`"static"`/`"dynamic"`/`"kinematic"`),
		 * `shapes`, `collisionType`, `collisionMask`, `restitution`,
		 * `frictionAir`, `density`, `gravityScale`, `isSensor`,
		 * `maxVelocity`, `fixedRotation`. See {@link BodyDefinition}.
		 * @type {object|undefined}
		 * @default undefined
		 */
		this.bodyDef = undefined;

		/**
		 * (G)ame (U)nique (Id)entifier" <br>
		 * a GUID will be allocated for any renderable object added <br>
		 * to an object container (including the `app.world` container)
		 * @type {string}
		 */
		this.GUID = undefined;

		/**
		 * an event handler that is called when the renderable leave or enter a camera viewport
		 * @type {Function}
		 * @default undefined
		 * @example
		 * this.onVisibilityChange = function(inViewport) {
		 *     if (inViewport === true) {
		 *         console.log("object has entered the in a camera viewport!");
		 *     }
		 * };
		 */
		this.onVisibilityChange = undefined;

		/**
		 * Whether the renderable object will always update, even when outside of the viewport<br>
		 * @type {boolean}
		 * @default false
		 */
		this.alwaysUpdate = false;

		/**
		 * Whether to update this object when the game is paused.
		 * @type {boolean}
		 * @default false
		 */
		this.updateWhenPaused = false;

		/**
		 * make the renderable object persistent over level changes<br>
		 * @type {boolean}
		 * @default false
		 */
		this.isPersistent = false;

		/**
		 * If true, this renderable will be rendered using screen coordinates,
		 * as opposed to world coordinates. Use this, for example, to define UI elements.
		 * @type {boolean}
		 * @default false
		 */
		this.floating = false;

		/**
		 * If true, this floating renderable will be rendered by all cameras
		 * (e.g. background image layers). If false (default), floating elements
		 * are only rendered by the default camera (e.g. UI/HUD elements).
		 * Only applies to floating renderables in multi-camera setups.
		 * @type {boolean}
		 * @default false
		 */
		this.visibleInAllCameras = false;

		/**
		 * When enabled, an object container will automatically apply
		 * any defined transformation before calling the child draw method.
		 * @type {boolean}
		 * @default true
		 * @example
		 * // enable "automatic" transformation when the object is activated
		 * onActivateEvent: function () {
		 *     // reset the transformation matrix
		 *     this.currentTransform.identity();
		 *     // ensure the anchor point is the renderable center
		 *     this.anchorPoint.set(0.5, 0.5);
		 *     // enable auto transform
		 *     this.autoTransform = true;
		 *     ....
		 * }
		 */
		this.autoTransform = true;

		/**
		 * Define the renderable opacity<br>
		 * Set to zero if you do not wish an object to be drawn
		 * @see Renderable#setOpacity
		 * @see Renderable#getOpacity
		 * @type {number}
		 * @default 1.0
		 */
		this.alpha = 1.0;

		/**
		 * a reference to the parent object that contains this renderable
		 * @type {Container|Entity}
		 * @default undefined
		 */
		this.ancestor = undefined;

		/**
		 * A mask limits rendering elements to the shape and position of the given mask object.
		 * So, if the renderable is larger than the mask, only the intersecting part of the renderable will be visible.
		 * @type {Rect|RoundRect|Polygon|Line|Ellipse}
		 * @default undefined
		 * @example
		 * // apply a mask in the shape of a Star
		 * myNPCSprite.mask = new me.Polygon(myNPCSprite.width / 2, 0, [
		 *    // draw a star
		 *    {x: 0, y: 0},
		 *    {x: 14, y: 30},
		 *    {x: 47, y: 35},
		 *    {x: 23, y: 57},
		 *    {x: 44, y: 90},
		 *    {x: 0, y: 62},
		 *    {x: -44, y: 90},
		 *    {x: -23, y: 57},
		 *    {x: -47, y: 35},
		 *    {x: -14, y: 30}
		 * ]);
		 */
		this.mask = undefined;

		/**
		 * the list of post-processing shader effects applied to this renderable (WebGL only).
		 * Effects are applied in order. Use {@link addPostEffect}, {@link getPostEffect},
		 * and {@link removePostEffect} to manage effects, or assign directly.
		 * In Canvas mode, this property is ignored.
		 * @type {Array<GLShader|ShaderEffect>}
		 * @default []
		 * @example
		 * // add effects via helper methods
		 * mySprite.addPostEffect(new DesaturateEffect(renderer));
		 * mySprite.addPostEffect(new VignetteEffect(renderer));
		 * @example
		 * // assign directly
		 * mySprite.postEffects = [new SepiaEffect(renderer), new VignetteEffect(renderer)];
		 */
		this.postEffects = [];

		/**
		 * whether this renderable manages its own FBO lifecycle (e.g. Camera2d)
		 * @ignore
		 */
		this._postEffectManaged = false;

		/**
		 * the blend mode to be applied to this renderable (see renderer setBlendMode for available blend mode)
		 * @type {string}
		 * @default "normal"
		 * @see CanvasRenderer#setBlendMode
		 * @see WebGLRenderer#setBlendMode
		 */
		this.blendMode = "normal";

		/**
		 * The name of the renderable
		 * @type {string}
		 * @default ""
		 */
		this.name = "";

		/**
		 * to identify the object as a renderable object
		 * @ignore
		 */
		this.isRenderable = true;

		/**
		 * If true then physic collision and input events will not impact this renderable
		 * @type {boolean}
		 * @default true
		 */
		this.isKinematic = true;

		/**
		 * when true the renderable will be redrawn during the next update cycle
		 * @type {boolean}
		 * @default true
		 */
		this.isDirty = true;

		/**
		 * cache the absolute position of the renderable
		 * @ignore
		 */
		this._absPos = undefined;

		/**
		 * keep track of when we flip
		 * @ignore
		 */
		this._flip = {
			x: false,
			y: false,
		};

		/**
		 * viewport flag
		 * @ignore
		 */
		this._inViewport = false;

		/**
		 * cache value for the parentApp
		 * @ignore
		 */
		this._parentApp = undefined;

		/**
		 * renderable cache tint value used by the getter/setter
		 * @ignore
		 */
		this._tint = colorPool.get(255, 255, 255, 1.0);

		// ensure it's fully opaque by default
		this.setOpacity(1.0);
	}

	/**
	 * returns the parent application (or game) to which this renderable is attached to
	 * @return {Application} the parent application or undefined if not attached to any container/app
	 */
	get parentApp() {
		if (!this._parentApp && this.ancestor) {
			// the `app` property is only defined in the world "root" container
			this._parentApp = this.ancestor.getRootAncestor().app;
		}
		return this._parentApp;
	}

	/**
	 * Whether the renderable object is floating (i.e. used screen coordinates), or contained in a floating parent container
	 * @see Renderable#floating
	 * @type {boolean}
	 */
	get isFloating() {
		return (
			this.floating === true ||
			(this.ancestor && this.ancestor.isFloating === true)
		);
	}

	/**
	 * define a tint for this renderable. a (255, 255, 255) r, g, b value will remove the tint effect.
	 * @type {Color}
	 * @default (255, 255, 255)
	 * @example
	 * // add a red tint to this renderable
	 * this.tint.setColor(255, 128, 128);
	 * // remove the tint
	 * this.tint.setColor(255, 255, 255);
	 */
	get tint() {
		return this._tint;
	}
	set tint(value) {
		this._tint.copy(value);
		this.isDirty = true;
	}

	/**
	 * the depth of this renderable on the z axis
	 * @type {number}
	 */
	get depth() {
		return this.pos.z;
	}
	set depth(value) {
		this.pos.z = value;
		this.isDirty = true;
	}

	/**
	 * Whether the renderable object is visible and within the viewport
	 * @type {boolean}
	 * @default false
	 */
	get inViewport() {
		return this._inViewport;
	}
	set inViewport(value) {
		if (this._inViewport !== value) {
			this._inViewport = value;
			if (typeof this.onVisibilityChange === "function") {
				this.onVisibilityChange.call(this, value);
			}
		}
	}

	/**
	 * @deprecated since 19.2.0 — use {@link addPostEffect} / {@link getPostEffect} / {@link removePostEffect} instead
	 * @type {GLShader|ShaderEffect|undefined}
	 */
	get shader() {
		return this.postEffects[0];
	}

	set shader(value) {
		// destroy existing effects before replacing
		for (const effect of this.postEffects) {
			if (typeof effect.destroy === "function") {
				effect.destroy();
			}
		}
		if (typeof value === "undefined") {
			this.postEffects.length = 0;
		} else {
			this.postEffects = [value];
		}
	}

	/**
	 * Add a post-processing shader effect to this renderable.
	 * @param {GLShader|ShaderEffect} effect - the effect to add
	 * @returns {GLShader|ShaderEffect} the added effect
	 * @example
	 * mySprite.addPostEffect(new DesaturateEffect(renderer));
	 */
	addPostEffect(effect) {
		this.postEffects.push(effect);
		return effect;
	}

	/**
	 * Get post-processing shader effects.
	 * When called with a class, returns the first effect matching the given class.
	 * When called without arguments, returns the full effects array.
	 * @param {Function} [effectClass] - the effect class to search for
	 * @returns {GLShader|ShaderEffect|Array|undefined} the matching effect, the effects array, or undefined
	 * @example
	 * const desat = sprite.getPostEffect(DesaturateEffect);
	 * const allEffects = sprite.getPostEffect();
	 */
	getPostEffect(effectClass) {
		if (typeof effectClass === "undefined") {
			return this.postEffects;
		}
		return this.postEffects.find((fx) => {
			return fx instanceof effectClass;
		});
	}

	/**
	 * Remove all post-processing shader effects.
	 * @example
	 * sprite.clearPostEffects();
	 */
	clearPostEffects() {
		for (const effect of this.postEffects) {
			if (typeof effect.destroy === "function") {
				effect.destroy();
			}
		}
		this.postEffects.length = 0;
	}

	/**
	 * Remove a specific post-processing shader effect.
	 * @param {GLShader|ShaderEffect} effect - the effect to remove
	 * @example
	 * sprite.removePostEffect(effect);
	 */
	removePostEffect(effect) {
		const idx = this.postEffects.indexOf(effect);
		if (idx !== -1) {
			this.postEffects.splice(idx, 1);
			if (typeof effect.destroy === "function") {
				effect.destroy();
			}
		}
	}

	/**
	 * returns true if this renderable is flipped on the horizontal axis
	 * @public
	 * @see Renderable#flipX
	 * @type {boolean}
	 */
	get isFlippedX() {
		return this._flip.x === true;
	}

	/**
	 * returns true if this renderable is flipped on the vertical axis
	 * @public
	 * @see Renderable#flipY
	 * @type {boolean}
	 */
	get isFlippedY() {
		return this._flip.y === true;
	}

	/**
	 * returns the bounding box for this renderable
	 * @returns {Bounds} bounding box Rectangle object
	 */
	getBounds() {
		if (typeof this._bounds === "undefined") {
			super.getBounds();
			if (this.isFinite()) {
				this.updateBounds();
			} else {
				// e.g. containers or game world can have infinite size
				this._bounds.setMinMax(this.pos.x, this.pos.y, this.width, this.height);
			}
		}
		return this._bounds;
	}

	/**
	 * get the renderable alpha channel value<br>
	 * @returns {number} current opacity value between 0 and 1
	 */
	getOpacity() {
		return this.alpha;
	}

	/**
	 * set the renderable alpha channel value<br>
	 * @param {number} alpha - opacity value between 0.0 and 1.0
	 */
	setOpacity(alpha) {
		if (typeof alpha === "number") {
			this.alpha = clamp(alpha, 0.0, 1.0);
			// Set to 1 if alpha is NaN
			if (isNaN(this.alpha)) {
				this.alpha = 1.0;
			}
			this.isDirty = true;
		}
	}

	/**
	 * flip the renderable on the horizontal axis (around the center of the renderable)
	 * @see Matrix3d#scaleX
	 * @param {boolean} [flip=true] - `true` to flip this renderable.
	 * @returns {Renderable} Reference to this object for method chaining
	 */
	flipX(flip = true) {
		this._flip.x = !!flip;
		this.isDirty = true;
		return this;
	}

	/**
	 * flip the renderable on the vertical axis (around the center of the renderable)
	 * @see Matrix3d#scaleY
	 * @param {boolean} [flip=true] - `true` to flip this renderable.
	 * @returns {Renderable} Reference to this object for method chaining
	 */
	flipY(flip = true) {
		this._flip.y = !!flip;
		this.isDirty = true;
		return this;
	}

	/**
	 * multiply the renderable currentTransform with the given matrix
	 * @see Renderable#currentTransform
	 * @param {Matrix2d|Matrix3d} m - the transformation matrix
	 * @returns {Renderable} Reference to this object for method chaining
	 */
	transform(m) {
		this.currentTransform.multiply(m);
		this.updateBounds();
		this.isDirty = true;
		return this;
	}

	/**
	 * return the angle to the specified target
	 * @param {Renderable|Vector2d|Vector3d} target
	 * @returns {number} angle in radians
	 */
	angleTo(target) {
		const a = this.getBounds();
		let ax;
		let ay;

		if (target instanceof Renderable) {
			const b = target.getBounds();
			ax = b.centerX - a.centerX;
			ay = b.centerY - a.centerY;
		} else {
			// vector object
			ax = target.x - a.centerX;
			ay = target.y - a.centerY;
		}

		return Math.atan2(ay, ax);
	}

	/**
	 * return the distance to the specified target
	 * @param {Renderable|Vector2d|Vector3d} target
	 * @returns {number} distance
	 */
	distanceTo(target) {
		const a = this.getBounds();
		let dx;
		let dy;

		if (target instanceof Renderable) {
			const b = target.getBounds();
			dx = a.centerX - b.centerX;
			dy = a.centerY - b.centerY;
		} else {
			// vector object
			dx = a.centerX - target.x;
			dy = a.centerY - target.y;
		}

		return Math.sqrt(dx * dx + dy * dy);
	}

	/**
	 * Rotate this renderable towards the given target.
	 * @param {Renderable|Vector2d|Vector3d} target - the renderable or position to look at
	 * @returns {Renderable} Reference to this object for method chaining
	 */
	lookAt(target) {
		let position;

		if (target instanceof Renderable) {
			position = target.pos;
		} else {
			position = target;
		}

		const angle = this.angleTo(position);

		this.rotate(angle);

		return this;
	}

	/**
	 * Rotate this renderable by the specified angle (in radians).
	 * When called with just an angle, rotates around the Z axis (2D rotation).
	 * When called with an angle and a Vector3d axis, rotates around that axis in 3D.
	 * @param {number} angle - The angle to rotate (in radians)
	 * @param {Vector3d} [v] - the axis to rotate around (defaults to Z axis for 2D)
	 * @returns {Renderable} Reference to this object for method chaining
	 */
	rotate(angle, v) {
		if (angle !== 0) {
			this.currentTransform.rotate(angle, v);
			this.updateBounds();
			this.isDirty = true;
		}
		return this;
	}

	/**
	 * scale the renderable around his anchor point.  Scaling actually applies changes
	 * to the currentTransform member which is used by the renderer to scale the object
	 * when rendering.  It does not scale the object itself.  For example if the renderable
	 * is an image, the image.width and image.height properties are unaltered but the currentTransform
	 * member will be changed.
	 * @param {number} x - a number representing the abscissa of the scaling vector.
	 * @param {number} [y=x] - a number representing the ordinate of the scaling vector.
	 * @param {number} [z=1] - a number representing the depth of the scaling vector.
	 * @returns {Renderable} Reference to this object for method chaining
	 */
	scale(x, y = x, z = 1) {
		this.currentTransform.scale(x, y, z);
		this.updateBounds();
		this.isDirty = true;
		return this;
	}

	/**
	 * scale the renderable around his anchor point
	 * @param {Vector2d} v - scaling vector
	 * @returns {Renderable} Reference to this object for method chaining
	 */
	scaleV(v) {
		this.scale(v.x, v.y);
		return this;
	}

	/**
	 * Translate the renderable by the specified offset.
	 * @param {number} x - x offset
	 * @param {number} [y=0] - y offset
	 * @param {number} [z=0] - z offset
	 * @returns {Renderable} Reference to this object for method chaining
	 */
	translate(x, y = 0, z = 0) {
		this.currentTransform.translate(x, y, z);
		this.updateBounds();
		this.isDirty = true;
		return this;
	}

	/**
	 * update function (automatically called by melonJS).
	 * @param {number} dt - time since the last update in milliseconds.
	 * @returns {boolean} true if the renderable is dirty
	 */
	// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
	update(dt) {
		return this.isDirty;
	}

	/**
	 * update the bounding box for this shape.
	 * @param {boolean} [absolute=true] - update the bounds size and position in (world) absolute coordinates
	 * @returns {Bounds} this shape bounding box Rectangle object
	 */
	updateBounds(absolute = true) {
		if (this.isRenderable) {
			const bounds = this.getBounds();

			bounds.clear();
			if (this.autoTransform === true && !this.currentTransform.isIdentity()) {
				// temporarily translate the matrix based on the anchor point
				this.currentTransform.translate(
					-this.width * this.anchorPoint.x,
					-this.height * this.anchorPoint.y,
				);
				bounds.addFrame(0, 0, this.width, this.height, this.currentTransform);
				this.currentTransform.translate(
					this.width * this.anchorPoint.x,
					this.height * this.anchorPoint.y,
				);
			} else {
				bounds.addFrame(0, 0, this.width, this.height);
				// translate the bounds based on the anchor point
				bounds.translate(
					-this.width * this.anchorPoint.x,
					-this.height * this.anchorPoint.y,
				);
			}

			if (absolute === true) {
				const absPos = this.getAbsolutePosition();
				bounds.centerOn(
					absPos.x + bounds.x + bounds.width / 2,
					absPos.y + bounds.y + bounds.height / 2,
				);
			}
			return bounds;
		} else {
			// manage the case where updateBounds is called
			// before the object being yet properly initialized
			return super.updateBounds(absolute);
		}
	}

	/**
	 * return the renderable absolute position in the game world. The
	 * returned vector is a {@link Vector3d} so the z component is summed
	 * across the ancestor chain too — important for {@link Camera3d}'s
	 * frustum culling, which previously read `obj.depth` (local
	 * `pos.z`) and mis-culled children nested under a container with
	 * its own non-zero depth.
	 * @returns {Vector3d}
	 */
	getAbsolutePosition() {
		if (typeof this._absPos === "undefined") {
			this._absPos = vector3dPool.get();
		}
		// TODO: cache the absolute position and invalidate when pos or ancestor changes
		this._absPos.set(this.pos.x, this.pos.y, this.pos.z);
		if (this.ancestor && this.floating !== true) {
			this._absPos.add(this.ancestor.getAbsolutePosition());
		}
		return this._absPos;
	}

	/**
	 * Prepare the rendering context before drawing (automatically called by melonJS).
	 * This will apply any defined transforms, anchor point, tint or blend mode and translate the context accordingly to this renderable position.
	 * @see Renderable#draw
	 * @see Renderable#postDraw
	 * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer object
	 */
	preDraw(renderer) {
		const ax = this.width * this.anchorPoint.x;
		const ay = this.height * this.anchorPoint.y;

		// save renderer context
		renderer.save();

		// apply the defined alpha value
		renderer.setGlobalAlpha(renderer.globalAlpha() * this.getOpacity());

		// apply flip
		if (this._flip.x || this._flip.y) {
			const dx = this._flip.x ? this.centerX - ax : 0;
			const dy = this._flip.y ? this.centerY - ay : 0;

			renderer.translate(dx, dy);
			renderer.scale(this._flip.x ? -1 : 1, this._flip.y ? -1 : 1);
			renderer.translate(-dx, -dy);
		}

		// apply stencil mask if defined
		if (this.mask) {
			renderer.translate(this.pos.x, this.pos.y);
			renderer.setMask(this.mask);
			renderer.translate(-this.pos.x, -this.pos.y);
		}

		// delegate post-effect setup to the renderer.
		// Camera manages its own FBO lifecycle in draw().
		if (!this._postEffectManaged) {
			renderer.beginPostEffect(this);
		}

		if (this.autoTransform === true && !this.currentTransform.isIdentity()) {
			// apply the renderable transformation matrix
			renderer.translate(this.pos.x, this.pos.y);
			renderer.transform(this.currentTransform);
			renderer.translate(-this.pos.x, -this.pos.y);
		}

		// offset by the anchor point
		renderer.translate(-ax, -ay);

		// apply the current tint and opacity
		renderer.setTint(this.tint, this.getOpacity());

		// forward depth to the renderer — pushed into the WebGL vertex
		// stream as the `z` component by the batchers. No visible effect
		// under the default ortho projection; required for perspective
		// (Camera3d) to scale and parallax this renderable by distance.
		renderer.setDepth(this.depth);

		// apply blending if different from "normal"
		if (this.blendMode !== renderer.getBlendMode()) {
			renderer.setBlendMode(this.blendMode);
		}
	}

	/**
	 * Draw this renderable (automatically called by melonJS).
	 * All draw operations for renderable are made respectively
	 * to the position or transforms set or applied by the preDraw method.
	 * The main draw loop will first call preDraw() to prepare the context for drawing the renderable,
	 * then draw() to draw the renderable, and finally postDraw() to clear the context.
	 * If you override this method, be mindful about the drawing logic; for example if you draw a shape
	 * from the draw method, you should make sure that your draw it at the 0, 0 coordinates.
	 * @see Renderable#preDraw
	 * @see Renderable#postDraw
	 * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer instance
	 * @param {Camera2d} [viewport] - the viewport to (re)draw
	 */
	// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
	draw(renderer, viewport) {
		// empty one !
	}

	/**
	 * restore the rendering context after drawing (automatically called by melonJS).
	 * @see Renderable#preDraw
	 * @see Renderable#draw
	 * @param {CanvasRenderer|WebGLRenderer} renderer - a renderer object
	 */
	postDraw(renderer) {
		// remove the previously applied tint
		renderer.clearTint();

		// clear the mask if set
		if (this.mask) {
			renderer.clearMask();
		}

		// finalize post-effects (renderer decides what to do)
		if (!this._postEffectManaged) {
			renderer.endPostEffect(this);
		}

		// restore the context (also restores customShader)
		renderer.restore();

		// reset the dirty flag
		this.isDirty = false;
	}

	/**
	 * Legacy collision callback — fires every frame this renderable body is
	 * overlapping another body. Kept for backward compatibility with code
	 * written against pre-19.5 melonJS; semantics are unchanged from the
	 * 19.4 contract.
	 *
	 * **NOTE — `onCollision` is NOT equivalent to {@link Renderable.onCollisionActive}.**
	 * The two handlers exist side by side and have intentionally different
	 * contracts:
	 *
	 * | | `onCollision` (legacy) | `onCollisionActive` (modern) |
	 * |---|---|---|
	 * | Cadence for dynamic-dynamic pairs | 2× per frame per side | 1× per frame per side |
	 * | `response.a` semantics | Fixed per pair (first body in detector call) | Always the receiver (`response.a === this`) |
	 * | `response.b` semantics | Fixed per pair | Always the partner (`response.b === other`) |
	 * | `response.normal` / `response.depth` | ✗ | ✓ — `normal.y < -0.7` = "push me up" |
	 * | `return false` to skip push-out | ✓ (honored by SAT) | ✗ — use `bodyDef.isSensor` or `setSensor` instead |
	 *
	 * If you're writing new code, prefer `onCollisionActive`. Keep
	 * `onCollision` only when its every-frame, return-false, fixed-`a`/`b`
	 * semantics are what you want.
	 *
	 * @param {import("../physics/adapter.ts").CollisionResponse} response - the collision response object
	 * @param {Renderable} other - the other renderable touching this one (a reference to response.a or response.b)
	 * @returns {boolean} true if the object should respond to the collision (its position and velocity will be corrected); the return value is only honored by the builtin SAT adapter.
	 * @example
	 * // legacy collision handler — note the receiver-side check on response.a
	 * onCollision(response) {
	 *     if (response.b.body.collisionType === me.collision.types.ENEMY_OBJECT) {
	 *         this.pos.sub(response.overlapV);
	 *         this.hurt();
	 *         return false;   // skip the SAT push-out
	 *     }
	 *     return true;
	 * }
	 */
	onCollision(_response, _other) {
		// Default returns `undefined` so that bodies without a user-defined
		// `onCollision` get push-out by default (matches matter's "solver
		// resolves contacts unless `isSensor`" model). A user-defined
		// override returning `false` still opts out — that's the legacy
		// SAT idiom for one-way platforms / triggers, preserved for
		// backward compatibility. Subclasses that don't override this
		// method effectively inherit the modern default.
	}

	/*
	 * Modern collision lifecycle hooks (`onCollisionStart`,
	 * `onCollisionActive`, `onCollisionEnd`) are intentionally NOT
	 * defined on the base class. Subclasses override the ones they
	 * care about; the adapter's dispatch logic checks
	 * `typeof renderable.onCollisionX === "function"` to know whether
	 * to fire them. Defining base stubs would make every renderable
	 * "look like" it implements every hook, which would in turn make
	 * the `onCollisionActive supersedes onCollision` rule fire even
	 * for legacy-only code.
	 *
	 * Documented surface (subclasses implement these as needed):
	 *
	 *   onCollisionStart(response, other)
	 *     One-shot on first contact. Use for stomp bounces, pickups,
	 *     trigger entry. Receiver-symmetric (`response.a === this`,
	 *     `response.b === other`). Same contract on every adapter.
	 *
	 *   onCollisionActive(response, other)
	 *     Every step while two bodies remain in contact. The modern
	 *     equivalent of `onCollision`, with different semantics:
	 *       - 1× per frame per side (vs `onCollision`'s 2× for dyn-dyn)
	 *       - `response.a === this`, `response.b === other`
	 *       - `response.normal` is the MTV of the receiver
	 *       - `return false` is NOT honored (use `bodyDef.isSensor`)
	 *     When defined, `onCollision` is suppressed on the same
	 *     renderable. Stomp idiom: `response.normal.y < -0.7`.
	 *
	 *   onCollisionEnd(response, other)
	 *     One-shot when two bodies separate.
	 */

	/**
	 * Destroy function<br>
	 * @ignore
	 */
	destroy() {
		// allow recycling object properties
		this.currentTransform = undefined;

		this.anchorPoint.revoke();
		this.anchorPoint = undefined;

		this.pos = undefined;

		if (this._absPos) {
			vector3dPool.release(this._absPos);
			this._absPos = undefined;
		}

		if (this._bounds) {
			boundsPool.release(this._bounds);
			this._bounds = undefined;
		}

		this.onVisibilityChange = undefined;

		if (this.mask) {
			pool.push(this.mask);
			this.mask = undefined;
		}

		if (this._tint) {
			colorPool.release(this._tint);
			this._tint = undefined;
		}

		// cannot import and reference a Container from a Renderable, since Container extends Renderable, creating a circular dependency
		//if (this.ancestor instanceof Container || this.ancestor instanceof Entity) {
		this.ancestor = undefined;

		// cannot import and reference a Application from a Renderable, messsing up class order in the bundle
		//if (this._parentApp instanceof Application) {
		this._parentApp = undefined;
		//}

		// destroy the physic body if a builtin body object
		if (this.body instanceof Body) {
			this.body.destroy.apply(this.body, arguments);
			this.body = undefined;
		}

		// release all registered events
		releaseAllPointerEvents(this);

		// call the user defined destroy method
		this.onDestroyEvent.apply(this, arguments);

		// destroy any shader effects if not done by the user through onDestroyEvent()
		for (const effect of this.postEffects) {
			if (typeof effect.destroy === "function") {
				effect.destroy();
			}
		}
		this.postEffects.length = 0;
	}

	/**
	 * OnDestroy Notification function<br>
	 * Called by engine before deleting the object. `Stage.destroy(app)`
	 * forwards the active Application, so subclasses that wire teardown
	 * against the app object can override as `onDestroyEvent(app)`.
	 * @param {...*} _args - forwarded by `destroy(...args)`; Stage passes the Application instance
	 */
	onDestroyEvent(..._args) {
		// to be extended !
	}

	/**
	 * Lifecycle hook fired by {@link Container} when this renderable is
	 * added to a container that is part of the active scene graph.
	 * Override to wire up input handlers, register external listeners,
	 * or grab adapter references — `this.parentApp` is guaranteed to be
	 * available here. Pair with {@link Renderable#onDeactivateEvent}.
	 * @param {...*} _args - forwarded by `Container.addChild`
	 */
	onActivateEvent(..._args) {
		// to be extended !
	}

	/**
	 * Lifecycle hook fired by {@link Container} when this renderable is
	 * removed from its container or its container is itself removed.
	 * Override to release input handlers, unsubscribe from events, or
	 * drop adapter references. Pair with {@link Renderable#onActivateEvent}.
	 * @param {...*} _args - forwarded by `Container.removeChildNow`
	 */
	onDeactivateEvent(..._args) {
		// to be extended !
	}
}
