import { ExtensionType } from '../../../../extensions/Extensions';
import { Container } from '../../../../scene/container/Container';
import { Texture } from '../texture/Texture';
import type { ColorSource } from '../../../../color/Color';
import type { ICanvas } from '../../../../environment/canvas/ICanvas';
import type { Rectangle } from '../../../../maths/shapes/Rectangle';
import type { Renderer } from '../../types';
import type { System } from '../system/System';
import type { GetPixelsOutput } from '../texture/GenerateCanvas';
import type { GenerateTextureOptions } from './GenerateTextureSystem';
const imageTypes = {
    png: 'image/png',
    jpg: 'image/jpeg',
    webp: 'image/webp',
};
type Formats = keyof typeof imageTypes;
/**
 * Options for creating an image from a renderer.
 * @memberof rendering
 */
export interface ImageOptions
{
    /** The format of the image. */
    format?: Formats;
    /** The quality of the image. */
    quality?: number;
}
/**
 * Options for extracting content from a renderer.
 * @memberof rendering
 */
export interface BaseExtractOptions
{
    /** The target to extract. */
    target: Container | Texture;
    /** The region of the target to extract. */
    frame?: Rectangle;
    /** The resolution of the extracted content. */
    resolution?: number;
    /** The color used to clear the extracted content. */
    clearColor?: ColorSource;
    /** Whether to enable anti-aliasing. This may affect performance. */
    antialias?: boolean;
}
/**
 * Options for extracting an HTMLImage from the renderer.
 * @memberof rendering
 */
export type ExtractImageOptions = BaseExtractOptions & ImageOptions;
/**
 * Options for extracting and downloading content from a renderer.
 * @memberof rendering
 */
export type ExtractDownloadOptions = BaseExtractOptions & {
    /** The filename to use when downloading the content. */
    filename: string;
};
/**
 * Options for extracting content from a renderer.
 * @memberof rendering
 */
export type ExtractOptions = BaseExtractOptions | ExtractImageOptions | ExtractDownloadOptions;
/**
 * This class provides renderer-specific plugins for exporting content from a renderer.
 * For instance, these plugins can be used for saving an Image, Canvas element or for exporting the raw image data (pixels).
 *
 * Do not instantiate these plugins directly. It is available from the `renderer.extract` property.
 * @example
 * import { Application, Graphics } from 'pixi.js';
 *
 * // Create a new application (extract will be auto-added to renderer)
 * const app = new Application();
 * await app.init();
 *
 * // Draw a red circle
 * const graphics = new Graphics()
 *     .circle(0, 0, 50);
 *     .fill(0xFF0000)
 *
 * // Render the graphics as an HTMLImageElement
 * const image = await app.renderer.extract.image(graphics);
 * document.body.appendChild(image);
 * @memberof rendering
 */
export class ExtractSystem implements System
{
    /** @ignore */
    public static extension = {
        type: [
            ExtensionType.WebGLSystem,
            ExtensionType.WebGPUSystem,
        ],
        name: 'extract',
    } as const;
    /** Default options for creating an image. */
    public static defaultImageOptions: ImageOptions = {
        /** The format of the image. */
        format: 'png' as Formats,
        /** The quality of the image. */
        quality: 1,
    };
    private _renderer: Renderer;
    /** @param renderer - The renderer this System works for. */
    constructor(renderer: Renderer)
    {
        this._renderer = renderer;
    }
    private _normalizeOptions<T extends ExtractOptions>(
        options: ExtractImageOptions | Container | Texture,
        defaults: Partial<T> = {},
    ): T
    {
        if (options instanceof Container || options instanceof Texture)
        {
            return {
                target: options,
                ...defaults
            } as T;
        }
        return {
            ...defaults,
            ...options,
        } as T;
    }
    /**
     * Will return a HTML Image of the target
     * @param options - The options for creating the image, or the target to extract
     * @returns - HTML Image of the target
     */
    public async image(options: ExtractImageOptions | Container | Texture): Promise<HTMLImageElement>
    {
        const image = new Image();
        image.src = await this.base64(options);
        return image;
    }
    /**
     * Will return a base64 encoded string of this target. It works by calling
     * `Extract.canvas` and then running toDataURL on that.
     * @param options - The options for creating the image, or the target to extract
     */
    public async base64(options: ExtractImageOptions | Container | Texture): Promise<string>
    {
        options = this._normalizeOptions<ExtractImageOptions>(
            options,
            ExtractSystem.defaultImageOptions
        );
        const { format, quality } = options;
        const canvas = this.canvas(options);
        if (canvas.toBlob !== undefined)
        {
            return new Promise<string>((resolve, reject) =>
            {
                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                canvas.toBlob!((blob) =>
                {
                    if (!blob)
                    {
                        reject(new Error('ICanvas.toBlob failed!'));
                        return;
                    }
                    const reader = new FileReader();
                    reader.onload = () => resolve(reader.result as string);
                    reader.onerror = reject;
                    reader.readAsDataURL(blob);
                }, imageTypes[format], quality);
            });
        }
        if (canvas.toDataURL !== undefined)
        {
            return canvas.toDataURL(imageTypes[format], quality);
        }
        if (canvas.convertToBlob !== undefined)
        {
            const blob = await canvas.convertToBlob({ type: imageTypes[format], quality });
            return new Promise<string>((resolve, reject) =>
            {
                const reader = new FileReader();
                reader.onload = () => resolve(reader.result as string);
                reader.onerror = reject;
                reader.readAsDataURL(blob);
            });
        }
        throw new Error('Extract.base64() requires ICanvas.toDataURL, ICanvas.toBlob, '
            + 'or ICanvas.convertToBlob to be implemented');
    }
    /**
     * Creates a Canvas element, renders this target to it and then returns it.
     * @param options - The options for creating the canvas, or the target to extract
     * @returns - A Canvas element with the texture rendered on.
     */
    public canvas(options: ExtractOptions | Container | Texture): ICanvas
    {
        options = this._normalizeOptions(options);
        const target = options.target;
        const renderer = this._renderer;
        if (target instanceof Texture)
        {
            return renderer.texture.generateCanvas(target);
        }
        const texture = renderer.textureGenerator.generateTexture(options as GenerateTextureOptions);
        const canvas = renderer.texture.generateCanvas(texture);
        texture.destroy();
        return canvas;
    }
    /**
     * Will return a one-dimensional array containing the pixel data of the entire texture in RGBA
     * order, with integer values between 0 and 255 (included).
     * @param options - The options for extracting the image, or the target to extract
     * @returns - One-dimensional array containing the pixel data of the entire texture
     */
    public pixels(options: ExtractOptions | Container | Texture): GetPixelsOutput
    {
        options = this._normalizeOptions(options);
        const target = options.target;
        const renderer = this._renderer;
        const texture = target instanceof Texture
            ? target
            : renderer.textureGenerator.generateTexture(options as GenerateTextureOptions);
        const pixelInfo = renderer.texture.getPixels(texture);
        if (target instanceof Container)
        {
            // destroy generated texture
            texture.destroy();
        }
        return pixelInfo;
    }
    /**
     * Will return a texture of the target
     * @param options - The options for creating the texture, or the target to extract
     * @returns - A texture of the target
     */
    public texture(options: ExtractOptions | Container | Texture): Texture
    {
        options = this._normalizeOptions(options);
        if (options.target instanceof Texture) return options.target;
        return this._renderer.textureGenerator.generateTexture(options as GenerateTextureOptions);
    }
    /**
     * Will extract a HTMLImage of the target and download it
     * @param options - The options for downloading and extracting the image, or the target to extract
     */
    public download(options: ExtractDownloadOptions | Container | Texture)
    {
        options = this._normalizeOptions<ExtractDownloadOptions>(options);
        const canvas = this.canvas(options);
        const link = document.createElement('a');
        link.download = options.filename ?? 'image.png';
        link.href = canvas.toDataURL('image/png');
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }
    /**
     * Logs the target to the console as an image. This is a useful way to debug what's happening in the renderer.
     * @param options - The options for logging the image, or the target to log
     */
    public log(options: (ExtractOptions & {width?: number}) | Container | Texture)
    {
        const width = options.width ?? 200;
        options = this._normalizeOptions(options);
        const canvas = this.canvas(options);
        const base64 = canvas.toDataURL();
        // eslint-disable-next-line no-console
        console.log(`[Pixi Texture] ${canvas.width}px ${canvas.height}px`);
        const style = [
            'font-size: 1px;',
            `padding: ${width}px ${300}px;`,
            `background: url(${base64}) no-repeat;`,
            'background-size: contain;',
        ].join(' ');
        // eslint-disable-next-line no-console
        console.log('%c ', style);
    }
    public destroy(): void
    {
        this._renderer = null as any as Renderer;
    }
}