package io.dropwizard.jetty;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.util.Duration;
import io.dropwizard.validation.MinDuration;
import jakarta.servlet.DispatcherType;
import jakarta.validation.constraints.Min;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlets.PushCacheFilter;

import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * A factory for building HTTP/2 {@link PushCacheFilter},
 * <p/>
 * <b>Configuration Parameters:</b>
 * <table>
 *     <tr>
 *         <td>Name</td>
 *         <td>Default</td>
 *         <td>Description</td>
 *     </tr>
 *     <tr>
 *         <td>{@code enabled}</td>
 *         <td>false</td>
 *         <td>
 *             If true, the filter will organize resources as primary resources (those referenced by the
 *             <i>Referer</i> header) and secondary resources (those that have the <i>Referer</i> header).
 *             Secondary resources that have been requested within a time window from the request of the
 *             primary resource will be associated with the it. The next time a client will
 *             request the primary resource, the server will send to the client the secondary resources
 *             along with the primary in a single response.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code associatePeriod}</td>
 *         <td>4 seconds</td>
 *         <td>
 *             The time window within which a request for a secondary resource will be associated to a
 *             primary resource.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code maxAssociations}</td>
 *         <td>16</td>
 *         <td>
 *             The maximum number of secondary resources that may be associated to a primary resource.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code refererHosts}</td>
 *         <td>All hosts</td>
 *         <td>
 *             The list of referrer hosts for which the server push technology is supported.
 *         </td>
 *     </tr>
 *     <tr>
 *         <td>{@code refererPorts}</td>
 *         <td>All ports</td>
 *         <td>
 *             The list of referrer ports for which the server push technology is supported.
 *         </td>
 *     </tr>
 * </table>
 */
public class ServerPushFilterFactory {

    private boolean enabled = false;

    @MinDuration(value = 1, unit = TimeUnit.MILLISECONDS)
    private Duration associatePeriod = Duration.seconds(4);

    @Min(1)
    private int maxAssociations = 16;

    @Nullable
    private List<String> refererHosts;

    @Nullable
    private List<Integer> refererPorts;

    @JsonProperty
    public boolean isEnabled() {
        return enabled;
    }

    @JsonProperty
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    @JsonProperty
    public Duration getAssociatePeriod() {
        return associatePeriod;
    }

    @JsonProperty
    public void setAssociatePeriod(Duration associatePeriod) {
        this.associatePeriod = associatePeriod;
    }

    @JsonProperty
    public int getMaxAssociations() {
        return maxAssociations;
    }

    @JsonProperty
    public void setMaxAssociations(int maxAssociations) {
        this.maxAssociations = maxAssociations;
    }

    @Nullable
    @JsonProperty
    public List<String> getRefererHosts() {
        return refererHosts;
    }

    @JsonProperty
    public void setRefererHosts(@Nullable List<String> refererHosts) {
        this.refererHosts = refererHosts;
    }

    @Nullable
    @JsonProperty
    public List<Integer> getRefererPorts() {
        return refererPorts;
    }

    @JsonProperty
    public void setRefererPorts(@Nullable List<Integer> refererPorts) {
        this.refererPorts = refererPorts;
    }

    public void addFilter(ServletContextHandler handler) {
        if (!enabled) {
            return;
        }

        handler.setInitParameter("associatePeriod", String.valueOf(associatePeriod.toMilliseconds()));
        handler.setInitParameter("maxAssociations", String.valueOf(maxAssociations));
        if (refererHosts != null) {
            handler.setInitParameter("hosts", String.join(",", refererHosts));
        }
        if (refererPorts != null) {
            final String ports = refererPorts.stream()
                    .map(Object::toString)
                    .collect(Collectors.joining(","));
            handler.setInitParameter("ports", ports);
        }
        handler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
    }
}
