import { writeFile, writeFileSync } from "fs";
import { __native_encode } from "./native";

export interface EncodeOptions {
    /**
     * The width of the image to be encoded in pixels.
     */
    width: number;
    /**
     * The height of the image to be encoded in pixels.
     */
    height?: number;
    /**
     * level of compression to use 0 - no compression, 1 - fastest, 9 - best size.
     */
    compressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
}

/**
 * Encode a buffer of raw RGB or RGBA image data into PNG format.
 * Only RGB and RGBA color formats are supported. This function will automatically calculate whether an
 * alpha channel is present by calculating the amount of bytes per pixel from the length of the buffer
 * and the provided `width` and `height`. Only 8bit colors are supported.
 *
 * @param buffer The buffer of raw pixel data to encode.
 * @param options Options used to encode the image.
 *
 * @return the encoded PNG as a new buffer.
 */
export function encode(buffer: Buffer, options: EncodeOptions): Buffer {
    if (!Buffer.isBuffer(buffer)) {
        throw new Error("Input is not a buffer.");
    }
    if (typeof options !== "object" || options === null) {
        throw new Error("Options need to be an object.");
    }
    let { width, height, compressionLevel = 9 } = options;
    if (typeof width !== "number" || typeof height !== "number") {
        throw new Error("Error encoding PNG. Width and height need to be specified.");
    }
    if (!Number.isInteger(width)) {
        throw new Error("Error encoding PNG. Width needs to be an integer.");
    }
    if (!Number.isInteger(height)) {
        throw new Error("Error encoding PNG. Height needs to be an integer.");
    }
    if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {
        throw new Error("Error encoding PNG. CompressionLevel needs to be an integer between 0 and 9.");
    }
    const bytesPerPixel = buffer.length / (width * height);
    if (bytesPerPixel !== 3 && bytesPerPixel !== 4) {
        throw new Error("Error encoding PNG. Unsupported color type.");
    }
    const alpha = bytesPerPixel === 4;
    return __native_encode(buffer, width, height, alpha, compressionLevel);
}

export type WritePngFileCallback = (error: Error) => void;

export function writePngFile(
    path: string,
    buffer: Buffer,
    options: EncodeOptions,
    callback: WritePngFileCallback,
): void;
export function writePngFile(path: string, buffer: Buffer, options: EncodeOptions): Promise<void>;
/**
 * Invoke `writePngFile` to asynchroneously write a raw buffer of pixel data as an encoded PNG image.
 * For convenience, both Node.js callbacks and Promises are supported.
 * If no callback is provided as a second argument, a Promise is returned which will resolve
 * once the file is written.
 *
 * @param path The path the file should be written to.
 * @param buffer The buffer of raw pixel data which should be encoded and written to disk.
 * @param options Options used to encode the image.
 * @param callback An optional callback to use instead of a returned Promise. Will be called with
 *                 an error as the first argument or `null` if everything went well.
 * @return A Promise if no callback was provided and `undefined` otherwise.
 */
export function writePngFile(
    path: string,
    buffer: Buffer,
    options: EncodeOptions,
    callback?: WritePngFileCallback,
): Promise<void> {
    // Checlkif the user provided a `callback`.
    if (typeof callback === "function") {
        // Encode the buffer and call the `callback` with an error if an error occured.
        let encoded: Buffer;
        try {
            encoded = encode(buffer, options);
        } catch (encodeError) {
            callback(encodeError);
            return;
        }
        // Write the file and hand over the callback. This way it will be called with `null` or an error
        // if an error occured.
        writeFile(path, encoded, callback);
        return;
    }
    // If the user didn't provide a callback, return a Promise which will resolve once the file is written,
    // or reject with an error if an error occured.
    return new Promise<void>((resolve, reject) => {
        // Encode the buffer and reject the Promise if an error occured.
        let encoded: Buffer;
        try {
            encoded = encode(buffer, options);
        } catch (encodeError) {
            reject(encodeError);
            return;
        }
        // Write the file and make the Promise resolve or reject depending on whether an error occured when writing.
        writeFile(path, encoded, error => {
            if (error) {
                reject(error);
                return;
            }
            resolve();
        });
        return;
    });
}

/**
 * Encode and write a PNG file synchroneously.
 *
 * @param path The path the file should be written to.
 * @param buffer The buffer of raw pixel data which should be encoded and written to disk.
 * @param options Options used to encode the image.
 *
 * @return The decoded image.
 */
export function writePngFileSync(path: string, buffer: Buffer, options: EncodeOptions): void {
    writeFileSync(path, encode(buffer, options));
}
