package webserver

import (
	"net/http"
	"strings"
)

// This is partly based on the Go standard library file net/http/fs.go.

// scanETag determines if a syntactically valid ETag is present at s. If so,
// the ETag and remaining text after consuming ETag is returned. Otherwise,
// it returns "", "".
func scanETag(s string) (etag string, remain string) {
	s = strings.TrimLeft(s, " \t\n\r")
	start := 0
	if strings.HasPrefix(s, "W/") {
		start = 2
	}
	if len(s[start:]) < 2 || s[start] != '"' {
		return "", ""
	}
	// ETag is either W/"text" or "text".
	// See RFC 7232 2.3.
	for i := start + 1; i < len(s); i++ {
		c := s[i]
		switch {
		// Character values allowed in ETags.
		case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80:
		case c == '"':
			return s[:i+1], s[i+1:]
		default:
			return "", ""
		}
	}
	return "", ""
}

// etagMatch returns whether the given eTag matches an IM/INM header value.
// The empty string in etag is interpreted as a non-existent object.
func etagMatch(etag, header string) bool {
	if header == "" {
		return false
	}
	if header == etag {
		return true
	}

	for {
		header = strings.TrimLeft(header, " \t\n\r")
		if len(header) == 0 {
			break
		}
		if header[0] == ',' {
			header = header[1:]
			continue
		}
		if header[0] == '*' {
			return etag != ""
		}
		e, remain := scanETag(header)
		if e == "" {
			break
		}
		if e == etag {
			return true
		}
		header = remain
	}

	return false
}

func writeNotModified(w http.ResponseWriter) {
	// RFC 7232 section 4.1:
	// a sender SHOULD NOT generate representation metadata other than the
	// above listed fields unless said metadata exists for the purpose of
	// guiding cache updates (e.g., Last-Modified might be useful if the
	// response does not have an ETag field).
	h := w.Header()
	delete(h, "Content-Type")
	delete(h, "Content-Length")
	delete(h, "Content-Encoding")
	if h.Get("Etag") != "" {
		delete(h, "Last-Modified")
	}
	w.WriteHeader(http.StatusNotModified)
}

// checkPreconditions evaluates request preconditions.
// It interprets an empty etag as a non-existent object.
func checkPreconditions(w http.ResponseWriter, r *http.Request, etag string) (done bool) {
	// RFC 7232 section 6.
	im := r.Header.Get("If-Match")
	if im != "" && !etagMatch(etag, im) {
		w.WriteHeader(http.StatusPreconditionFailed)
		return true
	}
	inm := r.Header.Get("If-None-Match")
	if inm != "" && etagMatch(etag, inm) {
		if r.Method == "GET" || r.Method == "HEAD" {
			writeNotModified(w)
			return true
		} else {
			w.WriteHeader(http.StatusPreconditionFailed)
			return true
		}
	}

	return false
}