/**
 * Simulate the [Fastly ImageOpto](https://docs.fastly.com/api/imageopto/) API.
 * Returns a middleware which attempts to run image responses through `sharp`.
 */
import { createLogger } from './logging';
import mime from 'mime-types';
import sharp from 'sharp';
import { URLSearchParams } from 'url';
import vary from 'vary';
import RequestErrors from './errors';
import FastlyParams from './fastly-params';
import mapOptions from './map-options';
import splice from './splice-response';
const HASTILY_HEADER = {
    NAME: 'X-Optimized',
    VALUE: 'hastily',
};
const streamableFileExtensions = new Set(Object.keys(sharp.format).filter((ext) => sharp.format[ext].input.stream));
// plus the one sharp doesn't mention outright, the alias to jpeg...
streamableFileExtensions.add('jpg');
// minus SVGs, which are widely supported in 2019 and we should not rasterize
streamableFileExtensions.delete('svg');
const imageExtensionRE = new RegExp(`\\.(?:${[...streamableFileExtensions].join('|')})$`);
/**
 * Use the `sharp.format` manifest to determine if the current request's file
 * extension matches a format that sharp can stream in to optimize.
 * @param req {Request}
 */
export const hasSupportedExtension = (req) => imageExtensionRE.test(req.path);
/**
 * Returns a new imageopto middleware for use in Express `app.use()`.
 * Won't do anything if the Express app isn't already serving images!
 *
 */
export function imageopto(filterOrOpts) {
    const options = {
        filter: hasSupportedExtension,
        force: false,
    };
    if (typeof filterOrOpts === 'function') {
        options.filter = filterOrOpts;
    }
    else if (typeof filterOrOpts === 'object') {
        options.filter = filterOrOpts.filter || options.filter;
        options.force = filterOrOpts.force;
        options.errorLog = options.quiet ? (_) => void 0 : filterOrOpts.errorLog;
    }
    const constructorLog = createLogger('middleware');
    constructorLog.debug('creating new middleware');
    if (options.filter === hasSupportedExtension) {
        constructorLog.debug('middleware filtering req.path for %s', imageExtensionRE.source);
    }
    const requestLog = createLogger('request');
    return function hastily(req, res, next) {
        const reqLog = requestLog.child({ req });
        if (!options.filter(req)) {
            reqLog.debug('did not pass supplied filter function');
            return next();
        }
        splice(req, res, next, () => {
            // determine if the entity should be transformed
            if (!shouldTransform(req, res, reqLog, options)) {
                return false;
            }
            reqLog.debug('hastily will handle this image by transforming the response through sharp');
            vary(res, 'Accept');
            res.setHeader(HASTILY_HEADER.NAME, HASTILY_HEADER.VALUE);
            // image opto stream
            const params = new FastlyParams(new Map(new URLSearchParams(req.query).entries()), req, res);
            const sharpStream = mapOptions(params);
            const warnings = params.getWarnings();
            if (warnings.length > 0) {
                const requestErrors = new RequestErrors(req.url, warnings);
                if (options.errorLog) {
                    options.errorLog(requestErrors);
                }
                else {
                    reqLog.warn(requestErrors.toString());
                }
            }
            reqLog.debug('mapped options and created sharp stream');
            return sharpStream;
        });
    };
}
const cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/;
/**
 * Determine if the entity should be transformed.
 * @private
 */
function shouldTransform(req, res, reqLog, options) {
    if (req.method !== 'GET') {
        reqLog.debug('no transform: request method must be GET but is %s', req.method);
        return false;
    }
    if (res.statusCode > 299 || res.statusCode < 200) {
        reqLog.debug('no transform: res.statusCode must be 2xx but is %s', res.statusCode);
        return false;
    }
    const cacheControl = res.getHeader('Cache-Control');
    // Don't optimize for Cache-Control: no-transform
    // https://tools.ietf.org/html/rfc7234#section-5.2.2.4
    if (cacheControl &&
        cacheControlNoTransformRegExp.test(cacheControl.toString())) {
        reqLog.debug('no transform: cache control header: "%s"', cacheControl);
        return false;
    }
    if (!options.force) {
        // Don't optimize if we've already done it somewhere
        const hastilyHeader = res.getHeader(HASTILY_HEADER.NAME);
        if (hastilyHeader === HASTILY_HEADER.VALUE) {
            reqLog.warn('no transform: header %o, hastily alrady transformed this earlier', HASTILY_HEADER);
            return false;
        }
        // Don't optimize if Fastly has already done it for us
        const fastlyHeader = res.getHeader('fastly-io-info');
        if (fastlyHeader) {
            reqLog.debug('no transform: fastly already transformed according to fastly-io-info header: "%s"', fastlyHeader);
            return false;
        }
    }
    const contentType = res.getHeader('content-type');
    const extension = mime.extension(contentType);
    if (!extension) {
        reqLog.error('no transform: no valid content-type could not be detected');
        return false;
    }
    const sharpFormatCapabilities = sharp.format[extension];
    if (!sharpFormatCapabilities || !sharpFormatCapabilities.input.stream) {
        reqLog.error('no transform: sharp does not support input of type "%s"', contentType);
        return false;
    }
    const contentEncoding = res.getHeader('content-encoding');
    if (contentEncoding &&
        typeof contentEncoding === 'string' &&
        contentEncoding.trim()) {
        reqLog.error('no transform: image is compressed with content-encoding "%s"; hastily does not support decompressing images. The server *should not* be compressing images! HTTP content encoding is only recommended for text documents. Binary files will not get smaller, and may evem get larger!');
    }
    return true;
}
//# sourceMappingURL=data:application/json;base64, 