import { TileSetNameParser, TileSetType, } from '@basemaps/config';
import { Bounds, Epsg, ImageFormat, TileMatrixSet, TileMatrixSets, VectorFormat } from '@basemaps/geo';
import { Config, Env, fsa, titleizeImageryName } from '@basemaps/shared';
import { Tiler } from '@basemaps/tiler';
import { TileMakerSharp } from '@basemaps/tiler-sharp';
import { CogTiff } from '@cogeotiff/core';
import { HttpHeader, LambdaHttpResponse } from '@linzjs/lambda';
import pLimit from 'p-limit';
import { NotFound, NotModified } from './routes/response.js';
import { TileEtag } from './routes/tile.etag.js';
import { St } from './source.tracer.js';
import { TiffCache } from './tiff.cache.js';
import { TileSets } from './tile.set.cache.js';
const LoadingQueue = pLimit(Env.getNumber(Env.TiffConcurrency, 25));
export const TileComposer = new TileMakerSharp(256);
const DefaultResizeKernel = { in: 'lanczos3', out: 'lanczos3' };
const DefaultBackground = { r: 0, g: 0, b: 0, alpha: 0 };
export function getTiffName(name) {
    if (name.endsWith('.tif') || name.endsWith('.tiff'))
        return name;
    return `${name}.tiff`;
}
export class TileSetRaster {
    constructor(name, tileMatrix) {
        this.type = TileSetType.Raster;
        this.components = TileSetNameParser.parse(name);
        this.tileMatrix = tileMatrix;
        this.tiler = new Tiler(this.tileMatrix);
    }
    get id() {
        return TileSets.id(this.fullName, this.tileMatrix);
    }
    get fullName() {
        return TileSetNameParser.componentsToName(this.components);
    }
    get title() {
        var _a, _b;
        return (_b = (_a = this.tileSet) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : this.components.name;
    }
    get description() {
        var _a, _b;
        return (_b = (_a = this.tileSet) === null || _a === void 0 ? void 0 : _a.description) !== null && _b !== void 0 ? _b : '';
    }
    get extent() {
        var _a;
        return (_a = this.extentOverride) !== null && _a !== void 0 ? _a : this.tileMatrix.extent;
    }
    /** Preferred default imagery format */
    get format() {
        var _a;
        return (_a = this.tileSet.format) !== null && _a !== void 0 ? _a : ImageFormat.Webp;
    }
    async init(record) {
        this.tileSet = record;
        this.imagery = await Config.getAllImagery(this.tileSet.layers, this.tileMatrix.projection);
    }
    async initTiffs(tile, log) {
        const tiffs = this.getTiffsForTile(tile, log);
        let failed = false;
        // Remove any tiffs that failed to load
        const promises = tiffs.map((c) => {
            return LoadingQueue(async () => {
                try {
                    await c.init();
                }
                catch (error) {
                    log.warn({ error, tiff: c.source.uri }, 'TiffLoadFailed');
                    failed = true;
                }
            });
        });
        await Promise.all(promises);
        if (failed)
            return tiffs.filter((f) => f.images.length > 0);
        return tiffs;
    }
    async tile(req, xyz) {
        var _a, _b;
        if (xyz.ext === VectorFormat.MapboxVectorTiles)
            return NotFound;
        const tiffs = await this.initTiffs(xyz, req.log);
        const layers = await this.tiler.tile(tiffs, xyz.x, xyz.y, xyz.z);
        // Generate a unique hash given the full URI, the layers used and a renderId
        const cacheKey = TileEtag.generate(layers, xyz);
        req.set('layers', layers.length);
        if (TileEtag.isNotModified(req, cacheKey))
            return NotModified;
        const res = await TileComposer.compose({
            layers,
            format: xyz.ext,
            background: (_a = this.tileSet.background) !== null && _a !== void 0 ? _a : DefaultBackground,
            resizeKernel: (_b = this.tileSet.resizeKernel) !== null && _b !== void 0 ? _b : DefaultResizeKernel,
            metrics: req.timer,
        });
        req.set('layersUsed', res.layers);
        req.set('bytes', res.buffer.byteLength);
        const response = new LambdaHttpResponse(200, 'ok');
        response.header(HttpHeader.ETag, cacheKey);
        response.header(HttpHeader.CacheControl, 'public, max-age=604800');
        response.buffer(res.buffer, 'image/' + xyz.ext);
        return response;
    }
    /**
     * Get a list of tiffs in the rendering order that is needed to render the tile
     * @param tms tile matrix set to describe the tiling scheme
     * @param tile tile to render
     */
    getTiffsForTile(tile, log) {
        const output = [];
        const tileBounds = this.tileMatrix.tileToSourceBounds(tile);
        // All zoom level config is stored as Google zoom levels
        const filterZoom = TileMatrixSet.convertZoomLevel(tile.z, this.tileMatrix, TileMatrixSets.get(Epsg.Google));
        for (const layer of this.tileSet.layers) {
            if (layer.maxZoom != null && filterZoom > layer.maxZoom)
                continue;
            if (layer.minZoom != null && filterZoom < layer.minZoom)
                continue;
            const imgId = layer[this.tileMatrix.projection.code];
            if (imgId == null) {
                log === null || log === void 0 ? void 0 : log.warn({ layer: layer.name, projection: this.tileMatrix.projection.code }, 'Failed to lookup imagery');
                continue;
            }
            const imagery = this.imagery.get(imgId);
            if (imagery == null) {
                log === null || log === void 0 ? void 0 : log.warn({ layer: layer.name, projection: this.tileMatrix.projection.code, imgId }, 'Failed to lookup imagery');
                continue;
            }
            if (!tileBounds.intersects(Bounds.fromJson(imagery.bounds)))
                continue;
            for (const tiff of this.getCogsForTile(imagery, tileBounds))
                output.push(tiff);
        }
        return output;
    }
    getCogsForTile(record, tileBounds) {
        const output = [];
        for (const c of record.files) {
            if (!tileBounds.intersects(Bounds.fromJson(c)))
                continue;
            const tiffPath = fsa.join(record.uri, getTiffName(c.name));
            let existing = TiffCache.get(tiffPath);
            if (existing == null) {
                const source = fsa.source(tiffPath);
                if (source == null) {
                    throw new Error(`Failed to create CogSource from  ${tiffPath}`);
                }
                St.trace(source);
                existing = new CogTiff(source);
                TiffCache.set(tiffPath, existing);
            }
            output.push(existing);
        }
        return output;
    }
    /** Look up imagery by imageryId or by image name */
    findImagery(imgId) {
        const existing = this.imagery.get(imgId);
        if (existing != null)
            return existing;
        for (const img of this.imagery.values()) {
            if (img.name === imgId)
                return img;
        }
        return null;
    }
    child(imgId) {
        var _a, _b, _c;
        const image = this.findImagery(imgId);
        if (image == null)
            return null;
        const childName = TileSetNameParser.componentsToName({ ...this.components, layer: image.name });
        const child = new TileSetRaster(childName, this.tileMatrix);
        // use parent data as prototype for child;
        child.tileSet = { ...this.tileSet };
        child.tileSet.background = undefined;
        const title = (_b = (_a = this.tileSet) === null || _a === void 0 ? void 0 : _a.title) !== null && _b !== void 0 ? _b : (_c = this.tileSet) === null || _c === void 0 ? void 0 : _c.name;
        child.tileSet.title = `${title} ${titleizeImageryName(image.name)}`;
        child.extentOverride = Bounds.fromJson(image.bounds);
        const layer = { name: image.name, minZoom: 0, maxZoom: 100 };
        layer[this.tileMatrix.projection.code] = image.id;
        child.tileSet.layers = [layer];
        child.imagery = new Map();
        child.imagery.set(image.id, image);
        return child;
    }
}
