/*
 * 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 java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;
import jakarta.servlet.DispatcherType;

import org.apache.felix.http.base.internal.handler.FilterHandler;
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.dto.FilterDTOBuilder;
import org.osgi.service.servlet.runtime.dto.FailedFilterDTO;
import org.osgi.service.servlet.runtime.dto.FilterDTO;
import org.osgi.service.servlet.runtime.dto.ServletContextDTO;

/**
 * The filter registry keeps track of all filter mappings for a single servlet context.
 *
 */
public final class FilterRegistry
{
    /** List of all filter registrations. These are sorted by the status objects. */
    private volatile List<FilterRegistrationStatus> filters = Collections.emptyList();

    /**
     * The status object keeps track of the registration status of a filter and holds
     * the resolvers to match against a uri.
     * The status objects are sorted by result first, followed by ranking. The active
     * filters ( result == -1) are first, followed by the inactive ones. This sorting
     * allows to only traverse over the active ones and also avoids any sorting
     * as the filters are processed in the correct order already.
     */
    private static final class FilterRegistrationStatus implements Comparable<FilterRegistrationStatus>
    {
        private final int result;
        private final FilterHandler handler;
        private final PathResolver[] resolvers;

        public FilterRegistrationStatus(@NotNull final FilterHandler handler, @Nullable final PathResolver[] resolvers, final int result)
        {
            this.handler = handler;
            this.resolvers = resolvers;
            this.result = result;
        }

        public int getResult()
        {
            return this.result;
        }

        public @NotNull FilterHandler getHandler()
        {
            return this.handler;
        }

        public @Nullable PathResolver[] getResolvers()
        {
            return this.resolvers;
        }

        @Override
        public int compareTo(final FilterRegistrationStatus o) {
            int result = this.result - o.result;
            if ( result == 0 )
            {
                result = this.handler.compareTo(o.handler);
            }
            return result;
        }
    }

    /**
     * Add a filter.
     * @param handler The handler for the filter
     */
    public synchronized void addFilter(@NotNull final FilterHandler handler)
    {
        final int result = handler.init();
        PathResolver[] prs = null;

        if ( result == -1 )
        {
            final List<PathResolver> resolvers = new ArrayList<PathResolver>();
            if ( handler.getFilterInfo().getPatterns() != null )
            {
                for(final String pattern : handler.getFilterInfo().getPatterns() ) {
                    resolvers.add(PathResolverFactory.createPatternMatcher(null, pattern));
                }
            }
            if ( handler.getFilterInfo().getRegexs() != null )
            {
                for(final String regex : handler.getFilterInfo().getRegexs() ) {
                    resolvers.add(PathResolverFactory.createRegexMatcher(regex));
                }
            }
            Collections.sort(resolvers);

            prs = resolvers.toArray(new PathResolver[resolvers.size()]);
        }

        final FilterRegistrationStatus status = new FilterRegistrationStatus(handler, prs, result);

        final List<FilterRegistrationStatus> newList = new ArrayList<FilterRegistry.FilterRegistrationStatus>(this.filters);
        newList.add(status);
        Collections.sort(newList);

        this.filters = newList;
    }

    /**
     * Remove a filter
     * @param filterInfo The filter info
     * @param destroy boolean flag indicating whether to call destroy on the filter.
     */
    public synchronized void removeFilter(@NotNull final FilterInfo filterInfo, final boolean destroy)
    {
        FilterRegistrationStatus found = null;
        final List<FilterRegistrationStatus> newList = new ArrayList<FilterRegistry.FilterRegistrationStatus>(this.filters);
        final Iterator<FilterRegistrationStatus> i = newList.iterator();
        while ( i.hasNext() )
        {
            final FilterRegistrationStatus status = i.next();
            if ( status.getHandler().getFilterInfo().equals(filterInfo) )
            {
                found = status;
                i.remove();
                break;
            }
        }
        if ( found != null )
        {
            this.filters = newList;

            if ( found.getResult() == -1 && destroy )
            {
                found.getHandler().dispose();
            }
        }
    }

    public synchronized void cleanup()
    {
        this.filters = Collections.emptyList();
    }

    /**
     * Get all filters handling the request.
     * Filters are applied to the url and/or the servlet
     * @param handler Optional servlet handler
     * @param dispatcherType The dispatcher type
     * @param requestURI The request uri
     * @return The array of filter handlers, might be empty.
     */
    public @NotNull FilterHandler[] getFilterHandlers(@Nullable final ServletHandler handler,
            @NotNull final DispatcherType dispatcherType,
            @NotNull final String requestURI)
    {
        final List<FilterHandler> result = new ArrayList<FilterHandler>();
        final List<FilterRegistrationStatus> allFilters = this.filters;

        for(final FilterRegistrationStatus status : allFilters)
        {
            // as soon as we encounter a failing filter, we can stop
            if ( status.getResult() != -1 )
            {
                break;
            }
            if (referencesDispatcherType(status.getHandler(), dispatcherType) )
            {
                boolean added = false;
                for(final PathResolver resolver : status.getResolvers())
                {
                    if ( resolver.resolve(requestURI) != null )
                    {
                        result.add(status.getHandler());
                        added = true;
                        break;
                    }
                }
                // check for servlet name if it's not a resource
                final String servletName = (handler != null && !handler.getServletInfo().isResource()) ? handler.getName() : null;
                if ( !added && servletName != null && status.getHandler().getFilterInfo().getServletNames() != null )
                {
                    for(final String name : status.getHandler().getFilterInfo().getServletNames())
                    {
                        if ( servletName.equals(name) )
                        {
                            result.add(status.getHandler());
                            added = true;
                            break;
                        }
                    }
                }

            }
        }

        return result.toArray(new FilterHandler[result.size()]);
    }

    /**
     * Check if the filter is registered for the required dispatcher type
     * @param handler The filter handler
     * @param dispatcherType The requested dispatcher type
     * @return {@code true} if the filter can be applied.
     */
    private boolean referencesDispatcherType(final FilterHandler handler, final DispatcherType dispatcherType)
    {
        for(final DispatcherType dt : handler.getFilterInfo().getDispatcher())
        {
            if ( dt == dispatcherType )
            {
                return true;
            }
        }
        return false;
    }

    /**
     * Get the runtime information about filters
     * @param servletContextDTO The servlet context DTO
     * @param failedFilterDTOs The collection holding the failed filters.
     */
    public void getRuntimeInfo(final ServletContextDTO servletContextDTO,
                               final Collection<FailedFilterDTO> failedFilterDTOs)
    {
        final List<FilterDTO> filterDTOs = new ArrayList<FilterDTO>();

        final List<FilterRegistrationStatus> allFilters = this.filters;
        for(final FilterRegistrationStatus status : allFilters)
        {
            if ( status.getResult() != -1 )
            {
                failedFilterDTOs.add((FailedFilterDTO)FilterDTOBuilder.build(status.getHandler(), status.getResult()));
            }
            else
            {
                filterDTOs.add(FilterDTOBuilder.build(status.getHandler(), status.getResult()));
            }
        }

        if ( !filterDTOs.isEmpty() )
        {
            servletContextDTO.filterDTOs = filterDTOs.toArray(new FilterDTO[filterDTOs.size()]);
        }
    }
}
