import {describe, test, expect} from 'vitest';
import {CrossTileSymbolIndex, KDBUSH_THRESHHOLD} from './cross_tile_symbol_index';
import {OverscaledTileID} from '../tile/tile_id';
import {type StyleLayer} from '../style/style_layer';

const styleLayer = {
    id: 'test'
} as StyleLayer;

const makeSymbolInstance = (x, y, key): any => {
    return {
        anchorX: x,
        anchorY: y,
        key
    };
};

const makeTile = (tileID, symbolInstances): any => {
    const bucket = {
        symbolInstances: {
            get(i) { return symbolInstances[i]; },
            length: symbolInstances.length
        },
        layerIds: ['test']
    };
    return {
        tileID,
        getBucket: () => bucket,
        latestFeatureIndex: {}
    };
};

describe('CrossTileSymbolIndex.addLayer', () => {

    test('matches ids', () => {
        const index = new CrossTileSymbolIndex();

        const mainID = new OverscaledTileID(6, 0, 6, 8, 8);
        const mainInstances = [
            makeSymbolInstance(1000, 1000, 'Detroit'),
            makeSymbolInstance(2000, 2000, 'Toronto')
        ];
        const mainTile = makeTile(mainID, mainInstances);

        index.addLayer(styleLayer, [mainTile], 0);
        // Assigned new IDs
        expect(mainInstances[0].crossTileID).toBe(1);
        expect(mainInstances[1].crossTileID).toBe(2);

        const childID = new OverscaledTileID(7, 0, 7, 16, 16);
        const childInstances = [
            makeSymbolInstance(2000, 2000, 'Detroit'),
            makeSymbolInstance(2000, 2000, 'Windsor'),
            makeSymbolInstance(3000, 3000, 'Toronto'),
            makeSymbolInstance(4001, 4001, 'Toronto')
        ];
        const childTile = makeTile(childID, childInstances);

        index.addLayer(styleLayer, [mainTile, childTile], 0);
        // matched parent tile
        expect(childInstances[0].crossTileID).toBe(1);
        // does not match because of different key
        expect(childInstances[1].crossTileID).toBe(3);
        // does not match because of different location
        expect(childInstances[2].crossTileID).toBe(4);
        // matches with a slightly different location
        expect(childInstances[3].crossTileID).toBe(2);

        const parentID = new OverscaledTileID(5, 0, 5, 4, 4);
        const parentInstances = [
            makeSymbolInstance(500, 500, 'Detroit')
        ];
        const parentTile = makeTile(parentID, parentInstances);

        index.addLayer(styleLayer, [mainTile, childTile, parentTile], 0);
        // matched child tile
        expect(parentInstances[0].crossTileID).toBe(1);

        const grandchildID = new OverscaledTileID(8, 0, 8, 32, 32);
        const grandchildInstances = [
            makeSymbolInstance(4000, 4000, 'Detroit'),
            makeSymbolInstance(4000, 4000, 'Windsor')
        ];
        const grandchildTile = makeTile(grandchildID, grandchildInstances);

        index.addLayer(styleLayer, [mainTile], 0);
        index.addLayer(styleLayer, [mainTile, grandchildTile], 0);
        // Matches the symbol in `mainBucket`
        expect(grandchildInstances[0].crossTileID).toBe(1);
        // Does not match the previous value for Windsor because that tile was removed
        expect(grandchildInstances[1].crossTileID).toBe(5);

    });

    test('overwrites ids when re-adding', () => {
        const index = new CrossTileSymbolIndex();

        const mainID = new OverscaledTileID(6, 0, 6, 8, 8);
        const mainInstances = [makeSymbolInstance(1000, 1000, 'Detroit')];
        const mainTile = makeTile(mainID, mainInstances);

        const childID = new OverscaledTileID(7, 0, 7, 16, 16);
        const childInstances = [makeSymbolInstance(2000, 2000, 'Detroit')];
        const childTile = makeTile(childID, childInstances);

        // assigns a new id
        index.addLayer(styleLayer, [mainTile], 0);
        expect(mainInstances[0].crossTileID).toBe(1);

        // removes the tile
        index.addLayer(styleLayer, [], 0);

        // assigns a new id
        index.addLayer(styleLayer, [childTile], 0);
        expect(childInstances[0].crossTileID).toBe(2);

        // overwrites the old id to match the already-added tile
        index.addLayer(styleLayer, [mainTile, childTile], 0);
        expect(mainInstances[0].crossTileID).toBe(2);
        expect(childInstances[0].crossTileID).toBe(2);

    });

    test('does not duplicate ids within one zoom level', () => {
        const index = new CrossTileSymbolIndex();

        const mainID = new OverscaledTileID(6, 0, 6, 8, 8);
        const mainInstances = [
            makeSymbolInstance(1000, 1000, ''), // A
            makeSymbolInstance(1000, 1000, '')  // B
        ];
        const mainTile = makeTile(mainID, mainInstances);

        const childID = new OverscaledTileID(7, 0, 7, 16, 16);
        const childInstances = [
            makeSymbolInstance(2000, 2000, ''), // A'
            makeSymbolInstance(2000, 2000, ''), // B'
            makeSymbolInstance(2000, 2000, '')  // C'
        ];
        const childTile = makeTile(childID, childInstances);

        // assigns new ids
        index.addLayer(styleLayer, [mainTile], 0);
        expect(mainInstances[0].crossTileID).toBe(1);
        expect(mainInstances[1].crossTileID).toBe(2);

        const layerIndex = index.layerIndexes[styleLayer.id];
        expect(Object.keys(layerIndex.usedCrossTileIDs[6])).toEqual(['1', '2']);

        // copies parent ids without duplicate ids in this tile
        index.addLayer(styleLayer, [childTile], 0);
        expect(childInstances[0].crossTileID).toBe(1); // A' copies from A
        expect(childInstances[1].crossTileID).toBe(2); // B' copies from B
        expect(childInstances[2].crossTileID).toBe(3); // C' gets new ID

        // Updates per-zoom usedCrossTileIDs
        expect(Object.keys(layerIndex.usedCrossTileIDs[6])).toEqual([]);
        expect(Object.keys(layerIndex.usedCrossTileIDs[7])).toEqual(['1', '2', '3']);

    });

    test('does not regenerate ids for same zoom', () => {
        const index = new CrossTileSymbolIndex();

        const tileID = new OverscaledTileID(6, 0, 6, 8, 8);
        const firstInstances = [
            makeSymbolInstance(1000, 1000, ''), // A
            makeSymbolInstance(1000, 1000, '')  // B
        ];
        const firstTile = makeTile(tileID, firstInstances);

        const secondInstances = [
            makeSymbolInstance(1000, 1000, ''), // A'
            makeSymbolInstance(1000, 1000, ''), // B'
            makeSymbolInstance(1000, 1000, ''), // C'
        ];
        const secondTile = makeTile(tileID, secondInstances);

        // assigns new ids
        index.addLayer(styleLayer, [firstTile], 0);
        expect(firstInstances[0].crossTileID).toBe(1);
        expect(firstInstances[1].crossTileID).toBe(2);

        const layerIndex = index.layerIndexes[styleLayer.id];
        expect(Object.keys(layerIndex.usedCrossTileIDs[6])).toEqual(['1', '2']);

        // uses same ids when tile gets updated
        index.addLayer(styleLayer, [secondTile], 0);
        expect(secondInstances[0].crossTileID).toBe(1); // A' copies from A
        expect(secondInstances[1].crossTileID).toBe(2); // B' copies from B
        expect(secondInstances[2].crossTileID).toBe(3); // C' gets new ID

        expect(Object.keys(layerIndex.usedCrossTileIDs[6])).toEqual(['1', '2', '3']);

    });

    test('reuses indexes when longitude is wrapped', () => {
        const index = new CrossTileSymbolIndex();
        const longitude = 370;

        const tileID = new OverscaledTileID(6, 1, 6, 8, 8);
        const firstInstances = [
            makeSymbolInstance(1000, 1000, ''), // A
        ];
        const tile = makeTile(tileID, firstInstances);

        index.addLayer(styleLayer, [tile], longitude);
        expect(firstInstances[0].crossTileID).toBe(1); // A

        tile.tileID = tileID.wrapped();

        index.addLayer(styleLayer, [tile], longitude % 360);
        expect(firstInstances[0].crossTileID).toBe(1);

    });

    test('indexes data for findMatches perf', () => {
        const index = new CrossTileSymbolIndex();

        const mainID = new OverscaledTileID(6, 0, 6, 8, 8);
        const childID = new OverscaledTileID(7, 0, 7, 16, 16);

        const mainInstances: any[] = [];
        const childInstances: any[] = [];

        for (let i = 0; i < KDBUSH_THRESHHOLD + 1; i++) {
            mainInstances.push(makeSymbolInstance(0, 0, ''));
            childInstances.push(makeSymbolInstance(0, 0, ''));
        }
        const mainTile = makeTile(mainID, mainInstances);
        const childTile = makeTile(childID, childInstances);
        index.addLayer(styleLayer, [mainTile], 0);
        index.addLayer(styleLayer, [childTile], 0);

        // check that we matched the parent tile
        expect(childInstances[0].crossTileID).toBe(1);

    });
});

test('CrossTileSymbolIndex.pruneUnusedLayers', () => {

    const index = new CrossTileSymbolIndex();

    const tileID = new OverscaledTileID(6, 0, 6, 8, 8);
    const instances = [
        makeSymbolInstance(1000, 1000, ''), // A
        makeSymbolInstance(1000, 1000, '')  // B
    ];
    const tile = makeTile(tileID, instances);

    // assigns new ids
    index.addLayer(styleLayer, [tile], 0);
    expect(instances[0].crossTileID).toBe(1);
    expect(instances[1].crossTileID).toBe(2);
    expect(index.layerIndexes[styleLayer.id]).toBeTruthy();

    // remove styleLayer
    index.pruneUnusedLayers([]);
    expect(index.layerIndexes[styleLayer.id]).toBeFalsy();

});

