package controllers

import (
	"bytes"
	"fmt"
	"image"
	"image/gif"
	"image/jpeg"
	"image/png"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"git.kirsle.net/apps/gophertype/pkg/console"
	"git.kirsle.net/apps/gophertype/pkg/responses"
	"git.kirsle.net/apps/gophertype/pkg/settings"
	"github.com/edwvee/exiffix"
	"github.com/nfnt/resize"
)

// TODO: configurable max image width.
var (
	MaxImageWidth = 1280
	JpegQuality   = 90

	// images folder for upload, relative to web root.
	ImagePath = filepath.Join("static", "photos")
)

// UploadHandler handles quick file uploads from the front-end for logged-in users.
func UploadHandler(w http.ResponseWriter, r *http.Request) {
	// Parameters.
	var (
		filetype = r.FormValue("type") // image only for now
		filename = r.FormValue("filename")
	)
	var buf bytes.Buffer

	type response struct {
		Success  bool   `json:"success"`
		Error    string `json:"error,omitempty"`
		Filename string `json:"filename,omitempty"`
		URI      string `json:"uri,omitempty"`
		Checksum string `json:"checksum,omitempty"`
	}

	// Validate the upload type.
	if filetype != "image" {
		responses.JSON(w, http.StatusBadRequest, response{
			Error: "Only 'image' type uploads supported for now.",
		})
		return
	}

	// Get the file from the form data.
	file, header, err := r.FormFile("file")
	if err != nil {
		responses.JSON(w, http.StatusBadRequest, response{
			Error: err.Error(),
		})
		return
	}
	defer file.Close()

	// Validate the extension is an image type.
	ext := strings.ToLower(filepath.Ext(header.Filename))
	if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" {
		responses.JSON(w, http.StatusBadRequest, response{
			Error: "Invalid file type, only common image types are supported: jpg, png, gif",
		})
		return
	}

	// Default filename?
	if filename == "" {
		filename = filepath.Base(header.Filename)
	}

	// Read the file.
	io.Copy(&buf, file)
	binary := buf.Bytes()

	// Process and image and resize it down, strip metadata, etc.
	binary, err = processImage(binary, ext)
	if err != nil {
		responses.JSON(w, http.StatusBadRequest, response{
			Error: "Resize error: " + err.Error(),
		})
	}

	console.Info("Uploaded file named: %s  name=%s", header.Filename, filename)

	// Write to the /static/photos directory of the user root. Ensure the path
	// exists or create it if not.
	outputPath := filepath.Join(settings.UserRoot, ImagePath)
	if _, err := os.Stat(outputPath); os.IsNotExist(err) {
		os.MkdirAll(outputPath, 0755)
	}

	// Ensure the filename is unique.
	filename = uniqueFilename(filepath.Join(settings.UserRoot, ImagePath), filename)

	// Write the output file.
	console.Info("Uploaded image: %s", filename)
	outfh, err := os.Create(filename)
	if err != nil {
		responses.JSON(w, http.StatusBadRequest, response{
			Error: err.Error(),
		})
		return
	}
	defer outfh.Close()
	outfh.Write(binary)

	responses.JSON(w, http.StatusOK, response{
		Success:  true,
		Filename: header.Filename,
		URI:      fmt.Sprintf("/static/photos/%s", filepath.Base(filename)),
	})
}

// processImage manhandles an image's binary data, scaling it down to <= 1280
// pixels and stripping off any metadata.
func processImage(input []byte, ext string) ([]byte, error) {
	if ext == ".gif" {
		return input, nil
	}

	reader := bytes.NewReader(input)

	// Decode the image using exiffix, which will auto-rotate jpeg images etc.
	// based on their EXIF values.
	origImage, _, err := exiffix.Decode(reader)
	if err != nil {
		return input, err
	}

	// Read the config to get the image width.
	reader.Seek(0, io.SeekStart)
	config, _, _ := image.DecodeConfig(reader)
	width := config.Width

	// If the width is too great, scale it down.
	if width > MaxImageWidth {
		width = MaxImageWidth
	}
	newImage := resize.Resize(uint(width), 0, origImage, resize.Lanczos3)

	var output bytes.Buffer
	switch ext {
	case ".jpeg":
		fallthrough
	case ".jpg":
		jpeg.Encode(&output, newImage, &jpeg.Options{
			Quality: JpegQuality,
		})
	case ".png":
		png.Encode(&output, newImage)
	case ".gif":
		gif.Encode(&output, newImage, nil)
	}

	return output.Bytes(), nil
}

// uniqueFilename gets a filename in a folder that doesn't already exist.
// Returns the file path with unique filename included.
func uniqueFilename(path string, filename string) string {
	ext := filepath.Ext(filename)
	basename := strings.TrimSuffix(filename, ext)

	// Try files.
	var i = 1
	for {
		if _, err := os.Stat(filepath.Join(path, filename)); !os.IsNotExist(err) {
			filename = fmt.Sprintf("%s~%d%s", basename, i, ext)
			i++
			continue
		}
		break
	}

	return filepath.Join(path, filename)
}
