package httproute

import (
	"bytes"
	"fmt"
	"net/url"
	"strings"
)

// Execute interpolates the provided template, resulting in a URI path that
// matches the pattern. It uses the lookup function to determine the values of
// any referenced variables.
func (t *Template) Execute(lookup func(key string) (val string, ok bool)) (string, error) {

	var badValues []string

	var buf bytes.Buffer
	for _, seg := range t.segments {
		buf.WriteString("/")
		if seg.variable == nil {
			switch seg.literal {
			case "*":
				// The template includes a literal '*', and since it's not
				// part of a variable declaration we have no data to use to
				// fill in a value. We cannot generate an appropriate URI.
				return "", fmt.Errorf("httproute: template includes literal * segment with no data")
			case "**":
				// This would only be allowed at the end. We'll "fill in" the
				// empty string here, and allow the loop to terminate
				// naturally.
			default:
				buf.WriteString(seg.literal)
			}
		} else {
			key := strings.Join([]string(seg.variable.fieldPath), ".")
			val, ok := lookup(key)
			if !ok {
				return "", fmt.Errorf("httproute: lookup failed for field %q", key)
			}

			esc := val
			if len(seg.variable.segments) == 1 && seg.variable.segments[0].literal == "*" {
				// When a variable matches a single path segment, make sure that
				// the value we're filling in fits in a single path segment.

				esc = escapePathSegment(val)
			} else {
				esc = escapeMultipleSegments(val)
			}

			if tmpl := (&Template{segments: seg.variable.segments}); !tmpl.isMatch("/" + esc) {
				rule := strings.TrimPrefix(tmpl.MatchRule(), "/")
				badValues = append(badValues, fmt.Sprintf("%s (have %q, need %q)", key, esc, rule))
			}

			buf.WriteString(esc)
		}
	}

	if t.verb != "" {
		buf.WriteString(":")
		buf.WriteString(string(t.verb))
	}

	if len(badValues) > 0 {
		noun, verb := "variable", "leads"
		if len(badValues) > 1 {
			noun, verb = "variables", "lead"
		}
		return "", fmt.Errorf("httproute: %s %s %s to URI path %q which does not match template %q",
			noun, strings.Join(badValues, ", "), verb, buf.String(), t.MatchRule())
	}

	return buf.String(), nil
}

// escapePathSegment escapes the string so it can be safely placed inside a
// URL path segment.
func escapePathSegment(s string) string {
	// TODO: Go 1.8 added url.PathEscape. Using it directly would be
	// preferable to the following, which triggers the net/url package to
	// path-escape the provided segment and then fixes up the differences
	// between full-path encoding and path-segment encoding.

	// Disable net/url's special case for Path of "*"
	s = "/" + s

	s = (&url.URL{Path: s}).EscapedPath()
	s = strings.TrimPrefix(s, "/")

	// Per net/url.shouldEscape, RFC 3986 §3.3 reserves '/', ';', and ',' in
	// URI paths as having meaning for individual path segments.
	s = strings.NewReplacer(
		"/", url.QueryEscape("/"),
		";", url.QueryEscape(";"),
		",", url.QueryEscape(","),

		":", url.QueryEscape(":"),
	).Replace(s)

	return s
}

func escapeMultipleSegments(s string) string {
	// Disable net/url's special case for Path of "*"
	s = "/" + s

	s = (&url.URL{Path: s}).EscapedPath()
	s = strings.TrimPrefix(s, "/")

	s = strings.NewReplacer(
		":", url.QueryEscape(":"),
	).Replace(s)

	return s
}
