// Copyright 2014, 2015 by Sascha L. Teichmann
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.

package main

import (
	"fmt"
	"image"
	"image/color"
	"image/png"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"time"

	"bitbucket.org/s_l_teichmann/mtsatellite/common"
	"github.com/gorilla/mux"
)

type subBaseLine struct {
	mapDir string
	bg     color.RGBA
}

func newSubBaseLine(mapDir string, bg color.RGBA) *subBaseLine {
	return &subBaseLine{mapDir: mapDir, bg: bg}
}

func (sb *subBaseLine) ServeHTTP(rw http.ResponseWriter, r *http.Request) {

	rw.Header().Set("Cache-Control", "max-age=0, no-cache, no-store")

	vars := mux.Vars(r)
	xs := vars["x"]
	ys := vars["y"]
	zs := vars["z"]

	x, y, z := toUint(xs), toUint(ys), toUint(zs)
	if z < 9 {
		http.ServeFile(rw, r, filepath.Join(sb.mapDir,
			strconv.Itoa(int(z)),
			strconv.Itoa(int(x)),
			strconv.Itoa(int(y))+".png"))
		return
	}

	if z > 16 {
		z = 16
	}
	tx := x >> (z - 8)
	ty := y >> (z - 8)

	baseTile := filepath.Join(
		sb.mapDir,
		"8",
		strconv.Itoa(int(tx)),
		strconv.Itoa(int(ty))+".png")

	var err error
	var fi os.FileInfo
	if fi, err = os.Stat(baseTile); err != nil {
		http.NotFound(rw, r)
		return
	}

	if checkLastModified(rw, r, fi.ModTime()) || checkETag(rw, r, fi) {
		return
	}

	rx := x & ^(^uint(0) << (z - 8))
	ry := y & ^(^uint(0) << (z - 8))

	parts := uint(1) << (z - 8)

	w := uint(256) / parts
	xo := w * rx
	yo := w * (parts - 1 - ry)

	img := common.LoadPNG(baseTile, sb.bg)

	type subImage interface {
		SubImage(image.Rectangle) image.Image
	}

	if si, ok := img.(subImage); ok {
		img = si.SubImage(image.Rect(int(xo), int(yo), int(xo+w), int(yo+w)))
	} else {
		// Should not happen.
		http.Error(rw,
			http.StatusText(http.StatusInternalServerError),
			http.StatusInternalServerError)
		return
	}

	img = blowUp(img)

	rw.Header().Set("Content-Type", "image/png")
	if err = png.Encode(rw, img); err != nil {
		log.Printf("WARN: encoding image failed: %s\n", err)
	}
}

func blowUp(src image.Image) *image.RGBA {

	// Fast path for RGBA -> RGBA
	if rgba, ok := src.(*image.RGBA); ok {
		return blowUpRGBA(rgba)
	}

	// Fallback
	dst := image.NewRGBA(image.Rect(0, 0, 256, 256))

	// fix point numbers x:8
	dx, dy := src.Bounds().Dx(), src.Bounds().Dy()

	bx, by := src.Bounds().Min.X<<8, src.Bounds().Min.Y<<8

	//start := time.Now()

	pix := dst.Pix

	lineOfs := dst.PixOffset(0, 0) // Should be 0.

	py := by
	var r, g, b, a uint8
	for y := 0; y < 256; y++ {
		sy := (py >> 8) & 0xff
		ox := -1
		px := bx
		ofs := lineOfs // Should not really b needed
		lineOfs += dst.Stride
		for x := 0; x < 256; x++ {
			sx := (px >> 8) & 0xff
			if sx != ox { // Minimize interface indirection access.
				ox = sx
				xr, xg, xb, xa := src.At(sx, sy).RGBA()
				r, g, b, a = uint8(xr), uint8(xg), uint8(xb), uint8(xa)
			}
			pix[ofs] = r
			pix[ofs+1] = g
			pix[ofs+2] = b
			pix[ofs+3] = a
			ofs += 4
			px += dx
		}
		py += dy
	}

	//log.Printf("Rendering took: %s\n", time.Since(start))

	return dst
}

func blowUpRGBA(src *image.RGBA) *image.RGBA {

	dst := image.NewRGBA(image.Rect(0, 0, 256, 256))

	// fix point numbers x:8
	dx, dy := src.Bounds().Dx(), src.Bounds().Dy()

	bx, by := src.Bounds().Min.X<<8, src.Bounds().Min.Y<<8

	//start := time.Now()

	sPix := src.Pix
	dPix := dst.Pix

	py := by
	// Assuming memory layout is packed 256*256*4 with stride of 4*256.
	// for dLineOfs, dEnd := dst.PixOffset(0, 0), dst.PixOffset(0, 256); dLineOfs < dEnd; dLineOfs += dst.Stride {
	for ofs := 0; ofs < 256*256*4; {
		sy := (py >> 8) & 0xff
		sLineOfs := src.PixOffset(0, sy)
		px := bx
		// ofs := dLineOfs
		for end := ofs + 4*256; ofs < end; ofs += 4 {
			sOfs := sLineOfs + ((px >> 6) & 0x3fc)
			px += dx
			dPix[ofs] = sPix[sOfs]
			dPix[ofs+1] = sPix[sOfs+1]
			dPix[ofs+2] = sPix[sOfs+2]
			dPix[ofs+3] = sPix[sOfs+3]
		}
		py += dy
	}

	//log.Printf("Rendering took: %s\n", time.Since(start))
	return dst
}

func checkETag(w http.ResponseWriter, r *http.Request, fi os.FileInfo) bool {
	etag := fmt.Sprintf("%x-%x", fi.ModTime().Unix(), fi.Size())
	if ifNoneMatch := r.Header.Get("If-None-Match"); ifNoneMatch == etag {
		w.WriteHeader(http.StatusNotModified)
		return true
	}
	w.Header().Set("ETag", etag)
	return false
}

func checkLastModified(w http.ResponseWriter, r *http.Request, modtime time.Time) bool {

	if modtime.IsZero() {
		return false
	}

	// The Date-Modified header truncates sub-second precision, so
	// use mtime < t+1s instead of mtime <= t to check for unmodified.
	if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) {
		w.WriteHeader(http.StatusNotModified)
		return true
	}
	w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
	return false
}

func toUint(s string) uint {
	x, err := strconv.Atoi(s)
	if err != nil {
		log.Printf("WARN: Cannot convert to int: %s\n", err)
		return 0
	}
	return uint(x)
}