/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.felix.http.base.internal.registry;

import jakarta.servlet.DispatcherType;

import org.apache.felix.http.base.internal.HttpConfig;
import org.apache.felix.http.base.internal.handler.FilterHandler;
import org.apache.felix.http.base.internal.handler.ListenerHandler;
import org.apache.felix.http.base.internal.handler.ServletHandler;
import org.apache.felix.http.base.internal.runtime.FilterInfo;
import org.apache.felix.http.base.internal.runtime.ListenerInfo;
import org.apache.felix.http.base.internal.runtime.ServletContextHelperInfo;
import org.apache.felix.http.base.internal.runtime.ServletInfo;
import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder;
import org.apache.felix.http.base.internal.service.HttpServiceFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.servlet.runtime.dto.ServletContextDTO;

/**
 * This registry keeps track of all processing components per context:
 * - servlets
 * - filters
 * - error pages
 */
public final class PerContextHandlerRegistry implements Comparable<PerContextHandlerRegistry>
{
    /** Service id of the context. */
    private final long serviceId;

    /** Ranking of the context. */
    private final int ranking;

    /** The context path. */
    private final String path;

    /** The context prefix. */
    private final String prefix;

    private final ServletRegistry servletRegistry = new ServletRegistry();

    private final FilterRegistry filterRegistry = new FilterRegistry();

    private final ErrorPageRegistry errorPageRegistry = new ErrorPageRegistry();

    private final EventListenerRegistry eventListenerRegistry = new EventListenerRegistry();

    private final HttpConfig config;


    /**
     * Default http service registry
     */
    public PerContextHandlerRegistry(@NotNull final HttpConfig config)
    {
        this.config = config;
        this.serviceId = HttpServiceFactory.HTTP_SERVICE_CONTEXT_SERVICE_ID;
        this.ranking = Integer.MAX_VALUE;
        this.path = "/";
        this.prefix = null;
    }

    /**
     * Registry for a servlet context helper (whiteboard support)
     * @param info The servlet context helper info
     */
    public PerContextHandlerRegistry(@NotNull final ServletContextHelperInfo info, @NotNull final HttpConfig config)
    {
        this.config = config;
        this.serviceId = info.getServiceId();
        this.ranking = info.getRanking();
        this.path = info.getPath();
        if ( this.path.equals("/") )
        {
            this.prefix = null;
        }
        else
        {
            this.prefix = this.path + "/";
        }
    }

    public long getContextServiceId()
    {
        return this.serviceId;
    }

    public HttpConfig getConfig()
    {
        return this.config;
    }

    public void removeAll()
    {
        this.errorPageRegistry.cleanup();
        this.eventListenerRegistry.cleanup();
        this.filterRegistry.cleanup();
        this.servletRegistry.cleanup();
    }

    @Override
    public int compareTo(@NotNull final PerContextHandlerRegistry other)
    {
        final int result = Integer.valueOf(other.path.length()).compareTo(this.path.length());
        if ( result == 0 ) {
            if (this.ranking == other.ranking)
            {
                // Service id's can be negative. Negative id's follow the reverse natural ordering of integers.
                int reverseOrder = ( this.serviceId <= 0 && other.serviceId <= 0 ) ? -1 : 1;
                return reverseOrder * Long.valueOf(this.serviceId).compareTo(other.serviceId);
            }

            return Integer.valueOf(other.ranking).compareTo(this.ranking);
        }
        return result;
    }

    public String isMatching(@NotNull final String requestURI)
    {
        if (requestURI.equals(this.path))
        {
            return "";
        }
        if (this.prefix == null)
        {
            return requestURI;
        }
        if (requestURI.startsWith(this.prefix))
        {
            return requestURI.substring(this.prefix.length() - 1);
        }
        return null;
    }

    public PathResolution resolve(@NotNull final String relativeRequestURI)
    {
        return this.servletRegistry.resolve(relativeRequestURI);
    }

    public ServletHandler resolveServletByName(final String name)
    {
        return this.servletRegistry.resolveByName(name);
    }

    /**
     * Get filter handlers for the request uri
     * @param servletHandler The servlet handler (might be null)
     * @param dispatcherType The dispatcher type
     * @param requestURI The request uri
     * @return The array of filter handlers, the array might be empty.
     */
    public @NotNull FilterHandler[] getFilterHandlers(@Nullable final ServletHandler servletHandler,
            @NotNull final DispatcherType dispatcherType,
            @NotNull final String requestURI)
    {
        return this.filterRegistry.getFilterHandlers(servletHandler, dispatcherType, requestURI);
    }

    /**
     * Get the servlet handling the error.
     * @param code The error code
     * @param exception The optional exception
     * @return The servlet handler or {@code null}.
     */
    public @Nullable ServletHandler getErrorHandler(final int code, @Nullable final Throwable exception)
    {
        return this.errorPageRegistry.get(exception, code);
    }

    public EventListenerRegistry getEventListenerRegistry()
    {
        return this.eventListenerRegistry;
    }

    /**
     * Create all DTOs for servlets, filters, resources and error pages
     * @param dto The servlet context DTO
     * @param failedDTOHolder The container for all failed DTOs
     */
    public void getRuntime(final ServletContextDTO dto,
            final FailedDTOHolder failedDTOHolder)
    {
        // collect filters
        this.filterRegistry.getRuntimeInfo(dto, failedDTOHolder.failedFilterDTOs);

        // collect error pages
        this.errorPageRegistry.getRuntimeInfo(dto, failedDTOHolder.failedErrorPageDTOs);

        // collect servlets and resources
        this.servletRegistry.getRuntimeInfo(dto, failedDTOHolder.failedServletDTOs, failedDTOHolder.failedResourceDTOs);

        // collect listeners
        this.eventListenerRegistry.getRuntimeInfo(dto, failedDTOHolder.failedListenerDTOs);
    }

    /**
     * Add a servlet
     * @param handler The servlet handler
     */
    public void registerServlet(@NotNull final ServletHandler handler)
    {
        this.servletRegistry.addServlet(handler);
        this.errorPageRegistry.addServlet(handler);
    }

    /**
     * Remove a servlet
     * @param servletInfo The servlet info
     * @param destroy Destroy the servlet
     */
    public void unregisterServlet(@NotNull final ServletInfo servletInfo, final boolean destroy)
    {
        this.servletRegistry.removeServlet(servletInfo, destroy);
        this.errorPageRegistry.removeServlet(servletInfo, destroy);
    }

    /**
     * Add a filter
     * @param handler The filter handler
     */
    public void registerFilter(@NotNull final FilterHandler handler)
    {
        this.filterRegistry.addFilter(handler);
    }

    /**
     * Remove a filter
     * @param info The filter info
     * @param destroy Destroy the filter
     */
    public void unregisterFilter(@NotNull final FilterInfo info, final boolean destroy)
    {
        this.filterRegistry.removeFilter(info, destroy);
    }

    /**
     * Register listeners
     * @param listenerHandler
     */
    public void registerListeners(@NotNull final ListenerHandler listenerHandler)
    {
        this.eventListenerRegistry.addListeners(listenerHandler);
    }

    /**
     * Unregister listeners
     *
     * @param info The listener info
     */
    public void unregisterListeners(@NotNull final ListenerInfo info)
    {
        this.eventListenerRegistry.removeListeners(info);
    }

}
