import sharp from "sharp"
import { epsg3857extent } from "../constants"
import { Coordinate, Extent } from "../interfaces/map"
import path from "path"
import fs from 'fs'

function getTilesPerSide(zoom: number) {
    return Math.pow(2, zoom)
}

function normalize(value: number, min: number, max: number) {
    return (value - min) / (max - min)
}

function getTileIndex(normalized: number, tilesPerSide: number) {
    return Math.floor(normalized * tilesPerSide)
}

function getGridCellPosition(x: number, y: number, extent: Extent, zoom: number) {
    const tilesPerSide = getTilesPerSide(zoom)
    const minX = extent[0]
    const minY = extent[1]
    const maxX = extent[2]
    const maxY = extent[3]
    const xNormalized = normalize(x, minX, maxX)
    const yNormalized = normalize(y, minY, maxY)
    const tileX = getTileIndex(xNormalized, tilesPerSide)
    const tileY = getTileIndex(1 - yNormalized, tilesPerSide)
    return { tileX, tileY }
}

function calculateRotationAngle(bottomLeft: Coordinate, bottomRight: Coordinate) {
    const deltaX = bottomRight.x - bottomLeft.x
    const deltaY = bottomRight.y - bottomLeft.y
    const angle = -Math.atan2(deltaY, deltaX)
    return angle
}

function roundUpToNearest(number: number, mod: number) {
    return Math.floor(number / mod) * mod
}

export async function generateTilesForZoomLevel(uploadDir: string, tileFolder: string, file: Express.Multer.File, polygonExtent: Extent, bottomLeft: Coordinate, topLeft: Coordinate, topRight: Coordinate, bottomRight: Coordinate, zoomLevel: number) {
    const angleDegrees = calculateRotationAngle(bottomLeft, bottomRight) * 180 / Math.PI

    const { tileX: blX, tileY: blY } = getGridCellPosition(bottomLeft.x, bottomLeft.y, epsg3857extent, zoomLevel)
    const { tileX: tlX, tileY: tlY } = getGridCellPosition(topLeft.x, topLeft.y, epsg3857extent, zoomLevel)
    const { tileX: trX, tileY: trY } = getGridCellPosition(topRight.x, topRight.y, epsg3857extent, zoomLevel)
    const { tileX: brX, tileY: brY } = getGridCellPosition(bottomRight.x, topRight.y, epsg3857extent, zoomLevel)

    const minX = Math.min(blX, tlX, trX, brX)
    const maxX = Math.max(blX, tlX, trX, brX)
    const minY = Math.min(blY, tlY, trY, brY)
    const maxY = Math.max(blY, tlY, trY, brY)

    const mapWidth = Math.abs(epsg3857extent[0] - epsg3857extent[2])
    const mapHeight = Math.abs(epsg3857extent[1] - epsg3857extent[3])

    const tilesH = Math.sqrt(Math.pow(4, zoomLevel))
    const tileWidth = mapWidth / (Math.sqrt(Math.pow(4, zoomLevel)))
    const tileHeight = mapHeight / (Math.sqrt(Math.pow(4, zoomLevel)))


    let minPosX = minX - (tilesH / 2)
    let maxPosX = maxX - (tilesH / 2) + 1
    let minPosY = -(minY - (tilesH / 2))
    let maxPosY = -(maxY - (tilesH / 2) + 1)

    const newMinX = tileWidth * minPosX
    const newMaxX = tileWidth * maxPosX
    const newMinY = tileHeight * maxPosY
    const newMaxY = tileHeight * minPosY


    const paddingLeft = Math.abs(polygonExtent[0] - newMinX)
    const paddingRight = Math.abs(polygonExtent[2] - newMaxX)
    const paddingTop = Math.abs(polygonExtent[3] - newMaxY)
    const paddingBottom = Math.abs(polygonExtent[1] - newMinY)

    const pixelWidth = Math.abs(minX - (maxX + 1)) * 256
    const pixelHeight = Math.abs(minY - (maxY + 1)) * 256

    const width = Math.abs(newMinX - newMaxX)

    try {
        let perPixel = width / pixelWidth

        // constraint to original image width
        const imageMetadata = await sharp(file.path).metadata().then(res => {
            if (res.width) {
                perPixel = pixelWidth <= res.width ? perPixel : width / res.width
            }
        })

        const paddingLeftPixel = paddingLeft / perPixel
        const paddingRightPixel = paddingRight / perPixel
        const paddingTopPixel = paddingTop / perPixel
        const paddingBottomPixel = paddingBottom / perPixel

        const boundsWidthPixel = Math.abs(polygonExtent[0] - polygonExtent[2]) / perPixel
        const boundsHeightPixel = Math.abs(polygonExtent[1] - polygonExtent[3]) / perPixel

        if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString()))) {
            fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString()), { recursive: true });
        }

        const initialZoomImage = await sharp(path.join(uploadDir, file.filename))
            .rotate(Math.ceil(angleDegrees), {
                background: '#00000000'
            })
            .resize({
                width: Math.ceil(boundsWidthPixel),
                height: Math.ceil(boundsHeightPixel),
                background: '#00000000'
            })
            .extend({
                top: Math.ceil(paddingTopPixel),
                left: Math.ceil(paddingLeftPixel),
                bottom: Math.ceil(paddingBottomPixel),
                right: Math.ceil(paddingRightPixel),
                background: '#00000000'
            })
            .toFormat('png')
            .toBuffer({ resolveWithObject: true })

        if (initialZoomImage) {
            await sharp(initialZoomImage.data.buffer)
                .resize({
                    width: roundUpToNearest(Math.ceil(boundsWidthPixel) + Math.ceil(paddingLeftPixel) + Math.ceil(paddingRightPixel), Math.abs(minX - (maxX + 1))),
                    height: roundUpToNearest(Math.ceil(boundsHeightPixel) + Math.ceil(paddingTopPixel) + Math.ceil(paddingBottomPixel), Math.abs(minY - (maxY + 1))),
                })
                .toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
                .then(async (res) => {
                    let left = 0
                    for (let x = minX; x <= maxX; x++) {
                        if (!fs.existsSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()))) {
                            fs.mkdirSync(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString()), { recursive: true });
                        }

                        let top = 0
                        for (let y = minY; y <= maxY; y++) {
                            console.log(`z: ${zoomLevel} x: ${x} y: ${y}`)

                            try {
                                await sharp(path.join(tileFolder, 'custom', zoomLevel.toString(), zoomLevel.toString() + '.png'))
                                    .extract({
                                        width: res.width / Math.abs(minX - (maxX + 1)),
                                        height: res.height / Math.abs(minY - (maxY + 1)),
                                        left: left,
                                        top: top
                                    })
                                    .toFile(path.join(tileFolder, 'custom', zoomLevel.toString(), x.toString(), y.toString() + '.png'))
                                    .then(() => {
                                        top = top + res.height / Math.abs(minY - (maxY + 1))
                                    })
                            } catch (error) {
                                console.log(error)
                            }
                        }
                        left = left + res.width / Math.abs(minX - (maxX + 1))
                    }
                })
        }
    } catch (error) {
        console.log(error)
    }
}