/*
Serve is a very simple static file server in go
Usage:
	-p="8100": port to serve on
	-d=".":    the directory of static files to host
Navigating to http://localhost:8100 will display the index.html or directory
listing file.
*/
package main

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"local/args"
	"local/gziphttp"
	"log"
	"net/http"
	"net/http/httptest"
	"os"
	"path"
	"regexp"
	"strings"
)

const (
	ENDPOINT_UPLOAD = "__upload__"
	ENDPOINT_DELETE = "__delete__"
)

var (
	fs *args.ArgSet
)

func main() {
	fs = args.NewArgSet()
	fs.Append(args.STRING, "p", "port to serve", "8100")
	fs.Append(args.STRING, "d", "static path to serve", "./public")
	if err := fs.Parse(); err != nil {
		panic(err)
	}

	d := fs.Get("d").GetString()
	p := strings.TrimPrefix(fs.Get("p").GetString(), ":")

	http.Handle("/", http.HandlerFunc(handler(d)))

	log.Printf("Serving %s on HTTP port: %s\n", d, p)

	log.Fatal(http.ListenAndServe(":"+p, nil))
}

func handler(d string) http.HandlerFunc {
	return gzip(endpoints(withDel(fserve(d))))
}

func writeMeta(w http.ResponseWriter) {
	fmt.Fprintf(w, `
      <head>
         <meta charset="UTF-8"/>
         <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
      </head>
   `)
}

func writeForm(w http.ResponseWriter) {
	fmt.Fprintf(w, `
      <form enctype="multipart/form-data" action="./%s" method="post">
         <input type="file" name="file" required/>
         <input type="submit" value="upload"/>
      </form>
   `, ENDPOINT_UPLOAD)
}

func gzip(foo http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if gziphttp.Can(r) {
			gz := gziphttp.New(w)
			defer gz.Close()
			w = gz
		}
		foo(w, r)
	}
}

func endpoints(foo http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if isDir(r) {
			writeMeta(w)
			writeForm(w)
		}
		if isUploaded(r) {
			if err := upload(w, r); err != nil {
				fmt.Fprintln(w, err.Error())
			}
		} else if isDeleted(r) {
			if err := del(w, r); err != nil {
				fmt.Fprintln(w, err.Error())
			}
		} else {
			setContentTypeIfMedia(w, r)
			foo(w, r)
		}
	}
}

func isUploaded(r *http.Request) bool {
	return path.Base(r.URL.Path) == ENDPOINT_UPLOAD
}

func isDeleted(r *http.Request) bool {
	return path.Base(r.URL.Path) == ENDPOINT_DELETE
}

func isDir(r *http.Request) bool {
	d := toRealPath(r.URL.Path)
	fi, err := os.Stat(d)
	if err != nil {
		return false
	}
	if !fi.IsDir() {
		return false
	}
	if _, err := os.Stat(path.Join(d, "index.html")); err == nil {
		return false
	}
	return true
}

func withDel(foo http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !isDir(r) {
			foo(w, r)
			return
		}
		fmt.Fprintln(w, `<a href=".."><input type="button" style="padding: .15em 4em .35em 4em" value=".."/></a>`+"\n")
		w2 := httptest.NewRecorder()
		foo(w2, r)
		b := bytes.Split(w2.Body.Bytes(), []byte("\n"))
		buff := bytes.NewBuffer(nil)
		for i := range b {
			if bytes.Contains(b[i], []byte("<a href=")) {
				re := regexp.MustCompile(`href="[^"]*"`)
				match := re.Find(b[i])
				if len(match) > 0 {
					match = bytes.Split(match, []byte(`href="`))[1]
					match = match[:len(match)-1]
					b[i] = []byte(fmt.Sprintf(`<a href="%s/%s"><input type="button" value="&#10060;" style="padding: .40em 1em .10em 1em; margin-right: .5em" onclick='return confirm("Delete "+%q+"?");'></input></a> %s`, match, ENDPOINT_DELETE, match, b[i]))
				}
			}
			buff.Write(b[i])
			buff.Write([]byte("\n"))
		}
		io.Copy(w, buff)
	}
}

func fserve(d string) http.HandlerFunc {
	h := http.FileServer(http.Dir(d))
	return h.ServeHTTP
}

func upload(w http.ResponseWriter, r *http.Request) error {
	r.ParseMultipartForm(100 << 20)
	file, handler, err := r.FormFile("file")
	if err != nil {
		return err
	}
	defer file.Close()
	p := toRealPath(path.Join(path.Dir(r.URL.Path), handler.Filename))
	if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
		return errors.New("already exists")
	}
	f, err := os.Create(p)
	if err != nil {
		return err
	}
	defer f.Close()
	if _, err := io.Copy(f, file); err != nil {
		return err
	}
	http.Redirect(w, r, path.Dir(r.URL.Path)+"/", http.StatusSeeOther)
	return nil
}

func del(w http.ResponseWriter, r *http.Request) error {
	p := toRealPath(path.Dir(r.URL.Path))
	_, err := os.Stat(p)
	if err != nil {
		return err
	}
	err = os.RemoveAll(p)
	if err != nil {
		return err
	}
	http.Redirect(w, r, path.Dir(path.Dir(r.URL.Path))+"/", http.StatusSeeOther)
	return nil
}

func toRealPath(p string) string {
	d := path.Join(fs.Get("d").GetString())
	return path.Join(d, p)
}

func setContentTypeIfMedia(w http.ResponseWriter, r *http.Request) {
	switch strings.ToLower(path.Ext(r.URL.Path)) {
	case ".mp4":
		w.Header().Set("Content-Type", "video/mp4")
	case ".mkv":
		w.Header().Set("Content-Type", "video/x-matroska")
	case ".mp3":
		w.Header().Set("Content-Type", "audio/mpeg3")
	case ".epub", ".mobi":
		w.Header().Set("Content-Disposition", "attachment")
	case ".jpg", ".jpeg":
		w.Header().Set("Content-Type", "image/jpeg")
	case ".gif":
		w.Header().Set("Content-Type", "image/gif")
	case ".png":
		w.Header().Set("Content-Type", "image/png")
	case ".ico":
		w.Header().Set("Content-Type", "image/x-icon")
	case ".svg":
		w.Header().Set("Content-Type", "image/svg+xml")
	case ".css":
		w.Header().Set("Content-Type", "text/css")
	case ".js":
		w.Header().Set("Content-Type", "text/javascript")
	case ".json":
		w.Header().Set("Content-Type", "application/json")
	case ".html", ".htm":
		w.Header().Set("Content-Type", "text/html")
	case ".pdf":
		w.Header().Set("Content-Type", "application/pdf")
	case ".webm":
		w.Header().Set("Content-Type", "video/webm")
	case ".weba":
		w.Header().Set("Content-Type", "audio/webm")
	case ".webp":
		w.Header().Set("Content-Type", "image/webp")
	case ".zip":
		w.Header().Set("Content-Type", "application/zip")
	case ".7z":
		w.Header().Set("Content-Type", "application/x-7z-compressed")
	case ".tar":
		w.Header().Set("Content-Type", "application/x-tar")
	}
}
