enum SpriteFlag {
    //% block="ghost"
    Ghost = sprites.Flag.Ghost,
    //% block="auto destroy"
    AutoDestroy = sprites.Flag.AutoDestroy,
    //% block="stay in screen"
    StayInScreen = sprites.Flag.StayInScreen,
    //% block="destroy on wall"
    DestroyOnWall = sprites.Flag.DestroyOnWall,
    //% block="bounce on wall"
    BounceOnWall = sprites.Flag.BounceOnWall,
    //% block="show physics"
    ShowPhysics = sprites.Flag.ShowPhysics,
    //% block="invisible"
    Invisible = sprites.Flag.Invisible,
    //% block="relative to camera"
    RelativeToCamera = sprites.Flag.RelativeToCamera,
    //% block="ghost through sprites"
    GhostThroughSprites = sprites.Flag.GhostThroughSprites,
    //% block="ghost through tiles"
    GhostThroughTiles = sprites.Flag.GhostThroughTiles,
    //% block="ghost through walls"
    GhostThroughWalls = sprites.Flag.GhostThroughWalls,
}

enum TileDirection {
    //% block="left"
    Left = 0,
    //% block="top"
    Top = 1,
    //% block="right"
    Right = 2,
    //% block="bottom"
    Bottom = 3,
    //% block="center"
    Center = 4
}

enum CollisionDirection {
    //% block="left"
    Left = 0,
    //% block="top"
    Top = 1,
    //% block="right"
    Right = 2,
    //% block="bottom"
    Bottom = 3
}

enum FlipOption {
    //% block=none
    None,
    //% block="flip x"
    FlipX,
    //% block="flip y"
    FlipY,
    //% block="flip x+y"
    FlipXY
}

enum ScaleDirection {
    //% block="vertically"
    Vertically = 0x01,
    //% block="horizontally"
    Horizontally = 0x02,
    //% block="uniformly"
    Uniformly = Vertically | Horizontally,
}

enum ScaleAnchor {
    //% block="middle"
    Middle = 0,
    //% block="top"
    Top = 0x01,
    //% block="left"
    Left = 0x02,
    //% block="right"
    Right = 0x04,
    //% block="bottom"
    Bottom = 0x08,
    //% block="top left"
    TopLeft = Top | Left,
    //% block="top right"
    TopRight = Top | Right,
    //% block="bottom left"
    BottomLeft = Bottom | Left,
    //% block="bottom right"
    BottomRight = Bottom | Right,
}

/**
 * A sprite on the screen
 **/
//% blockNamespace=sprites color="#3B6FEA" blockGap=8
class Sprite extends sprites.BaseSprite {
    _x: Fx8
    _y: Fx8
    _vx: Fx8
    _vy: Fx8
    _ax: Fx8
    _ay: Fx8
    _fx: Fx8 // friction
    _fy: Fx8 // friction
    _sx: Fx8 // scale
    _sy: Fx8 // scale
    _width: Fx8 // scaled width
    _height: Fx8 // scaled height
    _rotatedBBox: sprites.RotatedBoundingBox;

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="x" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/x#get
    get x(): number {
        return Fx.toFloat(Fx.add(this._x, Fx.div(this._width, Fx.twoFx8)));
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="x"
    //% blockCombineSetHelp=sprites/sprite/x#set
    set x(v: number) {
        this.left = v - (this.width / 2)
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="y" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/y#get
    get y(): number {
        return Fx.toFloat(Fx.add(this._y, Fx.div(this._height, Fx.twoFx8)));
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="y"
    //% blockCombineSetHelp=sprites/sprite/y#set
    set y(v: number) {
        this.top = v - (this.height / 2)
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="vx (velocity x)" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/vx#get
    get vx(): number {
        return Fx.toFloat(this._vx)
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="vx (velocity x)"
    //% blockCombineSetHelp=sprites/sprite/vx#set
    set vx(v: number) {
        this._vx = Fx8(v)
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="vy (velocity y)" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/vy#get
    get vy(): number {
        return Fx.toFloat(this._vy)
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="vy (velocity y)"
    //% blockCombineSetHelp=sprites/sprite/vy#set
    set vy(v: number) {
        this._vy = Fx8(v)
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="ax (acceleration x)" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/ax#get
    get ax(): number {
        return Fx.toFloat(this._ax)
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="ax (acceleration x)"
    //% blockCombineSetHelp=sprites/sprite/ax#set
    set ax(v: number) {
        this._ax = Fx8(v)
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="ay (acceleration y)" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/ay#get
    get ay(): number {
        return Fx.toFloat(this._ay)
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="ay (acceleration y)"
    //% blockCombineSetHelp=sprites/sprite/ay#set
    set ay(v: number) {
        this._ay = Fx8(v)
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="fx (friction x)" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/fx#get
    get fx(): number {
        return Fx.toFloat(this._fx)
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="fx (friction x)"
    //% blockCombineSetHelp=sprites/sprite/fx#set
    set fx(v: number) {
        this._fx = Fx8(Math.max(0, v))
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="fy (friction y)" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/fy#get
    get fy(): number {
        return Fx.toFloat(this._fy)
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="fy (friction y)"
    //% blockCombineSetHelp=sprites/sprite/fy#set
    set fy(v: number) {
        this._fy = Fx8(Math.max(0, v))
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="sx (scale x)" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/sx#get
    get sx(): number {
        return Fx.toFloat(this._sx);
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="sx (scale x)"
    //% blockCombineSetHelp=sprites/sprite/sx#set
    set sx(v: number) {
        const y = this.y;
        const x = this.x;
        this._sx = Fx8(Math.max(0, v));
        this.recalcSize();
        this.y = y;
        this.x = x;
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="sy (scale y)" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/sy#get
    get sy(): number {
        return Fx.toFloat(this._sy);
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="sy (scale y)"
    //% blockCombineSetHelp=sprites/sprite/sy#set
    set sy(v: number) {
        const y = this.y;
        const x = this.x;
        this._sy = Fx8(Math.max(0, v));
        this.recalcSize();
        this.y = y;
        this.x = x;
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="scale" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/scale#get
    get scale(): number {
        return Math.max(this.sx, this.sy);
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="scale"
    //% blockCombineGetHelp=sprites/sprite/scale#set
    set scale(v: number) {
        this.sx = this.sy = v;
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="rotation (radians)" callInDebugger
    get rotation(): number {
        return this._rotatedBBox ? this._rotatedBBox.rotation : 0;
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="rotation (radians)"
    set rotation(v: number) {
        const x = this.x;
        const y = this.y;
        if (!this._rotatedBBox) {
            this._rotatedBBox = new sprites.RotatedBoundingBox(this, this.width, this.height);
        }
        this._rotatedBBox.setRotation(v);
        this.recalcSize();
        this.x = x;
        this.y = y;
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="rotation (degrees)" callInDebugger
    get rotationDegrees(): number {
        return this.rotation * 180 / Math.PI;
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="rotation (degrees)"
    set rotationDegrees(v: number) {
        this.rotation = v * Math.PI / 180;
    }

    private _data: any;
    /**
     * Custom data
     */
    //%
    get data(): any {
        if (!this._data) this._data = {};
        return this._data;
    }

    set data(value: any) {
        this._data = value;
    }
    _kind: number;

    /**
     * A bitset of layer. Each bit is a layer, default is 1.
     */
    //% group="Physics"
    layer: number;

    _lastX: Fx8;
    _lastY: Fx8;

    _action: number; //Used with animation library

    /**
     * Time to live in milliseconds. The lifespan decreases by 1 on each millisecond
     * and the sprite gets destroyed when it reaches 0.
     */
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="lifespan"
    //% help=sprites/sprite/lifespan
    lifespan: number;
    private _image: Image;
    private _obstacles: sprites.Obstacle[];

    private sayEndTime: number;
    private sayRenderer: sprites.BaseSpriteSayRenderer;

    _hitbox: game.Hitbox;
    _overlappers: number[];
    _alreadyChecked: number[];
    _kindsOverlappedWith: number[];

    flags: number

    private destroyHandler: () => void;

    constructor(img: Image) {
        super(scene.SPRITE_Z);

        if (!img) {
            throw "Sprite image cannot be undefined or null";
        }

        this._x = Fx8(screen.width - img.width >> 1);
        this._y = Fx8(screen.height - img.height >> 1);
        this._lastX = this._x;
        this._lastY = this._y;
        this.vx = 0
        this.vy = 0
        this.ax = 0
        this.ay = 0
        this.fx = 0
        this.fy = 0
        this._sx = Fx.oneFx8;
        this._sy = Fx.oneFx8;
        this.flags = 0
        this.setImage(img);
        this.setKind(-1); // not a member of any type by default
        this.layer = 1; // by default, in layer 1
        this.lifespan = undefined;
        this._overlappers = [];
        this._obstacles = [];
    }

    __serialize(offset: number): Buffer {
        const buf = control.createBuffer(offset + 20);
        let k = offset;
        buf.setNumber(NumberFormat.Int16LE, k, Fx.toInt(this._x)); k += 2;
        buf.setNumber(NumberFormat.Int16LE, k, Fx.toInt(this._y)); k += 2;
        buf.setNumber(NumberFormat.Int16LE, k, Fx.toInt(this._vx)); k += 2;
        buf.setNumber(NumberFormat.Int16LE, k, Fx.toInt(this._vy)); k += 2;
        buf.setNumber(NumberFormat.Int16LE, k, Fx.toInt(this._ax)); k += 2;
        buf.setNumber(NumberFormat.Int16LE, k, Fx.toInt(this._ay)); k += 2;
        buf.setNumber(NumberFormat.Float32LE, k, Fx.toFloat(this._sx)); k += 4;
        buf.setNumber(NumberFormat.Float32LE, k, Fx.toFloat(this._sy)); k += 4;
        return buf;
    }

    /**
     * Gets the current image
     */
    //% group="Image"
    //% blockId=spriteimage block="%sprite(mySprite) image"
    //% weight=8 help=sprites/sprite/image
    //% blockCombineGetHelp=sprites/sprite/image
    get image(): Image {
        return this._image;
    }

    /**
     * Sets the image on the sprite
     */
    //% group="Image"
    //% blockId=spritesetimage block="set %sprite(mySprite) image to %img=screen_image_picker"
    //% weight=7 help=sprites/sprite/set-image
    setImage(img: Image) {
        if (!img || img === this._image) return;
        this._image = img;
        this.recalcSize();
    }

    calcDimensionalHash() {
        return this._image.revision() + Fx.toIntShifted(this._width, 8) + Fx.toIntShifted(this._height, 16) + this.rotation;
    }

    resetHitbox() {
        this._hitbox = null;
        this.setHitbox();
    }

    setHitbox() {
        if (this._hitbox) {
            this._hitbox.updateIfInvalid();
        } else {
            this._hitbox = game.calculateHitBox(this);
        }
    }

    isStatic() {
        return this._image.isStatic();
    }

    __visible() {
        return !(this.flags & SpriteFlag.Invisible);
    }

    protected recalcSize(): void {
        if (this._rotatedBBox) {
            this._rotatedBBox.setDimensions(this._image.width * this.sx, this._image.height * this.sy);
            this._width = Fx8(this._rotatedBBox.width);
            this._height = Fx8(this._rotatedBBox.height);
        }
        else {
            this._width = Fx8(this._image.width * this.sx);
            this._height = Fx8(this._image.height * this.sy);
        }
        this.resetHitbox();
    }

    private isScaled(): boolean {
        return this._sx !== Fx.oneFx8 || this._sy !== Fx.oneFx8;
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="width" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/width
    get width() {
        return Fx.toFloat(this._width);
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="height" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/height
    get height() {
        return Fx.toFloat(this._height);
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="left" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/left#get
    get left() {
        return Fx.toFloat(this._x)
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="left"
    //% blockCombineSetHelp=sprites/sprite/left#set
    set left(value: number) {
        const physics = game.currentScene().physicsEngine;
        physics.moveSprite(
            this,
            Fx.sub(
                Fx8(value),
                this._x
            ),
            Fx.zeroFx8
        );
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="right" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/righty#get
    get right() {
        return this.left + this.width
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="right"
    //% blockCombineSetHelp=sprites/sprite/right#set
    set right(value: number) {
        this.left = value - this.width
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="top" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/top#get
    get top() {
        return Fx.toFloat(this._y);
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="top"
    //% blockCombineSetHelp=sprites/sprite/top#set
    set top(value: number) {
        const physics = game.currentScene().physicsEngine;
        physics.moveSprite(
            this,
            Fx.zeroFx8,
            Fx.sub(
                Fx8(value),
                this._y
            )
        );
    }

    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="bottom" callInDebugger
    //% blockCombineGetHelp=sprites/sprite/bottom#get
    get bottom() {
        return this.top + this.height;
    }
    //% group="Physics" blockSetVariable="mySprite"
    //% blockCombine block="bottom"
    //% blockCombineSetHelp=sprites/sprite/bottom#set
    set bottom(value: number) {
        this.top = value - this.height;
    }

    // The z field (``get z()`` / ``set z()``) is declared in sprite.d.ts
    // as it is defnied in the superclass

    /**
     * The type of sprite
     */
    //% group="Overlaps"
    //% blockId="spritegetkind" block="%sprite(mySprite) kind"
    //% weight=79 help=sprites/sprite/kind
    kind() {
        return this._kind;
    }

    /**
     * The type of sprite
     */
    //% group="Overlaps"
    //% blockId="spritesetkind" block="set %sprite(mySprite) kind to %kind"
    //% kind.shadow=spritekind
    //% weight=80 help=sprites/sprite/set-kind
    setKind(value: number) {
        if (value == undefined || this._kind === value) return;

        const spritesByKind = game.currentScene().spritesByKind;
        if (this._kind >= 0 && spritesByKind[this._kind])
            spritesByKind[this._kind].remove(this);

        if (value >= 0) {
            if (!spritesByKind[value]) spritesByKind[value] = new sprites.SpriteSet();
            spritesByKind[value].add(this);
        }

        const overlapMap = game.currentScene().overlapMap;
        if (!overlapMap[value]) {
            overlapMap[value] = [];
        }

        this._kindsOverlappedWith = overlapMap[value];

        this._kind = value;
    }

    /**
     * Set the sprite position in pixels starting from the top-left corner of the screen.
     * @param x horizontal position in pixels
     * @param y vertical position in pixels
     */
    //% group="Physics"
    //% weight=100
    //% blockId=spritesetpos block="set %sprite(mySprite) position to x %x y %y"
    //% help=sprites/sprite/set-position
    //% x.shadow="positionPicker" y.shadow="positionPicker"
    setPosition(x: number, y: number): void {
        const physics = game.currentScene().physicsEngine;
        physics.moveSprite(
            this,
            Fx8(x - this.x),
            Fx8(y - this.y)
        );
    }

    /**
     * Sets the sprite velocity in pixel / sec
     * @param vx
     * @param vy
     */
    //% group="Physics"
    //% weight=100
    //% blockId=spritesetvel block="set %sprite(mySprite) velocity to vx %vx vy %vy"
    //% help=sprites/sprite/set-velocity
    //% vx.shadow=spriteSpeedPicker
    //% vy.shadow=spriteSpeedPicker
    setVelocity(vx: number, vy: number): void {
        this.vx = vx;
        this.vy = vy;
    }

    /**
     * Deprecated! Use sayText instead.
     *
     * Display a speech bubble with the text, for the given time.
     * @param text the text to say, eg: ":)"
     * @param time time to keep text on
     */
    //% group="Effects"
    //% weight=60
    //% blockId=spritesay block="%sprite(mySprite) say %text||for %millis ms"
    //% millis.shadow=timePicker
    //% text.shadow=text
    //% inlineInputMode=inline
    //% deprecated=true
    //% help=sprites/sprite/say
    say(text: any, timeOnScreen?: number, textColor = 15, textBoxColor = 1) {
        if (text === null || text === undefined || text === "") {
            if (this.sayRenderer) this.sayRenderer.destroy();
            this.sayRenderer = undefined;
            return;
        }

        if (this.sayRenderer && this.sayRenderer instanceof sprites.LegacySpriteSayRenderer &&
            this.sayRenderer.text === text && this.sayRenderer.bgColor === textBoxColor &&
            this.sayRenderer.fgColor === textColor && timeOnScreen === undefined && this.sayEndTime === undefined) {
                return;
        }

        if (timeOnScreen >= 0) this.sayEndTime = control.millis() + timeOnScreen;

        if (this.sayRenderer) this.sayRenderer.destroy();
        this.sayRenderer = undefined;
        text = console.inspect(text);

        this.sayRenderer = new sprites.LegacySpriteSayRenderer(text, timeOnScreen, this, textColor, textBoxColor);
    }

    /**
     * Display a speech bubble with the text, for the given time
     * @param text the text to say, eg: ":)"
     * @param time time to keep text on
     * @param animated whether to print the text character by character or not
     */
    //% group="Effects"
    //% weight=60
    //% blockId=spritesaytext block="$this say $text||for $timeOnScreen ms with animation $animated"
    //% timeOnScreen.shadow=timePicker
    //% text.shadow=text
    //% this.shadow=variables_get
    //% this.defl=mySprite
    //% inlineInputMode=inline
    //% help=sprites/sprite/say
    //% expandableArgumentMode=toggle
    sayText(text: any, timeOnScreen?: number, animated = false, textColor = 15, textBoxColor = 1) {
        if (text === null || text === undefined || text === "") {
            if (this.sayRenderer) this.sayRenderer.destroy();
            this.sayRenderer = undefined;
            return;
        }

        if (this.sayRenderer) this.sayRenderer.destroy();
        this.sayRenderer = undefined;

        if (timeOnScreen >= 0) this.sayEndTime = control.millis() + timeOnScreen;

        text = console.inspect(text);

        this.sayRenderer = new sprites.SpriteSayRenderer(text, textColor, textBoxColor, animated, timeOnScreen);
    }

    /**
     * Start an effect on this sprite
     * @param effect the type of effect to create
     */
    //% group="Effects"
    //% weight=90
    //% blockId=startEffectOnSprite block="%sprite(mySprite) start %effect effect || for %duration=timePicker|ms"
    //% help=sprites/sprite/start-effect
    startEffect(effect: effects.ParticleEffect, duration?: number) {
        effect.start(this, duration, null, !!(this.flags & sprites.Flag.RelativeToCamera));
    }

    /**
     * Indicates if the sprite is outside the screen
     */
    //%
    isOutOfScreen(camera: scene.Camera): boolean {
        const ox = (this.flags & sprites.Flag.RelativeToCamera) ? 0 : camera.drawOffsetX;
        const oy = (this.flags & sprites.Flag.RelativeToCamera) ? 0 : camera.drawOffsetY;
        return this.right - ox < 0 || this.bottom - oy < 0 || this.left - ox > screen.width || this.top - oy > screen.height;
    }

    __drawCore(camera: scene.Camera) {
        this.drawSay(camera);

        if (this.isOutOfScreen(camera)) return;

        const ox = (this.flags & sprites.Flag.RelativeToCamera) ? 0 : camera.drawOffsetX;
        const oy = (this.flags & sprites.Flag.RelativeToCamera) ? 0 : camera.drawOffsetY;

        const l = Math.floor(this.left - ox);
        const t = Math.floor(this.top - oy);

        this.drawSprite(l, t);
        this.drawDebug(l, t, ox, oy);
    }

    __update(camera: scene.Camera, dt: number) {
        if (this.lifespan !== undefined) {
            this.lifespan -= dt * 1000;
            if (this.lifespan <= 0) {
                this.lifespan = undefined;
                this._destroyCore();
            }
        }
        if ((this.flags & sprites.Flag.AutoDestroy)
            && this.isOutOfScreen(camera)) {
            this.destroy()
        }

        if (this.sayRenderer) this.sayRenderer.update(dt, camera, this);
    }

    /**
     * Set whether a sprite should be constrained within the screen (on) or not (off)
     */
    //% group="Effects"
    //% weight=30
    //% blockId=spritesetsetstayinscreen block="set %sprite(mySprite) stay in screen %on=toggleOnOff"
    //% on.defl=true
    //% help=sprites/sprite/set-stay-in-screen
    setStayInScreen(on: boolean) {
        this.setFlag(SpriteFlag.StayInScreen, on);
    }

    /**
     * Set whether a sprite should bounce when it hits a wall (on) or not (off)
     */
    //% group="Effects"
    //% weight=25
    //% blockId=spritesetsetbounceonwall block="set %sprite(mySprite) bounce on wall %on=toggleOnOff"
    //% on.defl=true
    //% help=sprites/sprite/set-bounce-on-wall
    setBounceOnWall(on: boolean) {
        this.setFlag(SpriteFlag.BounceOnWall, on);
    }

    /**
     * Set a sprite flag
     */
    //% group="Effects"
    //% weight=10
    //% blockId=spritesetsetflag block="set %sprite(mySprite) %flag %on=toggleOnOff"
    //% flag.defl=SpriteFlag.AutoDestroy
    //% help=sprites/sprite/set-flag
    setFlag(flag: SpriteFlag, on: boolean) {
        if (on) this.flags |= flag
        else this.flags = ~(~this.flags | flag);
    }

    /**
     * Check if this sprite overlaps another sprite
     * @param other
     */
    //% group="Overlaps"
    //% blockId=spriteoverlapswith block="%sprite(mySprite) overlaps with %other=variables_get(otherSprite)"
    //% other.defl=otherSprite
    //% help=sprites/sprite/overlaps-with
    //% weight=90
    overlapsWith(other: Sprite): boolean {
        control.enablePerfCounter("overlapsCPP")
        if (other == this) return false;
        if (this.flags & SPRITE_NO_SPRITE_OVERLAPS)
            return false
        if (other.flags & SPRITE_NO_SPRITE_OVERLAPS)
            return false
        if (this.flags & sprites.Flag.HitboxOverlaps || other.flags & sprites.Flag.HitboxOverlaps)
            return other._hitbox.overlapsWith(this._hitbox);
        if (!other._hitbox.overlapsWith(this._hitbox))
            return false;
        else if (this._rotatedBBox) {
            if (other._rotatedBBox) {
                if (this._rotatedBBox.overlaps(other._rotatedBBox)) {
                    return helpers.checkOverlapsTwoScaledRotatedImages(
                        other.image,
                        this.left - other.left,
                        this.top - other.top,
                        other.sx,
                        other.sy,
                        other.rotation,
                        this.image,
                        this.sx,
                        this.sy,
                        this.rotation
                    );
                }
                else {
                    return false;
                }
            }
            else {
                if (this._rotatedBBox.overlapsAABB(other.left, other.top, other.right, other.bottom)) {
                    return helpers.checkOverlapsScaledRotatedImage(
                        other.image,
                        this.left - other.left,
                        this.top - other.top,
                        this.image,
                        this.sx,
                        this.sy,
                        this.rotation
                    );
                }
                else {
                    return false;
                }
            }
        }
        else if (other._rotatedBBox) {
            return other.overlapsWith(this);
        }
        else if (!this.isScaled() && !other.isScaled()) {
            return other._image.overlapsWith(
                this._image,
                this.left - other.left,
                this.top - other.top)
        } else {
            if (this.sx == 0 || this.sy == 0 || other.sx == 0 || other.sy == 0) return false;

            let A: Sprite;
            let B: Sprite;

            // Render larger-scaled sprite onto smaller-scaled one so that we don't
            // skip over source pixels in the check.

            // A is the smaller-scaled sprite
            if (this.sx * this.sy < other.sx * other.sy) {
                A = this;
                B = other;
            } else {
                A = other;
                B = this;
            }

            // Render B onto A
            return helpers.imageBlit(
                A.image,
                // Dst rect in A
                (B.left - A.left) / A.sx,
                (B.top - A.top) / A.sy,
                B.width / A.sx,
                B.height / A.sy,
                B.image,
                // Src rect in B
                0, 0,
                B.image.width,
                B.image.height,
                true, true);
        }
    }

    /**
     * Check if there is an obstacle in the given direction
     * @param direction
     */
    //% blockId=spritehasobstacle block="is %sprite(mySprite) hitting wall %direction"
    //% blockNamespace="scene" group="Locations" blockGap=24
    //% help=scene/is-hitting-tile
    //% weight=15
    isHittingTile(direction: CollisionDirection): boolean {
        return this._obstacles && !!this._obstacles[direction];
    }

    /**
     * Get the tile kind in a given direction if any
     * @param direction
     */
    //% blockId=spritetileat block="tile to $direction of $this(mySprite) is $tile"
    //% tile.shadow=tileset_tile_picker
    //% blockNamespace="scene" group="Locations" blockGap=8
    //% help=scene/tile-kind-at
    //% weight=20
    tileKindAt(direction: TileDirection, tile: Image): boolean {
        const tilemap = game.currentScene().tileMap;
        let x = this.x >> tilemap.scale;
        let y = this.y >> tilemap.scale;
        switch (direction) {
            case TileDirection.Top:
                y = y - 1;
                break;
            case TileDirection.Bottom:
                y = y + 1;
                break;
            case TileDirection.Left:
                x = x - 1;
                break;
            case TileDirection.Right:
                x = x + 1;
                break;
            case TileDirection.Center:
            default:
                break;
        }
        return tiles.getTileImage(tilemap.getTile(x, y)).equals(tile);
    }

    /**
     * Get the obstacle sprite in a given direction if any
     * @param direction
     */
    //% blockId=spriteobstacle block="%sprite(mySprite) wall hit on %direction"
    //% blockNamespace="scene" group="Locations"
    //% help=sprites/sprite/tile-hit-from
    //% deprecated=1
    tileHitFrom(direction: number): number {
        return (this._obstacles && this._obstacles[direction]) ? this._obstacles[direction].tileIndex : -1;
    }

    /**
     * Gets the tilemap location at the center of a sprite
     */
    //% block="tilemap location of $this"
    //% blockId=tiles_location_of_sprite
    //% this.shadow=variables_get
    //% this.defl=mySprite
    //% blockNamespace="scene" group="Locations" weight=90
    //% help=scene/tilemap-location
    tilemapLocation(): tiles.Location {
        const scene = game.currentScene();
        if (!scene.tileMap) return undefined;
        return tiles.getTileLocation(this.x >> scene.tileMap.scale, this.y >> scene.tileMap.scale);
    }

    clearObstacles() {
        this._obstacles = [];
    }

    registerObstacle(direction: CollisionDirection, other: sprites.Obstacle, tm?: tiles.TileMap) {
        this._obstacles[direction] = other;
        const collisionHandlers = game.currentScene().collisionHandlers[other.tileIndex];
        const wallCollisionHandlers = game.currentScene().wallCollisionHandlers;

        if (collisionHandlers) {
            collisionHandlers
                .filter(h => h.kind == this.kind())
                .forEach(h => h.handler(this));
        }
        if (wallCollisionHandlers) {
            tm = tm || game.currentScene().tileMap;
            const wallHandlersToRun = wallCollisionHandlers
                .filter(h => h.spriteKind == this.kind());
            if (wallHandlersToRun.length) {
                const asTileLocation = tm.getTile(other.left >> tm.scale, other.top >> tm.scale);
                wallHandlersToRun
                    .forEach(h => h.handler(this, asTileLocation));
            }
        }
    }

    /**
     * Run code when the sprite is destroyed
     * @param handler
     */
    //% group="Lifecycle"
    //% weight=9
    onDestroyed(handler: () => void) {
        this.destroyHandler = handler
    }

    /**
     * Destroy the sprite
     */
    //% group="Effects"
    //% weight=80
    //% blockId=spritedestroy block="destroy %sprite(mySprite) || with %effect effect for %duration ms"
    //% duration.shadow=timePicker
    //% expandableArgumentMode="toggle"
    //% help=sprites/sprite/destroy
    //% deprecated=1
    destroy(effect?: effects.ParticleEffect, duration?: number) {
        if (this.flags & sprites.Flag.Destroyed)
            return;
        this.flags |= sprites.Flag.Destroyed;

        if (effect)
            effect.destroy(this, duration);
        else
            this._destroyCore();
    }

    _destroyCore() {
        this.flags |= sprites.Flag.Destroyed;
        const scene = game.currentScene();
        scene.allSprites.removeElement(this);
        if (this.kind() >= 0 && scene.spritesByKind[this.kind()])
            scene.spritesByKind[this.kind()].remove(this);
        scene.physicsEngine.removeSprite(this);
        if (this.destroyHandler)
            this.destroyHandler();
        scene.destroyedHandlers
            .filter(h => h.kind == this.kind())
            .forEach(h => h.handler(this));
    }

    /**
     * Make this sprite follow the target sprite.
     *
     * @param target the sprite this one should follow
     * @param speed the rate at which this sprite should move, eg: 100
     * @param turnRate how quickly the sprite should turn while following.
     *      The default (400) will cause the sprite to reach max speed after approximately 125 ms when standing still,
     *      and turn around 180 degrees when at max speed after approximately 250 ms.
     */
    //% group="Physics" weight=10
    //% blockId=spriteFollowOtherSprite
    //% block="set %sprite(myEnemy) follow %target=variables_get(mySprite) || with speed %speed"
    //% target.defl=mySprite
    //% help=sprites/sprite/follow
    follow(target: Sprite, speed = 100, turnRate = 400) {
        if (target === this) return;

        const sc = game.currentScene();
        if (!sc.followingSprites) {
            sc.followingSprites = [];
            let lastTime = game.runtime();

            sc.eventContext.registerFrameHandler(scene.FOLLOW_SPRITE_PRIORITY, () => {
                const currTime = game.runtime();
                const timeDiff = (currTime - lastTime) / 1000;
                let destroyedSprites = false;

                sc.followingSprites.forEach(fs => {
                    const { target, self, turnRate, rate } = fs;
                    // one of the involved sprites has been destroyed,
                    // so exit and remove that in the cleanup step
                    if ((self.flags | target.flags) & sprites.Flag.Destroyed) {
                        self.vx = 0;
                        self.vy = 0;
                        destroyedSprites = true;
                        return;
                    }

                    const dx = target.x - self.x;
                    const dy = target.y - self.y;

                    // already right on top of target; stop moving
                    if (Math.abs(dx) < 2 && Math.abs(dy) < 2) {
                        // snap to target location so it sits 'right on top' of sprite.
                        self.x = target.x;
                        self.y = target.y;

                        self.vx = 0;
                        self.vy = 0;
                        return;
                    }

                    const maxMomentumDiff = timeDiff * turnRate * (rate / 50);
                    const angleToTarget = Math.atan2(dy, dx);

                    // to move directly towards target, use this...
                    const targetTrajectoryVx = Math.cos(angleToTarget) * rate;
                    const targetTrajectoryVy = Math.sin(angleToTarget) * rate;

                    // ... but to keep momentum, calculate the diff in velocities and maintain some of the velocity
                    const diffVx = targetTrajectoryVx - self.vx;
                    const diffVy = targetTrajectoryVy - self.vy;

                    self.vx += Math.clamp(-maxMomentumDiff, maxMomentumDiff, diffVx);
                    self.vy += Math.clamp(-maxMomentumDiff, maxMomentumDiff, diffVy);
                });

                lastTime = currTime;

                // cleanup: remove followers where one has been destroyed
                if (destroyedSprites) {
                    sc.followingSprites = sc.followingSprites
                        .filter(fs => !((fs.self.flags | fs.target.flags) & sprites.Flag.Destroyed));
                }
            });
        }

        const fs = sc.followingSprites.find(fs => fs.self.id == this.id);

        if (!target || !speed) {
            if (fs) {
                sc.followingSprites.removeElement(fs);
                this.vx = 0;
                this.vy = 0;
            }
        } else if (!fs) {
            sc.followingSprites.push(new sprites.FollowingSprite(
                this,
                target,
                speed,
                turnRate
            ));
        } else {
            fs.target = target;
            fs.rate = speed;
            fs.turnRate = turnRate;
        }
    }

    /**
     * Stop this sprite from following any target sprite.
     */
    //% group="Physics" weight=9
    //% blockId=spriteUnfollow
    //% block="set $this unfollow"
    //% this.shadow=variables_get
    //% this.defl=myEnemy
    //% help=sprites/sprite/unfollow
    unfollow() {
        this.follow(null, 0);
    }

    setScaleCore(sx?: number, sy?: number, anchor?: ScaleAnchor, proportional?: boolean): void {
        anchor = anchor || ScaleAnchor.Middle;

        const hasSx = sx != null;
        const hasSy = sy != null;

        const oldW = this.width;
        const oldH = this.height;

        if (hasSx) {
            const oldSx = this.sx;
            this.sx = sx;
            if (!hasSy && proportional) {
                const ratio = sx / oldSx;
                this.sy *= ratio;
            }
        }
        if (hasSy) {
            const oldSy = this.sy;
            this.sy = sy;
            if (!hasSx && proportional) {
                const ratio = sy / oldSy;
                this.sx *= ratio;
            }
        }

        if (anchor & (ScaleAnchor.Left | ScaleAnchor.Right)) {
            const newW = this.width;
            const diff = newW - oldW;
            const diffOver2 = diff / 2;
            if (anchor & ScaleAnchor.Left) { this.x += diffOver2; }
            if (anchor & ScaleAnchor.Right) { this.x -= diffOver2; }
        }
        if (anchor & (ScaleAnchor.Top | ScaleAnchor.Bottom)) {
            const newH = this.height;
            const diff = newH - oldH;
            const diffOver2 = diff / 2;
            if (anchor & ScaleAnchor.Top) { this.y += diffOver2; }
            if (anchor & ScaleAnchor.Bottom) { this.y -= diffOver2; }
        }
    }

    //% blockId=sprite_set_scale
    //% block="set %sprite(mySprite) scale to $value anchor $anchor"
    //% expandableArgumentMode=enabled
    //% inlineInputMode=inline
    //% value.defl=1
    //% anchor.defl=ScaleAnchor.Middle
    //% help=sprites/sprite/set-scale
    //% group="Scale" weight=90
    setScale(value: number, anchor?: ScaleAnchor): void {
        const direction = ScaleDirection.Uniformly;
        anchor = anchor || ScaleAnchor.Middle;

        let sx: number;
        let sy: number;

        if (direction & ScaleDirection.Horizontally) sx = value;
        if (direction & ScaleDirection.Vertically) sy = value;

        this.setScaleCore(sx, sy, anchor);
    }

    //% blockId=sprite_change_scale
    //% block="change %sprite(mySprite) scale by $value anchor $anchor"
    //% expandableArgumentMode=enabled
    //% inlineInputMode=inline
    //% value.defl=1
    //% anchor.defl=ScaleAnchor.Middle
    //% help=sprites/sprite/change-scale
    //% group="Scale" weight=90
    changeScale(value: number, anchor?: ScaleAnchor): void {
        const direction = ScaleDirection.Uniformly;
        anchor = anchor || ScaleAnchor.Middle;

        let sx: number;
        let sy: number;

        if (direction & ScaleDirection.Horizontally) sx = this.sx + value;
        if (direction & ScaleDirection.Vertically) sy = this.sy + value;

        this.setScaleCore(sx, sy, anchor);
    }

    toString() {
        return `${this.id}(${this.x},${this.y})->(${this.vx},${this.vy})`;
    }

    protected drawSay(camera: scene.Camera) {
        if (this.sayRenderer) {
            if (this.sayEndTime !== undefined) {
                if (control.millis() < this.sayEndTime) {
                    this.sayRenderer.draw(screen, camera, this);
                }
                else {
                    this.sayRenderer.destroy();
                    this.sayRenderer = undefined;
                    this.sayEndTime = undefined;
                }
            }
            else {
                this.sayRenderer.draw(screen, camera, this)
            }
        }
    }

    protected drawDebug(left: number, top: number, offsetX: number, offsetY: number) {
        if (this.flags & SpriteFlag.ShowPhysics) {
            const font = image.font5;
            const margin = 2;
            let tx = left;
            let ty = top + this.height + margin;
            screen.print(`${this.x >> 0},${this.y >> 0}`, tx, ty, 1, font);
            tx -= font.charWidth;
            if (this.vx || this.vy) {
                ty += font.charHeight + margin;
                screen.print(`v${this.vx >> 0},${this.vy >> 0}`, tx, ty, 1, font);
            }
            if (this.ax || this.ay) {
                ty += font.charHeight + margin;
                screen.print(`a${this.ax >> 0},${this.ay >> 0}`, tx, ty, 1, font);
            }
        }

        // debug info
        if (game.debug) {
            screen.drawRect(
                Fx.toInt(this._hitbox.left) - offsetX,
                Fx.toInt(this._hitbox.top) - offsetY,
                Fx.toInt(this._hitbox.width),
                Fx.toInt(this._hitbox.height),
                1
            );
        }
    }

    protected drawSprite(drawLeft: number, drawTop: number) {
        if (this._rotatedBBox) {
            helpers.imageDrawScaledRotated(
                screen,
                drawLeft,
                drawTop,
                this._image,
                this.sx,
                this.sy,
                this.rotation
            );
        }
        else if (!this.isScaled())
            screen.drawTransparentImage(this._image, drawLeft, drawTop);
        else
            screen.blit(
                // dst rect in screen
                drawLeft, drawTop,
                this.width,
                this.height,
                // src rect in sprite image
                this._image,
                0, 0,
                this._image.width, this._image.height,
                true, false);
    }
}
