package giant_screenshot

import (
	"bytes"
	"errors"
	"fmt"
	"image"
	"image/png"
	"os"
	"path/filepath"
	"runtime"
	"time"

	"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
	"git.kirsle.net/SketchyMaze/doodle/pkg/level"
	"git.kirsle.net/SketchyMaze/doodle/pkg/log"
	"git.kirsle.net/SketchyMaze/doodle/pkg/plus/dpp"
	"git.kirsle.net/SketchyMaze/doodle/pkg/userdir"
	"git.kirsle.net/go/render"
	"golang.org/x/image/draw"
)

// CroppedScreenshot returns a rendered RGBA image of the level.
func CroppedScreenshot(lvl *level.Level, viewport render.Rect) (image.Image, error) {
	// Not for WASM for now.
	if runtime.GOOS == "js" {
		return nil, errors.New("screenshots not yet supported for WASM")
	}

	// Lock this to one user at a time.
	if locked {
		return nil, errors.New("a screenshot is still being processed; try later...")
	}
	locked = true
	defer func() {
		locked = false
	}()

	// How big will our image be?
	var (
		size      = render.NewRect(viewport.W-viewport.X, viewport.H-viewport.Y)
		chunkSize = int(lvl.Chunker.Size)
		// worldSize = viewport
	)

	// Create the image.
	img := image.NewRGBA(image.Rect(0, 0, size.W, size.H))

	// Render the wallpaper onto it.
	log.Debug("CroppedScreenshot: Render wallpaper to image (%s)...", size)
	img = WallpaperToImage(lvl, img, size.W, size.H, viewport.Point())

	// Render the chunks.
	log.Debug("CroppedScreenshot: Render level chunks...")
	for coord := range lvl.Chunker.IterViewportChunks(viewport) {
		if chunk, ok := lvl.Chunker.GetChunk(coord); ok {

			// Get this chunk's rendered bitmap.
			rgba, ok := chunk.CachedBitmap(render.Invisible).(*image.RGBA)
			if !ok {
				log.Error("CroppedScreenshot: couldn't turn chunk to RGBA")
			}
			log.Debug("Blot chunk %s onto image", coord)

			// Compute where on the output image to copy this bitmap to.
			dst := image.Pt(
				// (X * W) multiplies the chunk coord by its size,
				// Then subtract the Viewport (level scroll position)
				(coord.X*chunkSize)-viewport.X,
				(coord.Y*chunkSize)-viewport.Y,
			)

			log.Debug("Copy chunk: %s to %s", coord, dst)
			img = blotImage(img, rgba, dst)
		}
	}

	// Render the doodads.
	log.Debug("CroppedScreenshot: Render actors...")
	for _, actor := range lvl.Actors {
		doodad, err := dpp.Driver.LoadFromEmbeddable(actor.Filename, lvl, false)
		if err != nil {
			log.Error("CroppedScreenshot: Load doodad: %s", err)
			continue
		}

		// Offset the doodad position by the viewport (scroll position).
		drawAt := render.NewPoint(actor.Point.X, actor.Point.Y)
		drawAt.X -= viewport.X
		drawAt.Y -= viewport.Y

		// TODO: usually doodad sprites start at 0,0 and the chunkSize
		// is the same as their sprite size.
		if len(doodad.Layers) > 0 && doodad.Layers[0].Chunker != nil {
			var chunker = doodad.Layers[0].Chunker
			chunk, ok := chunker.GetChunk(render.Origin)
			if !ok {
				continue
			}

			// TODO: we always use RGBA but is risky:
			rgba, ok := chunk.CachedBitmap(render.Invisible).(*image.RGBA)
			if !ok {
				log.Error("CroppedScreenshot: couldn't turn chunk to RGBA")
			}
			img = blotImage(img, rgba, image.Pt(drawAt.X, drawAt.Y))
		}

	}

	return img, nil
}

// SaveCroppedScreenshot will take a screenshot and write it to a file on disk,
// returning the filename relative to ~/.config/doodle/screenshots
func SaveCroppedScreenshot(level *level.Level, viewport render.Rect) (string, error) {
	var filename = time.Now().Format("2006-01-02_15-04-05.png")

	img, err := CroppedScreenshot(level, viewport)
	if err != nil {
		return "", err
	}

	fh, err := os.Create(filepath.Join(userdir.ScreenshotDirectory, filename))
	if err != nil {
		return "", err
	}

	png.Encode(fh, img)
	return filename, nil
}

// UpdateLevelScreenshots will generate and embed the screenshot PNGs into the level data.
func UpdateLevelScreenshots(lvl *level.Level, scroll render.Point) error {
	// Take screenshots.
	large, medium, small, tiny, err := CreateLevelScreenshots(lvl, scroll)
	if err != nil {
		return err
	}

	// Save the images into the level's filesystem.
	for filename, img := range map[string]image.Image{
		balance.LevelScreenshotLargeFilename:  large,
		balance.LevelScreenshotMediumFilename: medium,
		balance.LevelScreenshotSmallFilename:  small,
		balance.LevelScreenshotTinyFilename:   tiny,
	} {
		var fh = bytes.NewBuffer([]byte{})
		if err := png.Encode(fh, img); err != nil {
			return fmt.Errorf("encode %s: %s", filename, err)
		}

		log.Debug("UpdateLevelScreenshots: add %s", filename)
		lvl.Files.Set(
			fmt.Sprintf("assets/screenshots/%s", filename),
			fh.Bytes(),
		)
	}

	return nil
}

// CreateLevelScreenshots generates a screenshot to save with the level data.
//
// This is called by the editor upon level save, and outputs the screenshots that
// will be embedded within the level data itself.
//
// Returns the large, medium and small images.
func CreateLevelScreenshots(lvl *level.Level, scroll render.Point) (large, medium, small, tiny image.Image, err error) {
	// Viewport to screenshot.
	viewport := render.Rect{
		X: scroll.X,
		W: scroll.X + balance.LevelScreenshotLargeSize.W,
		Y: scroll.Y,
		H: scroll.Y + balance.LevelScreenshotLargeSize.H,
	}

	// Get the full size screenshot as an image.
	large, err = CroppedScreenshot(lvl, viewport)
	if err != nil {
		return
	}

	// Scale the medium and small versions.
	medium = Scale(large, image.Rect(0, 0, balance.LevelScreenshotMediumSize.W, balance.LevelScreenshotMediumSize.H), draw.ApproxBiLinear)
	small = Scale(large, image.Rect(0, 0, balance.LevelScreenshotSmallSize.W, balance.LevelScreenshotSmallSize.H), draw.ApproxBiLinear)
	tiny = Scale(large, image.Rect(0, 0, balance.LevelScreenshotTinySize.W, balance.LevelScreenshotTinySize.H), draw.ApproxBiLinear)
	return large, medium, small, tiny, nil
}

// Scale down an image. Example:
//
// scaled := Scale(src, image.Rect(0, 0, 200, 200), draw.ApproxBiLinear)
func Scale(src image.Image, rect image.Rectangle, scale draw.Scaler) image.Image {
	dst := image.NewRGBA(rect)
	copyRect := image.Rect(
		rect.Min.X,
		rect.Min.Y,
		rect.Min.X+rect.Max.X,
		rect.Min.Y+rect.Max.Y,
	)
	scale.Scale(dst, copyRect, src, src.Bounds(), draw.Over, nil)
	return dst
}
