package caddyhugo

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

	"github.com/nfnt/resize"
)

type MediaSource struct {
	StorageDir string
	ThumbDir   string
}

func (ms *MediaSource) LocationOrig(m Media) string {
	return path.Join(ms.StorageDir, m.Name)
}

func (ms *MediaSource) ThumbPath(m Media, size image.Rectangle) string {
	w := size.Dx()
	h := size.Dy()

	var ws, hs string

	if w != 0 {
		ws = fmt.Sprint(w)
	}
	if h != 0 {
		hs = fmt.Sprint(h)
	}

	thumbSlug := filepath.Join(fmt.Sprintf("%sx%s", ws, hs), m.Name)
	return path.Join("/media", thumbSlug)
}

func (ms *MediaSource) ThumbFilename(m Media, size image.Rectangle) string {
	return filepath.Join(ms.ThumbDir, ms.ThumbPath(m, size))
}

func (ms *MediaSource) receiveNewMedia(name string, r io.Reader) error {
	dest := path.Join(ms.StorageDir, name)
	f, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		return err
	}

	_, err = io.Copy(f, r)
	if err != nil {
		return err
	}

	return f.Close()
}

type Media struct {
	Type string
	Name string
	Size image.Rectangle
}

func (ms *MediaSource) Size(name string) (image.Rectangle, error) {
	f, err := os.Open(name)
	if err != nil {
		return image.ZR, err
	}
	defer f.Close()

	cfg, _, err := image.DecodeConfig(f)
	if err != nil {
		return image.ZR, err
	}

	width := cfg.Width
	height := cfg.Height

	return image.Rect(0, 0, width, height), nil
}

func (ms *MediaSource) ThumbMax(m Media, maxDim int) (string, image.Rectangle, error) {
	f, err := os.Open(ms.LocationOrig(m))
	if err != nil {
		return "", image.ZR, err
	}
	defer f.Close()

	cfg, _, err := image.DecodeConfig(f)
	if err != nil {
		return "", image.ZR, err
	}

	width := cfg.Width
	height := cfg.Height

	if width > height {
		height = height * maxDim / width
		width = maxDim
	} else {
		width = width * maxDim / height
		height = maxDim
	}

	size := image.Rect(0, 0, width, height)
	if ms.HasThumb(m, size) {
		return ms.ThumbPath(m, size), size, nil
	}

	_, err = f.Seek(0, io.SeekStart)
	if err != nil {
		return "", image.ZR, err
	}

	img, _, err := image.Decode(f)
	if err != nil {
		return "", image.ZR, err
	}

	src, err := ms.ThumbImage(img, m, size)
	return src, size, err
}

func (ms *MediaSource) HasThumb(m Media, size image.Rectangle) bool {
	_, err := os.Stat(ms.ThumbFilename(m, size))
	return err == nil
}

func (ms *MediaSource) ByName(name string) *Media {
	size, _ := ms.Size(path.Join(ms.StorageDir, name))
	return &Media{
		Type: "image",
		Name: name,
		Size: size,
	}
}

func (ms *MediaSource) Thumb(m Media, size image.Rectangle) (string, error) {
	if ms.HasThumb(m, size) {
		return ms.ThumbPath(m, size), nil
	}

	f, err := os.Open(ms.LocationOrig(m))
	if err != nil {
		return "", err
	}
	defer f.Close()

	img, _, err := image.Decode(f)
	if err != nil {
		return "", err
	}

	return ms.ThumbImage(img, m, size)
}

func (ms *MediaSource) ThumbImage(img image.Image, m Media, size image.Rectangle) (string, error) {

	thumbLoc := ms.ThumbFilename(m, size)
	os.MkdirAll(path.Dir(thumbLoc), 0755)
	fthumb, err := os.OpenFile(thumbLoc, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0655)
	if err != nil {
		return "", err
	}

	img = resize.Resize(uint(size.Dx()), uint(size.Dy()), img, resize.Bilinear)

	err = jpeg.Encode(fthumb, img, nil)
	if err != nil {
		return "", err
	}

	err = fthumb.Close()
	if err != nil {
		return "", err
	}

	return ms.ThumbPath(m, size), nil
}

func (ms *MediaSource) Walk() ([]*Media, error) {
	var media []*Media

	err := filepath.Walk(ms.StorageDir, func(name string, fi os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if fi.IsDir() {
			return nil
		}
		media = append(media, ms.ByName(path.Base(name)))
		return nil
	})

	return media, err
}

func (ch *CaddyHugo) uploadMedia(w http.ResponseWriter, r *http.Request) (int, error) {
	if ch.Media == nil {
		http.NotFound(w, r)
		return 404, nil
	}

	mr, err := r.MultipartReader()
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return 400, nil
	}

	for {
		part, err := mr.NextPart()
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return 400, nil
		}

		name := part.FileName()
		if name != "" {
			err = ch.Media.receiveNewMedia(name, part)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return 500, nil
			}
		}
	}

	return 200, nil
}

func (ch *CaddyHugo) serveMediaPage(w http.ResponseWriter, r *http.Request) (int, error) {
	if ch.Media == nil {
		http.NotFound(w, r)
		return 404, nil
	}

	io.WriteString(w, `<html>
	<head><style>
	iframe { height: 100%; }
	.img { display: inline-block; text-align: center; max-width: 19.9vw; min-height: 120px;}
	.copy { cursor: pointer; }
	@media (max-width: 800px) {
		.img {
			width: 50%;
			max-width: 50%;
		}
		body {
			min-height: 300px;
		}
	}
	</style></head>
	<body>
	<div style="position: fixed; top: 0; height: 10vh; width: 100%; background-color: white;">
	`)
	io.WriteString(w, UploadPage("media"))

	io.WriteString(w, `</div>
	<div style="display: inline-block; width: 100%; height: 10vh;"></div>
	`)
	if ch.Media != nil {
		media, err := ch.Media.Walk()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return 500, nil
		}

		for _, m := range media {

			src, size, err := ch.Media.ThumbMax(*m, 100)
			if err != nil {
				fmt.Fprintf(w, `<div class="img">error rendering %q: %v</div>`, m.Name, err)
				continue
			}
			fmt.Fprintf(w, `<div class="img"><img width=%d height=%d src=%q /><br /><input type="text" readonly value=%q /><span class="copy">&#x1F4CB;</span></div>`, size.Dx(), size.Dy(), src, src)
		}
	}
	io.WriteString(w, `<script>
	document.querySelector('body').onclick = function (evt) {
		if (evt.target.tagName === "INPUT" && evt.target.type === "text") {
			evt.target.select();
		}
		if (evt.target.tagName === "SPAN" && evt.target.className === "copy") {
			evt.target.previousSibling.select();
			document.execCommand("copy");
		}
	}
	document.querySelector('body').onmouseup = function (evt) {
		if (evt.target.tagName === "INPUT" && evt.target.type === "text") {
			return false;
		}
	}
	</script></body><html>`)
	return 200, nil
}

var (
	sizeString = regexp.MustCompile(`([0-9]*)(x)?([0-9]*)`)
)

func parseSizeString(str string, actual image.Rectangle) (image.Rectangle, error) {
	var err = fmt.Errorf("expected a size string {width}x{height}, saw %q", str)

	strs := sizeString.FindStringSubmatch(str)
	if len(strs) < 4 {
		return image.ZR, err
	}

	var w, h int
	var strconvErr error

	if strs[1] != "" {
		w, strconvErr = strconv.Atoi(strs[1])
		if strconvErr != nil {
			return image.ZR, err
		}
	}

	if strs[3] != "" {
		h, strconvErr = strconv.Atoi(strs[3])
		if strconvErr != nil {
			return image.ZR, err
		}
	}

	if strs[2] != "x" {
		// w was the only dimension given, so set it to the greater dimension
		// of the actual image size
		if actual.Dx() > actual.Dy() {
			w = w
			h = 0
		} else {
			h = w
			w = 0
		}
	}

	return image.Rect(0, 0, w, h), nil
}

func (ch *CaddyHugo) serveMedia(w http.ResponseWriter, r *http.Request) (int, error) {
	if ch.Media == nil {
		http.NotFound(w, r)
		return 404, nil
	}

	segs := strings.Split(r.URL.Path, "/")
	name := segs[len(segs)-1] // the last segment is the filename

	size := image.Rectangle{}

	m := ch.Media.ByName(name)

	if len(segs) >= 4 && len(segs) > 2 {
		var err error
		size, err = parseSizeString(segs[len(segs)-2], m.Size)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return 400, nil
		}
	}

	file, err := ch.Media.Thumb(*m, size)
	if err != nil {
		http.Error(w, fmt.Sprintf("unable to load thumb"), http.StatusInternalServerError)
		return 500, nil
	}

	if file[0] == '/' {
		file = file[1:]
	}

	file = path.Join(ch.Media.ThumbDir, file)

	http.ServeFile(w, r, file)

	return 200, nil
}

func removeExtension(name string) string {
	ext := path.Ext(name)
	return name[:len(name)-len(ext)]
}
