// The main package is the entry point for the application.
package main

// Import necessary libraries.
import (
	"bytes"
	"context"
	"embed" // Used for embedding files into the binary.
	"encoding/json"
	"flag"
	"fmt"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"io"
	"io/fs"
	"log"
	"math"
	"math/rand"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/gorilla/websocket" // WebSocket library for real-time communication.
)

//go:embed templates/index.html
var indexHTML []byte // Embeds the index.html file into the binary.

//go:embed config/map_sources.json
var mapSourcesJSON []byte // Embeds the map_sources.json file into the binary.

//go:embed static/*
var staticFiles embed.FS

// upgrader is used to upgrade HTTP connections to WebSocket connections.
var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024, // Size of the read buffer.
	WriteBufferSize: 1024, // Size of the write buffer.
}

// Global variables used throughout the application.
var (
	mapSources       map[string]string  // Stores the available map sources.
	downloadCancel   context.CancelFunc // Function to cancel an ongoing download.
	downloading      bool               // Flag to indicate if a download is in progress.
	downloadingMutex sync.Mutex         // Mutex to protect access to the downloading flag.
	cacheDir         *string
	maxWorkers       *int
	rateLimit        *int
	maxRetries       *int
	userAgent        *string
)

// generateUserAgent returns a basic user agent that identifies as a tile downloader
func generateUserAgent() string {
	osName := runtime.GOOS
	var osInfo string

	switch osName {
	case "darwin":
		osInfo = "Macintosh"
	case "linux":
		osInfo = "Linux"
	case "windows":
		osInfo = "Windows"
	default:
		osInfo = osName
	}

	version := time.Now().Format("060102")
	return fmt.Sprintf("mesh/%s (%s)", version, osInfo)
}

// Tile represents a single map tile with X, Y coordinates and zoom level Z.
type Tile struct {
	X, Y, Z uint32
}

// BoundingBox represents a geographical area with North, South, East, and West boundaries.
type BoundingBox struct {
	North, South, East, West float64
}

// LatLng represents a geographical point with latitude and longitude.
type LatLng struct {
	Lat float64 `json:"lat"` // Latitude
	Lng float64 `json:"lng"` // Longitude
}

// DownloadRequest represents a request to download map tiles for a specific area.
type DownloadRequest struct {
	Polygons      [][]LatLng `json:"polygons"`        // The polygons defining the download area.
	MinZoom       int        `json:"min_zoom"`        // The minimum zoom level to download.
	MaxZoom       int        `json:"max_zoom"`        // The maximum zoom level to download.
	MapStyle      string     `json:"map_style"`       // The URL of the map tile server.
	ConvertTo8Bit bool       `json:"convert_to_8bit"` // Whether to convert images to 8-bit PNG.
}

// WorldDownloadRequest represents a request to download map tiles for the entire world.
type WorldDownloadRequest struct {
	MapStyle      string `json:"map_style"`       // The URL of the map tile server.
	ConvertTo8Bit bool   `json:"convert_to_8bit"` // Whether to convert images to 8-bit PNG.
}

// WSMessage represents a WebSocket message with a type and data.
type WSMessage struct {
	Type string      `json:"type"` // The type of the message (e.g., "start_download").
	Data interface{} `json:"data"` // The data associated with the message.
}

// main is the entry point of the application.
func main() {
	// Command line flags
	port := flag.Int("port", 8080, "Port number for the server")
	cacheDir = flag.String("maps-directory", "maps", "Directory for storing map tiles. This is where the downloaded tiles will be saved.")
	maxWorkers = flag.Int("max-workers", 4, "Number of concurrent download workers (max: 10)")
	rateLimit = flag.Int("rate-limit", 10, "Maximum number of tiles to download per second (max: 50)")
	maxRetries = flag.Int("max-retries", 3, "Maximum number of retries for downloading a tile")
	userAgent = flag.String("user-agent", generateUserAgent(), "User-Agent header for HTTP requests")
	help := flag.Bool("help", false, "Show help message")

	flag.Parse()

	if *help {
		flag.Usage()
		return
	}

	if *maxWorkers > 10 {
		log.Fatalf("max-workers cannot exceed 10 (got %d)", *maxWorkers)
	}

	if *rateLimit > 50 {
		log.Fatalf("rate-limit cannot exceed 50 (got %d)", *rateLimit)
	}

	// Create cache directory if it doesn't exist.
	if err := os.MkdirAll(*cacheDir, 0755); err != nil {
		log.Fatalf("Failed to create cache directory: %v", err)
	}

	// Load map sources from the embedded JSON file.
	if err := json.Unmarshal(mapSourcesJSON, &mapSources); err != nil {
		log.Fatalf("Failed to load map sources: %v", err)
	}

	// Register HTTP handlers for different routes.
	http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
		r.URL.Path = "/static/favicon.ico"
		http.FileServer(http.FS(staticFiles)).ServeHTTP(w, r)
	})
	http.HandleFunc("/", serveHome)
	http.HandleFunc("/get_map_sources", getMapSources)
	http.HandleFunc("/ws", wsHandler)

	http.HandleFunc("/tiles/", serveTile)
	http.HandleFunc("/get_cached_tiles/", getCachedTiles)

	staticFS, err := fs.Sub(staticFiles, "static")
	if err != nil {
		log.Fatal(err)
	}
	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))

	// Start the HTTP server on port 8080.
	addr := fmt.Sprintf(":%d", *port)
	log.Printf("Starting server on %s", addr)
	if err := http.ListenAndServe(addr, nil); err != nil {
		log.Fatalf("Error starting server: %v", err)
	}
}

// serveHome serves the main HTML page.
func serveHome(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html")
	if _, err := w.Write(indexHTML); err != nil {
		log.Printf("Could not write response: %v", err)
	}
}

// getMapSources serves the available map sources as JSON.
func getMapSources(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	if _, err := w.Write(mapSourcesJSON); err != nil {
		log.Printf("Could not write response: %v", err)
	}
}

// wsHandler handles WebSocket connections.
func wsHandler(w http.ResponseWriter, r *http.Request) {
	// Upgrade the HTTP connection to a WebSocket connection.
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}
	defer func() {
		if err := conn.Close(); err != nil {
			log.Printf("Could not close websocket connection: %v", err)
		}
	}()

	// Loop to read messages from the WebSocket connection.
	for {
		messageType, p, err := conn.ReadMessage()
		if err != nil {
			log.Println(err)
			return
		}
		if messageType == websocket.TextMessage {
			var msg WSMessage
			if err := json.Unmarshal(p, &msg); err != nil {
				log.Println("Error unmarshalling message:", err)
				continue
			}

			// Handle different message types.
			switch msg.Type {
			case "start_download":
				var req DownloadRequest
				b, _ := json.Marshal(msg.Data)
				if err := json.Unmarshal(b, &req); err != nil {
					sendError(conn, "Invalid download request")
					continue
				}
				go handleStartDownload(conn, req)
			case "start_world_download":
				var req WorldDownloadRequest
				b, _ := json.Marshal(msg.Data)
				if err := json.Unmarshal(b, &req); err != nil {
					sendError(conn, "Invalid world download request")
					continue
				}
				go handleStartWorldDownload(conn, req)
			case "cancel_download":
				handleCancelDownload(conn)
			}
		}
	}
}

// handleStartDownload starts a new download process for a defined area.
func handleStartDownload(conn *websocket.Conn, req DownloadRequest) {
	// Lock the mutex to ensure only one download runs at a time.
	downloadingMutex.Lock()
	if downloading {
		sendError(conn, "Another download is already in progress.")
		downloadingMutex.Unlock()
		return
	}
	downloading = true
	downloadingMutex.Unlock()

	// Defer setting the downloading flag to false.
	defer func() {
		downloadingMutex.Lock()
		downloading = false
		downloadingMutex.Unlock()
	}()

	log.Printf("Starting download for area: %v, zoom: %d-%d, map style: %s", req.Polygons, req.MinZoom, req.MaxZoom, req.MapStyle)

	// Create a new context to allow for cancellation.
	var ctx context.Context
	ctx, downloadCancel = context.WithCancel(context.Background())

	// Get the style name and cache directory.
	styleName := getStyleName(req.MapStyle)
	styleCacheDir := getStyleCacheDir(styleName)

	// Validate the zoom range.
	if req.MinZoom < 0 || req.MaxZoom > 19 || req.MinZoom > req.MaxZoom {
		sendError(conn, "Invalid zoom range (must be 0-19, min <= max)")
		return
	}
	// Validate the polygons.
	if len(req.Polygons) == 0 {
		sendError(conn, "No polygons provided")
		return
	}

	// Get the list of tiles to download.
	tilesToDownload := getTilesForPolygons(req.Polygons, req.MinZoom, req.MaxZoom)

	// Start the tile download process.
	downloadTiles(ctx, conn, tilesToDownload, req.MapStyle, styleCacheDir, req.ConvertTo8Bit)

	// If the download was not cancelled
	if ctx.Err() == nil {
		sendMessage(conn, "download_complete", nil)
	}
}

// handleStartWorldDownload starts a new download process for the entire world.
func handleStartWorldDownload(conn *websocket.Conn, req WorldDownloadRequest) {
	// Lock the mutex to ensure only one download runs at a time.
	downloadingMutex.Lock()
	if downloading {
		sendError(conn, "Another download is already in progress.")
		downloadingMutex.Unlock()
		return
	}
	downloading = true
	downloadingMutex.Unlock()

	// Defer setting the downloading flag to false.
	defer func() {
		downloadingMutex.Lock()
		downloading = false
		downloadingMutex.Unlock()
	}()

	log.Printf("Starting world download, map style: %s", req.MapStyle)

	// Create a new context to allow for cancellation.
	var ctx context.Context
	ctx, downloadCancel = context.WithCancel(context.Background())

	// Get the style name and cache directory.
	styleName := getStyleName(req.MapStyle)
	styleCacheDir := getStyleCacheDir(styleName)

	// Get the list of tiles to download for the world.
	tilesToDownload := getWorldTiles()

	// Start the tile download process.
	downloadTiles(ctx, conn, tilesToDownload, req.MapStyle, styleCacheDir, req.ConvertTo8Bit)

	// If the download was not cancelled
	if ctx.Err() == nil {
		sendMessage(conn, "download_complete", nil)
	}
}

// handleCancelDownload cancels an ongoing download.
func handleCancelDownload(conn *websocket.Conn) {
	if downloadCancel != nil {
		downloadCancel()
		log.Printf("Download cancelled by user")
		sendMessage(conn, "download_cancelled", nil)
	}
}

// downloadTiles downloads a list of tiles concurrently.
func downloadTiles(ctx context.Context, conn *websocket.Conn, tilesToDownload []Tile, mapStyle, styleCacheDir string, convertTo8Bit bool) {
	// Create a channel for WebSocket messages.
	msgChan := make(chan WSMessage)
	var writerWg sync.WaitGroup
	writerWg.Add(1)
	// Start a goroutine to send messages from the channel to the WebSocket connection.
	go func() {
		defer writerWg.Done()
		for msg := range msgChan {
			if err := conn.WriteJSON(msg); err != nil {
				log.Println("Error writing JSON to websocket:", err)
				return
			}
		}
	}()

	// Send a message indicating the download has started.
	msgChan <- WSMessage{Type: "download_started", Data: map[string]int{"total_tiles": len(tilesToDownload)}}

	// Use a WaitGroup to wait for all download goroutines to finish.
	var downloadWg sync.WaitGroup
	tileChan := make(chan Tile)

	// Start the download workers.
	for i := 0; i < *maxWorkers; i++ {
		downloadWg.Add(1)
		go func() {
			defer downloadWg.Done()
			for tile := range tileChan {
				select {
				case <-ctx.Done(): // Check if the download has been cancelled.
					return
				default:
					downloadTile(ctx, msgChan, tile, mapStyle, styleCacheDir, convertTo8Bit, *maxRetries)
				}
			}
		}()
	}

	// Rate limit the download of tiles.
	ticker := time.NewTicker(time.Second / time.Duration(*rateLimit))
	defer ticker.Stop()

DownloadLoop:
	for _, tile := range tilesToDownload {
		select {
		case <-ctx.Done():
			break DownloadLoop
		case <-ticker.C:
			tileChan <- tile
		}
	}
	close(tileChan)

	// Wait for all downloads to complete.
	downloadWg.Wait()
	close(msgChan)
	writerWg.Wait()

	// If the download was not cancelled, send a completion message.
	if ctx.Err() == nil {
		log.Printf("Download finished successfully")
		sendMessage(conn, "tiles_downloaded", nil)
	} else {
		log.Printf("Download failed or was cancelled")
	}
}

// downloadTile downloads a single map tile.
func downloadTile(ctx context.Context, msgChan chan<- WSMessage, tile Tile, mapStyle, styleCacheDir string, convertTo8Bit bool, maxRetries int) {
	// Construct the path to the tile file.
	tileDir := filepath.Join(styleCacheDir, fmt.Sprintf("%d/%d", tile.Z, tile.X))
	tilePath := filepath.Join(tileDir, fmt.Sprintf("%d.png", tile.Y))

	// Check if the tile already exists in the cache.
	if _, err := os.Stat(tilePath); err == nil {
		bounds := tileBounds(tile)
		msgChan <- WSMessage{Type: "tile_skipped", Data: map[string]float64{
			"west":  bounds.West,
			"south": bounds.South,
			"east":  bounds.East,
			"north": bounds.North,
		}}
		return
	}

	// Construct the URL for the tile.
	subdomain := []string{"a", "b", "c"}[rand.Intn(3)]
	url := strings.ReplaceAll(mapStyle, "{s}", subdomain)
	url = strings.ReplaceAll(url, "{z}", fmt.Sprintf("%d", tile.Z))
	url = strings.ReplaceAll(url, "{x}", fmt.Sprintf("%d", tile.X))
	url = strings.ReplaceAll(url, "{y}", fmt.Sprintf("%d", tile.Y))

	var err error
	for attempt := 0; attempt < maxRetries; attempt++ {
		select {
		case <-ctx.Done(): // Check for cancellation.
			return
		default:
		}

		var req *http.Request
		req, err = http.NewRequestWithContext(ctx, "GET", url, nil)
		if err != nil {
			log.Printf("Error creating request for tile %v: %v. Retrying...", tile, err)
			time.Sleep(time.Second * time.Duration(math.Pow(2, float64(attempt))))
			continue
		}

		// Set basic headers
		req.Header.Set("User-Agent", *userAgent)
		req.Header.Set("Accept", "image/*")

		// Add small random delay between requests (100-300ms)
		time.Sleep(time.Millisecond * time.Duration(100+rand.Intn(200)))

		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			log.Printf("Error downloading tile %v: %v. Retrying...", tile, err)
			time.Sleep(time.Second * time.Duration(math.Pow(2, float64(attempt))))
			continue
		}

		if resp.StatusCode != http.StatusOK {
			if err := resp.Body.Close(); err != nil {
				log.Printf("Could not close response body: %v", err)
			}
			log.Printf("Unexpected status code %d for tile %v. Retrying...", resp.StatusCode, tile)
			time.Sleep(time.Second * time.Duration(math.Pow(2, float64(attempt))))
			continue
		}

		body, err := io.ReadAll(resp.Body)
		if err := resp.Body.Close(); err != nil {
			log.Printf("Could not close response body: %v", err)
		}
		if err != nil {
			log.Printf("Error reading tile body for tile %v: %v. Retrying...", tile, err)
			time.Sleep(time.Second * time.Duration(math.Pow(2, float64(attempt))))
			continue
		}

		if err := os.MkdirAll(tileDir, 0755); err != nil {
			log.Printf("Error creating tile directory for tile %v: %v", tile, err)
			return // No point in retrying if we can't create the directory
		}

		// Convert the image to 8-bit PNG if requested.
		if convertTo8Bit {
			img, _, err := image.Decode(bytes.NewReader(body))
			if err == nil {
				paletted := image.NewPaletted(img.Bounds(), color.Palette{})
				draw.Draw(paletted, paletted.Rect, img, img.Bounds().Min, draw.Src)
				var buf bytes.Buffer
				if err := png.Encode(&buf, paletted); err == nil {
					body = buf.Bytes()
				}
			}
		}

		if err := os.WriteFile(tilePath, body, 0644); err != nil {
			log.Printf("Error writing tile %v: %v", tile, err)
			return // No point in retrying if we can't write the file
		}

		bounds := tileBounds(tile)
		msgChan <- WSMessage{Type: "tile_downloaded", Data: map[string]float64{
			"west":  bounds.West,
			"south": bounds.South,
			"east":  bounds.East,
			"north": bounds.North,
		}}
		return // Success!
	}

	// If all retries fail, send a failure message.
	log.Printf("Failed to download tile %v after %d attempts.", tile, maxRetries)
	msgChan <- WSMessage{Type: "tile_failed", Data: map[string]string{"tile": fmt.Sprintf("%d/%d/%d", tile.Z, tile.X, tile.Y)}}
}

// getTilesForPolygons calculates the tiles needed to cover the given polygons.
func getTilesForPolygons(polygonsData [][]LatLng, minZoom, maxZoom int) []Tile {
	var allTiles []Tile
	tileMap := make(map[Tile]bool)

	for _, polyData := range polygonsData {
		if len(polyData) < 3 {
			continue
		}

		minLat, minLon := 90.0, 180.0
		maxLat, maxLon := -90.0, -180.0
		for _, p := range polyData {
			if p.Lat < minLat {
				minLat = p.Lat
			}
			if p.Lat > maxLat {
				maxLat = p.Lat
			}
			if p.Lng < minLon {
				minLon = p.Lng
			}
			if p.Lng > maxLon {
				maxLon = p.Lng
			}
		}

		for z := minZoom; z <= maxZoom; z++ {
			tlx, tly := latLonToTile(maxLat, minLon, uint32(z))
			brx, bry := latLonToTile(minLat, maxLon, uint32(z))

			for x := tlx; x <= brx; x++ {
				for y := tly; y <= bry; y++ {
					tile := Tile{X: x, Y: y, Z: uint32(z)}
					if _, exists := tileMap[tile]; exists {
						continue
					}

					bounds := tileBounds(tile)

					// Check if the tile is completely inside the polygon
					if polygonContains(polyData, LatLng{Lat: bounds.North, Lng: bounds.West}) &&
						polygonContains(polyData, LatLng{Lat: bounds.North, Lng: bounds.East}) &&
						polygonContains(polyData, LatLng{Lat: bounds.South, Lng: bounds.West}) &&
						polygonContains(polyData, LatLng{Lat: bounds.South, Lng: bounds.East}) {
						allTiles = append(allTiles, tile)
						tileMap[tile] = true
						continue
					}

					// Check if the polygon is completely inside the tile
					polyInTile := true
					for _, p := range polyData {
						if !tileContains(bounds, p) {
							polyInTile = false
							break
						}
					}
					if polyInTile {
						allTiles = append(allTiles, tile)
						tileMap[tile] = true
						continue
					}

					// Check for intersection
					if polygonIntersects(polyData, bounds) {
						allTiles = append(allTiles, tile)
						tileMap[tile] = true
					}
				}
			}
		}
	}

	return allTiles
}

// tileContains checks if a tile contains a point.
func tileContains(bounds BoundingBox, point LatLng) bool {
	return point.Lat <= bounds.North && point.Lat >= bounds.South && point.Lng >= bounds.West && point.Lng <= bounds.East
}

// polygonIntersects checks if a polygon intersects with a tile.
func polygonIntersects(poly []LatLng, bounds BoundingBox) bool {
	// Check if any of the polygon's vertices are inside the tile
	for _, p := range poly {
		if tileContains(bounds, p) {
			return true
		}
	}

	// Check if any of the tile's corners are inside the polygon
	if polygonContains(poly, LatLng{Lat: bounds.North, Lng: bounds.West}) ||
		polygonContains(poly, LatLng{Lat: bounds.North, Lng: bounds.East}) ||
		polygonContains(poly, LatLng{Lat: bounds.South, Lng: bounds.West}) ||
		polygonContains(poly, LatLng{Lat: bounds.South, Lng: bounds.East}) {
		return true
	}

	// Check if any of the polygon's edges intersect with the tile's edges
	for i := 0; i < len(poly); i++ {
		p1 := poly[i]
		p2 := poly[(i+1)%len(poly)]

		if lineIntersects(p1, p2, LatLng{Lat: bounds.North, Lng: bounds.West}, LatLng{Lat: bounds.North, Lng: bounds.East}) ||
			lineIntersects(p1, p2, LatLng{Lat: bounds.North, Lng: bounds.East}, LatLng{Lat: bounds.South, Lng: bounds.East}) ||
			lineIntersects(p1, p2, LatLng{Lat: bounds.South, Lng: bounds.East}, LatLng{Lat: bounds.South, Lng: bounds.West}) ||
			lineIntersects(p1, p2, LatLng{Lat: bounds.South, Lng: bounds.West}, LatLng{Lat: bounds.North, Lng: bounds.West}) {
			return true
		}
	}

	return false
}

// lineIntersects checks if two line segments intersect.
func lineIntersects(p1, q1, p2, q2 LatLng) bool {
	o1 := orientation(p1, q1, p2)
	o2 := orientation(p1, q1, q2)
	o3 := orientation(p2, q2, p1)
	o4 := orientation(p2, q2, q1)

	if o1 != o2 && o3 != o4 {
		return true
	}

	// Special Cases for colinear points
	if o1 == 0 && onSegment(p1, p2, q1) {
		return true
	}
	if o2 == 0 && onSegment(p1, q2, q1) {
		return true
	}
	if o3 == 0 && onSegment(p2, p1, q2) {
		return true
	}
	if o4 == 0 && onSegment(p2, q1, q2) {
		return true
	}

	return false
}

// orientation finds the orientation of the ordered triplet (p, q, r).
func orientation(p, q, r LatLng) int {
	val := (q.Lng-p.Lng)*(r.Lat-q.Lat) - (q.Lat-p.Lat)*(r.Lng-q.Lng)
	if val == 0 {
		return 0 // Collinear
	}
	if val > 0 {
		return 1 // Clockwise
	}
	return 2 // Counterclockwise
}

// onSegment checks if point q lies on segment pr.
func onSegment(p, q, r LatLng) bool {
	if q.Lat <= math.Max(p.Lat, r.Lat) && q.Lat >= math.Min(p.Lat, r.Lat) &&
		q.Lng <= math.Max(p.Lng, r.Lng) && q.Lng >= math.Min(p.Lng, r.Lng) {
		return true
	}
	return false
}

// getWorldTiles returns a list of all tiles for the world up to zoom level 7.
func getWorldTiles() []Tile {
	var worldTiles []Tile
	for z := 0; z <= 7; z++ {
		max := 1 << z
		for x := 0; x < max; x++ {
			for y := 0; y < max; y++ {
				worldTiles = append(worldTiles, Tile{X: uint32(x), Y: uint32(y), Z: uint32(z)})
			}
		}
	}
	return worldTiles
}

// serveTile serves a single cached tile.
func serveTile(w http.ResponseWriter, r *http.Request) {
	parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/tiles/"), "/")
	if len(parts) != 4 {
		http.NotFound(w, r)
		return
	}
	styleName := parts[0]
	z := parts[1]
	x := parts[2]
	y := strings.TrimSuffix(parts[3], ".png")

	tilePath := filepath.Join(*cacheDir, sanitizeStyleName(styleName), z, x, y+".png")
	http.ServeFile(w, r, tilePath)
}

// getCachedTiles returns a list of cached tiles for a specific map style.
func getCachedTiles(w http.ResponseWriter, r *http.Request) {
	styleName := strings.TrimPrefix(r.URL.Path, "/get_cached_tiles/")
	styleCacheDir := getStyleCacheDir(styleName)

	var cachedTiles [][3]uint32
	err := filepath.Walk(styleCacheDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() && strings.HasSuffix(info.Name(), ".png") {
			parts := strings.Split(strings.TrimSuffix(path, ".png"), string(filepath.Separator))
			if len(parts) >= 4 {
				z, zErr := strToUint32(parts[len(parts)-3])
				x, xErr := strToUint32(parts[len(parts)-2])
				y, yErr := strToUint32(parts[len(parts)-1])
				if zErr == nil && xErr == nil && yErr == nil {
					cachedTiles = append(cachedTiles, [3]uint32{z, x, y})
				}
			}
		}
		return nil
	})

	if err != nil {
		http.Error(w, fmt.Sprintf("Error reading cache: %v", err), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(cachedTiles); err != nil {
		http.Error(w, fmt.Sprintf("Error encoding cached tiles: %v", err), http.StatusInternalServerError)
	}
}

// getStyleName returns the name of the map style for a given URL.
func getStyleName(mapStyleURL string) string {
	for name, url := range mapSources {
		if url == mapStyleURL {
			return name
		}
	}
	return "default"
}

// getStyleCacheDir returns the cache directory for a given style name.
func getStyleCacheDir(styleName string) string {
	return filepath.Join(*cacheDir, sanitizeStyleName(styleName))
}

// nonAlphanumeric is a regular expression to match any character that is not a letter, number, hyphen, or underscore.
var nonAlphanumeric = regexp.MustCompile(`[^a-zA-Z0-9-_]+`)

// sanitizeStyleName sanitizes the style name to be used as a directory name.
func sanitizeStyleName(styleName string) string {
	return nonAlphanumeric.ReplaceAllString(strings.ReplaceAll(styleName, " ", "-"), "")
}

// sendMessage sends a WebSocket message.
func sendMessage(conn *websocket.Conn, msgType string, data interface{}) {
	msg := WSMessage{Type: msgType, Data: data}
	if err := conn.WriteJSON(msg); err != nil {
		log.Println("Error sending message:", err)
	}
}

// sendError sends an error message over the WebSocket connection.
func sendError(conn *websocket.Conn, message string) {
	sendMessage(conn, "error", map[string]string{"message": message})
}

// strToUint32 converts a string to a uint32.
func strToUint32(s string) (uint32, error) {
	var i uint32
	_, err := fmt.Sscanf(s, "%d", &i)
	return i, err
}

// latLonToTile converts latitude and longitude to tile coordinates.
func latLonToTile(lat, lon float64, zoom uint32) (x, y uint32) {
	latRad := lat * math.Pi / 180
	n := math.Pow(2, float64(zoom))
	x = uint32(n * ((lon + 180) / 360))
	y = uint32(n * (1 - (math.Log(math.Tan(latRad)+1/math.Cos(latRad)) / math.Pi)) / 2)
	return
}

// tileBounds calculates the geographical bounding box of a tile.
func tileBounds(tile Tile) BoundingBox {
	n := math.Pow(2.0, float64(tile.Z))
	lonDeg := float64(tile.X)/n*360.0 - 180.0
	latRad := math.Atan(math.Sinh(math.Pi * (1 - 2*float64(tile.Y)/n)))
	latDeg := latRad * 180.0 / math.Pi

	lon2Deg := float64(tile.X+1)/n*360.0 - 180.0
	lat2Rad := math.Atan(math.Sinh(math.Pi * (1 - 2*float64(tile.Y+1)/n)))
	lat2Deg := lat2Rad * 180.0 / math.Pi

	return BoundingBox{
		North: latDeg,
		South: lat2Deg,
		East:  lon2Deg,
		West:  lonDeg,
	}
}

// polygonContains checks if a point is inside a polygon using the ray casting algorithm.
func polygonContains(poly []LatLng, point LatLng) bool {
	in := false
	for i, j := 0, len(poly)-1; i < len(poly); j, i = i, i+1 {
		if (poly[i].Lat > point.Lat) != (poly[j].Lat > point.Lat) &&
			(point.Lng < (poly[j].Lng-poly[i].Lng)*(point.Lat-poly[i].Lat)/(poly[j].Lat-poly[i].Lat)+poly[i].Lng) {
			in = !in
		}
	}
	return in
}
