// Copyright 2015 Light Code Labs, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package push

import (
	"net/http"
	"strings"

	"github.com/mholt/caddy/caddyhttp/httpserver"
	"github.com/mholt/caddy/caddyhttp/staticfiles"
)

func (h Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
	pusher, hasPusher := w.(http.Pusher)

	// no push possible, carry on
	if !hasPusher {
		return h.Next.ServeHTTP(w, r)
	}

	// check if this is a request for the pushed resource (avoid recursion)
	if _, exists := r.Header[pushHeader]; exists {
		return h.Next.ServeHTTP(w, r)
	}

	headers := h.filterProxiedHeaders(r.Header)

	// push first
outer:
	for _, rule := range h.Rules {
		urlPath := r.URL.Path
		matches := httpserver.Path(urlPath).Matches(rule.Path)
		// Also check IndexPages when requesting a directory
		if !matches {
			indexFile, isIndexFile := httpserver.IndexFile(h.Root, urlPath, staticfiles.IndexPages)
			if isIndexFile {
				matches = httpserver.Path(indexFile).Matches(rule.Path)
			}
		}
		if matches {
			for _, resource := range rule.Resources {
				pushErr := pusher.Push(resource.Path, &http.PushOptions{
					Method: resource.Method,
					Header: h.mergeHeaders(headers, resource.Header),
				})
				if pushErr != nil {
					// if we cannot push (either not supported or concurrent streams are full - break)
					break outer
				}
			}
		}
	}

	// serve later
	code, err := h.Next.ServeHTTP(w, r)

	// push resources returned in Link headers from upstream middlewares or proxied apps
	if links, exists := w.Header()["Link"]; exists {
		h.servePreloadLinks(pusher, headers, links)
	}

	return code, err
}

// servePreloadLinks parses Link headers from backend and pushes resources found in them.
// For accepted header formats check parseLinkHeader function.
//
// If resource has 'nopush' attribute then it will be omitted.
func (h Middleware) servePreloadLinks(pusher http.Pusher, headers http.Header, resources []string) {
outer:
	for _, resource := range resources {
		for _, resource := range parseLinkHeader(resource) {
			if _, exists := resource.params["nopush"]; exists {
				continue
			}

			if h.isRemoteResource(resource.uri) {
				continue
			}

			err := pusher.Push(resource.uri, &http.PushOptions{
				Method: http.MethodGet,
				Header: headers,
			})

			if err != nil {
				break outer
			}
		}
	}
}

func (h Middleware) isRemoteResource(resource string) bool {
	return strings.HasPrefix(resource, "//") ||
		strings.HasPrefix(resource, "http://") ||
		strings.HasPrefix(resource, "https://")
}

func (h Middleware) mergeHeaders(l, r http.Header) http.Header {
	out := http.Header{}

	for k, v := range l {
		out[k] = v
	}

	for k, vv := range r {
		for _, v := range vv {
			out.Add(k, v)
		}
	}

	return out
}

func (h Middleware) filterProxiedHeaders(headers http.Header) http.Header {
	filter := http.Header{}

	for _, header := range proxiedHeaders {
		if val, ok := headers[header]; ok {
			filter[header] = val
		}
	}

	return filter
}

var proxiedHeaders = []string{
	"Accept-Encoding",
	"Accept-Language",
	"Cache-Control",
	"Host",
	"User-Agent",
}