//go:build sqliteimport && !js && !wasm && !baremetal

// Command mbtilesdemo builds the tileset behind the gh-pages MBTiles demo
// (cmd/query_files_wasm/tiles-demo.html).
//
// It generates a small, purely synthetic "world" (see noise.go/tileimage.go —
// there is no real geographic data here, only a deterministic generative
// field), then round-trips it through tinySQL's actual MBTiles pipeline:
//
//  1. Render tiles for zoom 0..maxZoom, TMS rows, into an ordinary tinySQL
//     table shaped like the specification requires.
//  2. importer.ExportMBTiles writes a genuine, spec-compliant .mbtiles file —
//     this is the artifact the demo page offers for download, and proves the
//     export path against real output rather than an assertion in a test.
//  3. importer.ImportMBTiles reads that file back in, which is also where
//     tile_size/tile_sha256/tile_preview_base64 get computed.
//  4. The tiles are assembled into a browser-facing "tiles" table (tile_data
//     as base64 TEXT, not BLOB — see the comment on buildBrowserDB) and saved
//     as a tinySQL snapshot the WASM demo loads with one importDatabase call.
//
// Run: go run -tags=sqliteimport ./cmd/mbtilesdemo
package main

import (
	"context"
	"encoding/base64"
	"fmt"
	"os"
	"path/filepath"
	"time"

	tinysql "github.com/SimonWaldherr/tinySQL"
	"github.com/SimonWaldherr/tinySQL/internal/importer"
	"github.com/SimonWaldherr/tinySQL/internal/storage"
)

const maxZoom = 4 // zooms 0..4 => 341 tiles

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "mbtilesdemo:", err)
		os.Exit(1)
	}
}

func run() error {
	repoRoot, err := findRepoRoot()
	if err != nil {
		return err
	}
	outDir := filepath.Join(repoRoot, "cmd", "query_files_wasm")
	mbtilesPath := filepath.Join(outDir, "demo.mbtiles")

	ctx := context.Background()
	start := time.Now()

	src, tileCount, err := buildSourceTileset()
	if err != nil {
		return fmt.Errorf("render tiles: %w", err)
	}
	fmt.Printf("rendered %d tiles (zoom 0..%d) in %s\n", tileCount, maxZoom, time.Since(start))

	if err := os.Remove(mbtilesPath); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("remove stale %s: %w", mbtilesPath, err)
	}
	exportRes, err := importer.ExportMBTiles(ctx, src, "default", mbtilesPath, &importer.ExportMBTilesOptions{
		TileRowIsTMS: true,
		Metadata: map[string]string{
			"name":        "tinySQL generative demo",
			"format":      "png",
			"type":        "baselayer",
			"description": "A synthetic, non-geographic tileset generated for tinySQL's MBTiles demo. No real map data.",
			"attribution": "Generated by tinySQL (cmd/mbtilesdemo) — CC0",
			"bounds":      "-180,-85.0511,180,85.0511",
			"center":      "0,0,2",
		},
	})
	if err != nil {
		return fmt.Errorf("export mbtiles: %w", err)
	}
	fmt.Printf("wrote %s (%d tiles, zoom %d..%d)\n", mbtilesPath, exportRes.TilesWritten, exportRes.MinZoom, exportRes.MaxZoom)

	roundTrip := storage.NewDB()
	importRes, err := importer.ImportMBTiles(ctx, roundTrip, "default", "tiles", mbtilesPath, &importer.ImportOptions{CreateTable: true})
	if err != nil {
		return fmt.Errorf("import mbtiles: %w", err)
	}
	fmt.Printf("round-tripped %d tiles back through ImportMBTiles\n", importRes.RowsInserted)

	demoDB, err := buildBrowserDB(roundTrip)
	if err != nil {
		return fmt.Errorf("build browser DB: %w", err)
	}

	if err := writeAssets(outDir, demoDB, mbtilesPath); err != nil {
		return err
	}
	fmt.Printf("done in %s\n", time.Since(start))
	return nil
}

// findRepoRoot walks up from the working directory looking for go.mod, since
// this tool is meant to run via `go run ./cmd/mbtilesdemo` from anywhere in
// the tree.
func findRepoRoot() (string, error) {
	dir, err := os.Getwd()
	if err != nil {
		return "", err
	}
	for {
		if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
			return dir, nil
		}
		parent := filepath.Dir(dir)
		if parent == dir {
			return "", fmt.Errorf("go.mod not found above %s", dir)
		}
		dir = parent
	}
}

// buildSourceTileset renders every tile for zoom 0..maxZoom into a tinySQL
// table shaped like the MBTiles specification (TMS rows), ready for
// importer.ExportMBTiles.
func buildSourceTileset() (*storage.DB, int, error) {
	db := storage.NewDB()
	tiles := storage.NewTable("tiles", []storage.Column{
		{Name: "zoom_level", Type: storage.IntType},
		{Name: "tile_column", Type: storage.IntType},
		{Name: "tile_row", Type: storage.IntType},
		{Name: "tile_data", Type: storage.BlobType},
	}, false)

	pal := buildPalette()
	count := 0
	totalPNGBytes := 0
	for z := 0; z <= maxZoom; z++ {
		n := 1 << uint(z)
		for y := 0; y < n; y++ {
			for x := 0; x < n; x++ {
				img := renderTile(z, x, y, pal)
				png, err := encodePNG(img)
				if err != nil {
					return nil, 0, fmt.Errorf("encode tile %d/%d/%d: %w", z, x, y, err)
				}
				tmsRow := n - 1 - y
				tiles.Rows = append(tiles.Rows, []any{z, x, tmsRow, png})
				count++
				totalPNGBytes += len(png)
			}
		}
	}
	fmt.Printf("palette: %d colors; average tile: %d bytes\n", len(pal.table), totalPNGBytes/count)
	tiles.Version++
	if err := db.Put("default", tiles); err != nil {
		return nil, 0, err
	}
	return db, count, nil
}

// buildBrowserDB assembles the table the WASM demo actually queries. tile_data
// is TEXT holding base64, not a BLOB: the WASM bridge in
// cmd/query_files_wasm/main.go passes every Go value through to JSON except
// []byte, which it round-trips through Go's string(v) conversion first — that
// corrupts non-UTF-8 PNG bytes. Encoding to base64 here, once, at build time
// sidesteps the bridge entirely; the browser just calls atob() on a TEXT
// column like any other query result.
func buildBrowserDB(roundTrip *storage.DB) (*storage.DB, error) {
	src, err := roundTrip.Get("default", "tiles")
	if err != nil {
		return nil, err
	}
	zoomIdx, _ := src.ColIndex("zoom_level")
	colIdx, _ := src.ColIndex("tile_column")
	rowIdx, _ := src.ColIndex("tile_row")
	dataIdx, err := src.ColIndex("tile_data")
	if err != nil {
		return nil, fmt.Errorf("tiles table has no tile_data column: %w", err)
	}

	demo := storage.NewDB()
	tiles := storage.NewTable("tiles", []storage.Column{
		{Name: "zoom_level", Type: storage.IntType},
		{Name: "tile_column", Type: storage.IntType},
		{Name: "tile_row", Type: storage.IntType},
		{Name: "tile_data", Type: storage.TextType},
	}, false)
	for _, row := range src.Rows {
		raw, ok := row[dataIdx].([]byte)
		if !ok || len(raw) == 0 {
			return nil, fmt.Errorf("tile %v/%v/%v has no tile_data blob", row[zoomIdx], row[colIdx], row[rowIdx])
		}
		b64 := base64.StdEncoding.EncodeToString(raw)
		tiles.Rows = append(tiles.Rows, []any{row[zoomIdx], row[colIdx], row[rowIdx], b64})
	}
	tiles.Version++
	if err := tiles.CreateSecondaryIndex("tile_index", []string{"zoom_level", "tile_column", "tile_row"}, true); err != nil {
		return nil, fmt.Errorf("index tiles: %w", err)
	}
	if err := demo.Put("default", tiles); err != nil {
		return nil, err
	}

	meta, err := roundTrip.Get("default", "tiles_metadata")
	if err != nil {
		return nil, fmt.Errorf("get tiles_metadata: %w", err)
	}
	meta.Version++
	if err := demo.Put("default", meta); err != nil {
		return nil, err
	}

	if err := addSettlementsTable(demo); err != nil {
		return nil, fmt.Errorf("build settlements: %w", err)
	}
	if err := addMapshaperShapesTable(demo); err != nil {
		return nil, fmt.Errorf("build mapshaper shapes: %w", err)
	}

	return demo, nil
}

// addSettlementsTable adds the points-of-interest layer the demo page uses to
// show GEO_DISTANCE/GEO_BEARING/GEO_MIDPOINT running against real rows, not
// just literal coordinates. geometry is stored as the same GeoJSON Point text
// GEO_POINT() itself produces (see geoPointJSON in settlements.go), so it
// needs no conversion before reaching those functions.
func addSettlementsTable(demo *storage.DB) error {
	places := buildSettlements()
	settlements := storage.NewTable("settlements", []storage.Column{
		{Name: "name", Type: storage.TextType},
		{Name: "geometry", Type: storage.TextType},
		{Name: "biome", Type: storage.TextType},
		{Name: "population", Type: storage.IntType},
	}, false)
	for _, s := range places {
		geom, err := geoPointJSON(s.Lon, s.Lat)
		if err != nil {
			return fmt.Errorf("encode settlement %q: %w", s.Name, err)
		}
		settlements.Rows = append(settlements.Rows, []any{s.Name, geom, s.Biome, s.Population})
	}
	settlements.Version++
	if err := settlements.CreateSecondaryIndex("settlements_name", []string{"name"}, true); err != nil {
		return fmt.Errorf("index settlements: %w", err)
	}
	fmt.Printf("placed %d settlements\n", len(places))
	return demo.Put("default", settlements)
}

// addMapshaperShapesTable provides a small geometry layer for the browser's
// editing panel. Keeping these as GeoJSON text makes every operation run
// through tinySQL's GEO_*/ST_* functions, just like user-supplied geometry.
func addMapshaperShapesTable(demo *storage.DB) error {
	shapes := buildMapshaperShapes()
	table := storage.NewTable("mapshaper_shapes", []storage.Column{
		{Name: "name", Type: storage.TextType},
		{Name: "kind", Type: storage.TextType},
		{Name: "geometry", Type: storage.TextType},
		{Name: "description", Type: storage.TextType},
	}, false)
	for _, shape := range shapes {
		table.Rows = append(table.Rows, []any{shape.Name, shape.Kind, shape.Geometry, shape.Description})
	}
	table.Version++
	if err := table.CreateSecondaryIndex("mapshaper_shapes_name", []string{"name"}, true); err != nil {
		return fmt.Errorf("index mapshaper shapes: %w", err)
	}
	fmt.Printf("added %d mapshaper editing shapes\n", len(shapes))
	return demo.Put("default", table)
}

func writeAssets(outDir string, demoDB *storage.DB, mbtilesPath string) error {
	snapshot, err := tinysql.SaveToBytes(demoDB)
	if err != nil {
		return fmt.Errorf("save snapshot: %w", err)
	}
	if err := writeSnapshotJS(filepath.Join(outDir, "tiles-demo-data.js"), snapshot); err != nil {
		return err
	}
	info, err := os.Stat(mbtilesPath)
	if err != nil {
		return err
	}
	fmt.Printf("snapshot: %d bytes; demo.mbtiles: %d bytes\n", len(snapshot), info.Size())
	return nil
}
