namespace effects {

    //% fixedInstances
    export interface BackgroundEffect {
        startScreenEffect(): void;
    }

    //% fixedInstances
    export class ParticleEffect {
        protected sourceFactory: (anchor: particles.ParticleAnchor, pps: number) => particles.ParticleSource;
        protected defaultRate: number;
        protected defaultLifespan: number;

        constructor(defaultParticlesPerSecond: number, defaultLifespan: number,
                sourceFactory: (anchor: particles.ParticleAnchor, particlesPerSecond: number) => particles.ParticleSource) {
            this.sourceFactory = sourceFactory;
            this.defaultRate = defaultParticlesPerSecond;
            this.defaultLifespan = defaultLifespan;
        }

        /**
         * Attaches a new particle animation to the sprite or anchor for a short period of time
         * @param anchor
         * @param duration
         * @param particlesPerSecond
         */
        start(anchor: particles.ParticleAnchor, duration?: number, particlesPerSecond?: number, relativeToCamera?: boolean): void {
            if (!this.sourceFactory) return;
            const src = this.sourceFactory(anchor, particlesPerSecond ? particlesPerSecond : this.defaultRate);
            src.setRelativeToCamera(!!relativeToCamera);
            if (duration)
                src.lifespan = duration > 0 ? duration : this.defaultLifespan;
        }

        /**
         * Destroy the provided sprite with an effect
         * @param sprite
         * @param duration how long the sprite will remain on the screen. If set to 0 or undefined,
         *                  uses the default rate for this effect.
         * @param particlesPerSecond
         */
        destroy(anchor: Sprite, duration?: number, particlesPerSecond?: number) {
            anchor.setFlag(SpriteFlag.Ghost, true);
            this.start(anchor, particlesPerSecond, null, !!(anchor.flags & sprites.Flag.RelativeToCamera));
            anchor.lifespan = duration ? duration : this.defaultLifespan >> 2;
            effects.dissolve.applyTo(anchor);
        }
    }

    /**
     * Anchor used for effects that occur across the screen.
     */
    class SceneAnchor implements particles.ParticleAnchor {
        private camera: scene.Camera;

        constructor() {
            this.camera = game.currentScene().camera;
        }

        get x() {
            return this.camera.offsetX + (screen.width >> 1);
        }

        get y() {
            return this.camera.offsetY + (screen.height >> 1);
        }

        get width() {
            return screen.width;
        }

        get height() {
            return screen.height;
        }
    }

    //% fixedInstances
    export class ScreenEffect extends ParticleEffect implements BackgroundEffect {
        protected source: particles.ParticleSource;
        protected sceneDefaultRate: number;

        constructor(anchorDefault: number, sceneDefault: number, defaultLifespan: number,
                sourceFactory: (anchor: particles.ParticleAnchor, particlesPerSecond: number) => particles.ParticleSource) {
            super(anchorDefault, defaultLifespan, sourceFactory);
            this.sceneDefaultRate = sceneDefault;
        }

        /**
         * Creates a new effect that occurs over the entire screen
         * @param particlesPerSecond
         * @param duration
         */
        //% blockId=particlesStartScreenAnimation block="start screen %effect effect || for %duration ms"
        //% duration.shadow=timePicker
        //% blockNamespace=scene
        //% group="Effects" blockGap=8
        //% weight=90 help=effects/start-screen-effect
        startScreenEffect(duration?: number, particlesPerSecond?: number): void {
            if (!this.sourceFactory)
                return;

            if (this.source && this.source.enabled) {
                if (duration)
                    this.source.lifespan = duration;
                return;
            }

            this.endScreenEffect();
            this.source = this.sourceFactory(new SceneAnchor(), particlesPerSecond ? particlesPerSecond : this.sceneDefaultRate);
            this.source.priority = 10;
            if (duration)
                this.source.lifespan = duration;
        }

        /**
         * If this effect is currently occurring as a full screen effect, stop producing particles and end the effect
         * @param particlesPerSecond
         */
        //% blockId=particlesEndScreenAnimation block="end screen %effect effect"
        //% blockNamespace=scene
        //% group="Effects" blockGap=8
        //% weight=80 help=effects/end-screen-effect
        endScreenEffect(): void {
            if (this.source) {
                this.source.destroy();
                this.source = undefined;
            }
        }
    }

    /**
     * Removes all effects attached to the given anchor
     * @param anchor the anchor to remove effects from
     */
    //% blockId=particlesclearparticles block="clear effects on %anchor=variables_get(mySprite)"
    //% blockNamespace=sprites
    //% anchor.defl=mySprite
    //% group="Effects" weight=89
    //% help=effects/clear-particles
    export function clearParticles(anchor: Sprite | particles.ParticleAnchor) {
        const sources = game.currentScene().particleSources;
        if (!sources) return;
        sources
            .filter(ps => ps.anchor === anchor)
            .forEach(ps => ps.destroy());
    }

    function createEffect(defaultParticlesPerSecond: number, defaultLifespan: number,
            factoryFactory: (anchor?: particles.ParticleAnchor) => particles.ParticleFactory): ParticleEffect {
        return new ParticleEffect(defaultParticlesPerSecond, defaultLifespan,
                    (anchor: particles.ParticleAnchor, pps: number) =>
                        new particles.ParticleSource(anchor, pps, factoryFactory()));
    }

    //% fixedInstance whenUsed block="spray"
    export const spray = createEffect(20, 2000, function () { return new particles.SprayFactory(100, 0, 120) });

    //% fixedInstance whenUsed block="trail"
    export const trail = new ParticleEffect(20, 4000, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        const factory = new particles.TrailFactory(anchor, 250, 1000);
        return new particles.ParticleSource(anchor, particlesPerSecond, factory);
    });

    //% fixedInstance whenUsed block="fountain"
    export const fountain = new ParticleEffect(20, 3000, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        class FountainFactory extends particles.SprayFactory {
            galois: Math.FastRandom;

            constructor() {
                super(40, 180, 90);
                this.galois = new Math.FastRandom(1234);
            }

            createParticle(anchor: particles.ParticleAnchor) {
                const p = super.createParticle(anchor);
                p.color = this.galois.randomBool() ? 8 : 9;
                p.lifespan = 1500;
                return p;
            }

            drawParticle(p: particles.Particle, x: Fx8, y: Fx8) {
                screen.setPixel(Fx.toInt(x), Fx.toInt(y), p.color);
            }
        }

        const factory = new FountainFactory();
        const source = new particles.ParticleSource(anchor, particlesPerSecond, factory);
        source.setAcceleration(0, 40);
        return source;
    });

    //% fixedInstance whenUsed block="confetti"
    export const confetti = new ScreenEffect(10, 40, 4000, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        const factory = new particles.ConfettiFactory(anchor.width ? anchor.width : 16, 16);
        factory.setSpeed(30);
        return new particles.ParticleSource(anchor, particlesPerSecond, factory);
    });

    //% fixedInstance whenUsed block="hearts"
    export const hearts = new ScreenEffect(5, 20, 2000, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        const factory = new particles.ShapeFactory(anchor.width ? anchor.width : 16, 16, img`
            . F . F .
            F . F . F
            F . . . F
            . F . F .
            . . F . .
        `);

        // if large anchor, increase lifespan
        if (factory.xRange > 50) {
            factory.minLifespan = 1000;
            factory.maxLifespan = 2000;
        }

        factory.setSpeed(90);
        return new particles.ParticleSource(anchor, particlesPerSecond, factory);
    });

    //% fixedInstance whenUsed block="smiles"
    export const smiles = new ScreenEffect(5, 25, 1500, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        const factory = new particles.ShapeFactory(anchor.width ? anchor.width : 16, 16, img`
            . f . f .
            . f . f .
            . . . . .
            f . . . f
            . f f f .
        `);
        // if large anchor, increase lifespan
        if (factory.xRange > 50) {
            factory.minLifespan = 1250;
            factory.maxLifespan = 2500;
        }

        factory.setSpeed(50);
        return new particles.ParticleSource(anchor, particlesPerSecond, factory);
    });

    //% fixedInstance whenUsed block="rings"
    export const rings = createEffect(5, 1000, function () {
        return new particles.ShapeFactory(16, 16, img`
            . F F F .
            F . . . F
            F . . . F
            f . . . f
            . f f f .
        `);
    });

    //% fixedInstance whenUsed block="fire"
    export const fire = new ParticleEffect(50, 5000, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        const factory = new particles.FireFactory(5);
        const src = new particles.FireSource(anchor, particlesPerSecond, factory);
        src.setAcceleration(0, -20);
        return src;
    });

    //% fixedInstance whenUsed block="warm radial"
    export const warmRadial = createEffect(30, 2500, function () { return new particles.RadialFactory(0, 30, 10) });

    //% fixedInstance whenUsed block="cool radial"
    export const coolRadial = createEffect(30, 2000, function () { return new particles.RadialFactory(0, 30, 10, [0x6, 0x7, 0x8, 0x9, 0xA]) });

    //% fixedInstance whenUsed block="halo"
    export const halo = createEffect(70, 3000, function () {
        class RingFactory extends particles.RadialFactory {
            createParticle(anchor: particles.ParticleAnchor) {
                const p = super.createParticle(anchor);
                p.lifespan = this.galois.randomRange(200, 350);
                return p;
            }
        }
        return new RingFactory(30, 40, 10, [0x4, 0x4, 0x5]);
    });

    //% fixedInstance whenUsed block="ashes"
    export const ashes = new ParticleEffect(60, 2000, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        const factory = new particles.AshFactory(anchor);
        const src = new particles.ParticleSource(anchor, particlesPerSecond, factory);
        src.setAcceleration(0, 500);
        return src;
    });

    //% fixedInstance whenUsed block="disintegrate"
    export const disintegrate = new ParticleEffect(60, 1250, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        const factory = new particles.AshFactory(anchor, true, 30);
        factory.minLifespan = 200;
        factory.maxLifespan = 500;
        const src = new particles.ParticleSource(anchor, particlesPerSecond, factory);
        src.setAcceleration(0, 750);
        return src;
    });

    //% fixedInstance whenUsed block="blizzard"
    export const blizzard = new ScreenEffect(15, 50, 3000, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        class SnowFactory extends particles.ShapeFactory {
            constructor(xRange: number, yRange: number) {
                super(xRange, yRange, img`F`);
                this.addShape(img`
                    F
                    F`
                );
                this.minLifespan = 200;
                this.maxLifespan = this.xRange > 50 ? 1200: 700;
            }

            createParticle(anchor: particles.ParticleAnchor) {
                const p = super.createParticle(anchor);
                p.color = this.galois.percentChance(80) ? 0x1 : 0x9;
                return p;
            }
        }

        const factory = new SnowFactory(anchor.width ? anchor.width : 16, anchor.height ? anchor.height : 16);
        const src = new particles.ParticleSource(anchor, particlesPerSecond, factory);
        src.setAcceleration(-300, -100);
        return src;
    });

    //% fixedInstance whenUsed block="bubbles"
    export const bubbles = new ScreenEffect(15, 40, 5000, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        const min = anchor.width > 50 ? 2000 : 500;
        const factory = new particles.BubbleFactory(anchor, min, min * 2.5);
        return new particles.BubbleSource(anchor, particlesPerSecond, factory.stateCount - 1, factory);
    });

    //% fixedInstance whenUsed block="star field"
    export const starField = new ScreenEffect(2, 5, 5000, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        const factory = new particles.StarFactory([0x1, 0x3, 0x5, 0x9, 0xC]);
        return new particles.ParticleSource(anchor, particlesPerSecond, factory);
    });

    //% fixedInstance whenUsed block="clouds"
    export const clouds = new ScreenEffect(.5, 1.5, 5000, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        const factory = new particles.CloudFactory();
        const source = new particles.ParticleSource(anchor, particlesPerSecond, factory);

        // render behind tile map
        source.z = -2;
        return source;
    });

    //% fixedInstance whenUsed block="none"
    export const none = new ScreenEffect(0, 0, 0, function (anchor: particles.ParticleAnchor, particlesPerSecond: number) {
        class NullParticleSource extends particles.ParticleSource {
            constructor() {
                super(null, 0);
                this._prune();
            }

            __draw(camera: scene.Camera) {}

            _update(dt: number) {}

            // remove self at next opportunity
            _prune() {
                const scene = game.currentScene();
                if (!scene)
                    return;
                scene.allSprites.removeElement(this);
                const sources = scene.particleSources;
                if (sources && sources.length)
                    sources.removeElement(this);
            }
            destroy() { this._prune(); }
            clear() { this.head = undefined; }
        }
        const source = new NullParticleSource();

        return source;
    });
}