/*
 * 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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;

import org.apache.felix.http.base.internal.handler.ServletHandler;
import org.apache.felix.http.base.internal.runtime.ServletInfo;
import org.apache.felix.http.base.internal.runtime.dto.BuilderConstants;
import org.apache.felix.http.base.internal.runtime.dto.ErrorPageDTOBuilder;
import org.osgi.service.servlet.runtime.dto.DTOConstants;
import org.osgi.service.servlet.runtime.dto.ErrorPageDTO;
import org.osgi.service.servlet.runtime.dto.FailedErrorPageDTO;
import org.osgi.service.servlet.runtime.dto.ServletContextDTO;

/**
 * The error page registry keeps tracks of the active/inactive servlets handling
 * error pages (error code and/or exception).
 * This registry is per servlet context.
 */
public final class ErrorPageRegistry
{
    private static final String CLIENT_ERROR = "4xx";
    private static final String SERVER_ERROR = "5xx";
    private static final Pattern ERROR_CODE_PATTERN = Pattern.compile("\\d{3}");

    private static final List<Long> CLIENT_ERROR_CODES = hundredOf(400);
    private static final List<Long> SERVER_ERROR_CODES = hundredOf(500);

    private final Map<String, List<ServletHandler>> errorMapping = new ConcurrentHashMap<String, List<ServletHandler>>();

    private volatile List<ErrorRegistrationStatus> status = Collections.emptyList();

    public static final class ErrorRegistration {
        public final long[] errorCodes;
        public final String[] exceptions;

        public ErrorRegistration(final long[] errorCodes, final String[] exceptions)
        {
            this.errorCodes = errorCodes;
            this.exceptions = exceptions;
        }
    }

    static final class ErrorRegistrationStatus implements Comparable<ErrorRegistrationStatus> {

        private final ServletHandler handler;
        public final Map<Integer, ErrorRegistration> reasonMapping = new HashMap<Integer, ErrorRegistration>();

        public final boolean usesClientErrorCodes;
        public final boolean usesServerErrorCodes;
        
        public ErrorRegistrationStatus(final ServletHandler handler)
        {
            this.handler = handler;
            this.usesClientErrorCodes = hasErrorCode(handler, CLIENT_ERROR);
            this.usesServerErrorCodes = hasErrorCode(handler, SERVER_ERROR);
        }

        public ServletHandler getHandler()
        {
            return this.handler;
        }

        @Override
        public int compareTo(final ErrorRegistrationStatus o)
        {
            return this.handler.compareTo(o.getHandler());
        }
    }

    private static boolean hasErrorCode(final ServletHandler handler, final String key) 
    {
    	for(final String val : handler.getServletInfo().getErrorPage())
    	{
    		if ( key.equals(val) ) 
    		{
    			return true;
    		}
    	}
    	return false;
    }
    
    private static List<Long> hundredOf(final int start)
    {
        List<Long> result = new ArrayList<Long>();
        for (long i = start; i < start + 100; i++)
        {
            result.add(i);
        }
        return Collections.unmodifiableList(result);
    }

    private static long[] toLongArray(final Set<Long> set)
    {
        long[] codes = BuilderConstants.EMPTY_LONG_ARRAY;
        if ( !set.isEmpty() )
        {
            codes = new long[set.size()];
            int index = 0;
            for(final Long code : set)
            {
                codes[index++] = code;
            }
        }
        return codes;
    }

    private static Set<Long> toLongSet(final long[] codes)
    {
        final Set<Long> set = new TreeSet<Long>();
        for(final long c : codes)
        {
            set.add(c);
        }
        return set;
    }

    private static String[] toStringArray(final Set<String> set)
    {
        String[] array = BuilderConstants.EMPTY_STRING_ARRAY;
        if ( !set.isEmpty() )
        {
            array = set.toArray(new String[set.size()]);
        }
        return array;
    }

    private static Set<String> toStringSet(final String[] array)
    {
        final Set<String> set = new TreeSet<String>();
        for(final String s : array)
        {
            set.add(s);
        }
        return set;
    }

    private static String parseErrorCodes(final Set<Long> codes, final String string)
    {
        if (CLIENT_ERROR.equalsIgnoreCase(string))
        {
            codes.addAll(CLIENT_ERROR_CODES);
        }
        else if (SERVER_ERROR.equalsIgnoreCase(string))
        {
            codes.addAll(SERVER_ERROR_CODES);
        }
        else if (ERROR_CODE_PATTERN.matcher(string).matches())
        {
            codes.add(Long.parseLong(string));
        }
        else
        {
            return string;
        }
        return null;
    }

    /**
     * Parse the registration properties of the servlet for error handling
     * @param info The servlet info
     * @return An error registration object if the servlet handles errors
     */
    public static @Nullable ErrorRegistration getErrorRegistration(@NotNull final ServletInfo info)
    {
        if ( info.getErrorPage() != null )
        {
            final Set<Long> errorCodes = new TreeSet<Long>();
            final Set<String> exceptions = new TreeSet<String>();

            for(final String val : info.getErrorPage())
            {
                final String exception = parseErrorCodes(errorCodes, val);
                if ( exception != null )
                {
                    exceptions.add(exception);
                }
            }
            final long[] codes = toLongArray(errorCodes);
            final String[] exceptionsArray = toStringArray(exceptions);

            return new ErrorRegistration(codes, exceptionsArray);
        }
        return null;
    }

    /**
     * Add the servlet for error handling
     * @param handler The servlet handler.
     */
    public synchronized void addServlet(@NotNull final ServletHandler handler)
    {
        final ErrorRegistration reg = getErrorRegistration(handler.getServletInfo());
        if ( reg != null )
        {
            final ErrorRegistrationStatus status = new ErrorRegistrationStatus(handler);
            for(final long code : reg.errorCodes)
            {
                addErrorHandling(handler, status, code, null);
            }
            for(final String exception : reg.exceptions)
            {
                addErrorHandling(handler, status, 0, exception);
            }
            final List<ErrorRegistrationStatus> newList = new ArrayList<ErrorPageRegistry.ErrorRegistrationStatus>(this.status);
            newList.add(status);
            Collections.sort(newList);
            this.status = newList;
        }
    }

    /**
     * Remove the servlet from error handling
     * @param info The servlet info.
     * @param destroy Unused
     */
    public synchronized void removeServlet(@NotNull final ServletInfo info, final boolean destroy)
    {
        final ErrorRegistration reg = getErrorRegistration(info);
        if ( reg != null )
        {
            final List<ErrorRegistrationStatus> newList = new ArrayList<ErrorPageRegistry.ErrorRegistrationStatus>(this.status);
            final Iterator<ErrorRegistrationStatus> i = newList.iterator();
            while ( i.hasNext() )
            {
                final ErrorRegistrationStatus status = i.next();
                if ( status.handler.getServletInfo().equals(info) )
                {
                    i.remove();
                    break;
                }
            }
            this.status = newList;

            for(final long code : reg.errorCodes)
            {
                removeErrorHandling(info, code, null);
            }
            for(final String exception : reg.exceptions)
            {
                removeErrorHandling(info, 0, exception);
            }
        }
    }

    public synchronized void cleanup()
    {
        this.errorMapping.clear();
        this.status = Collections.emptyList();
    }

    private void addErrorHandling(final ServletHandler handler, final ErrorRegistrationStatus status, final long code, final String exception)
    {
        final String key = (exception != null ? exception : String.valueOf(code));

        final List<ServletHandler> newList;
        final List<ServletHandler> list = errorMapping.get(key);
        if ( list == null )
        {
            newList = Collections.singletonList(handler);
        }
        else
        {
            newList = new ArrayList<ServletHandler>(list);
            newList.add(handler);
            Collections.sort(newList);
        }
        if ( newList.get(0) == handler )
        {
            // try to activate (and deactivate old handler)
            final int result = handler.init();
            addReason(status, code, exception, result);
            if ( result == -1 )
            {
                if ( list != null )
                {
                    final ServletHandler old = list.get(0);
                    old.destroy();
                    errorMapping.put(key, newList);
                    ErrorRegistrationStatus oldStatus = null;
                    final Iterator<ErrorRegistrationStatus> i = this.status.iterator();
                    while ( oldStatus == null && i.hasNext() )
                    {
                        final ErrorRegistrationStatus current = i.next();
                        if ( current.handler.getServletInfo().equals(old.getServletInfo()) )
                        {
                            oldStatus = current;
                        }
                    }
                    if ( oldStatus != null )
                    {
                        removeReason(oldStatus, code, exception, -1);
                    	boolean addReason = true;
                    	if ( exception == null )
                    	{
                    		if ( code >= 400 && code < 500 && oldStatus.usesClientErrorCodes && !status.usesClientErrorCodes )
                    		{
                    			addReason = false;
                    		} 
                    		else if ( code >= 500 && code < 600 && oldStatus.usesServerErrorCodes && !status.usesServerErrorCodes )
                    		{
                    			addReason = false;
                    		}
                    	}
                    	if ( addReason )
                    	{
                    		addReason(oldStatus, code, exception, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE);
                    	}
                    }
                }
                else
                {
                    errorMapping.put(key, newList);
                }
            }
        }
        else
        {
            // failure
        	boolean addReason = true;
        	if ( exception == null )
        	{
        		if ( code >= 400 && code < 500 && status.usesClientErrorCodes && !hasErrorCode(newList.get(0), CLIENT_ERROR) )
        		{
        			addReason = false;
        		} 
        		else if ( code >= 500 && code < 600 && status.usesServerErrorCodes && !hasErrorCode(newList.get(0), SERVER_ERROR) )
        		{
        			addReason = false;
        		}
        	}
        	if ( addReason )
        	{
        		addReason(status, code, exception, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE);
        	}
            errorMapping.put(key, newList);
        }
    }

    /**
     * Make an entry in the status object (which are used to create the DTOs)
     * @param status The status object
     * @param code Either the code
     * @param exception or the exception
     * @param reason The code for the failure reason or {@code -1} for success.	
     */
    private void addReason(final ErrorRegistrationStatus status, 
    		final long code, 
    		final String exception, 
    		final int reason)
    {
        ErrorRegistration reg = status.reasonMapping.get(reason);
        if ( reg == null )
        {
            if ( exception != null )
            {
                reg = new ErrorRegistration(BuilderConstants.EMPTY_LONG_ARRAY, new String[] {exception});
            }
            else
            {
                reg = new ErrorRegistration(new long[] {code}, BuilderConstants.EMPTY_STRING_ARRAY);
            }
        }
        else
        {
            long[] codes = reg.errorCodes;
            String[] exceptions = reg.exceptions;
            if ( exception != null )
            {
                final Set<String> set = toStringSet(exceptions);
                set.add(exception);
                exceptions = toStringArray(set);
            }
            else
            {
                final Set<Long> set = toLongSet(codes);
                set.add(code);
                codes = toLongArray(set);
            }

            reg = new ErrorRegistration(codes, exceptions);
        }
        status.reasonMapping.put(reason, reg);
    }

    private void removeReason(final ErrorRegistrationStatus status, final long code, final String exception, final int reason)
    {
        ErrorRegistration reg = status.reasonMapping.get(reason);
        if ( reg != null )
        {
            long[] codes = reg.errorCodes;
            String[] exceptions = reg.exceptions;
            if ( exception != null )
            {
                final Set<String> set = toStringSet(exceptions);
                set.remove(exception);
                exceptions = toStringArray(set);
            }
            else
            {
                final Set<Long> set = toLongSet(codes);
                set.remove(code);
                codes = toLongArray(set);
            }
            if ( codes.length == 0 && exceptions.length == 0 )
            {
                status.reasonMapping.remove(reason);
            }
            else
            {
                status.reasonMapping.put(reason, new ErrorRegistration(codes, exceptions));
            }
        }
    }

    private void removeErrorHandling(final ServletInfo info, final long code, final String exception)
    {
        final String key = (exception != null ? exception : String.valueOf(code));

        final List<ServletHandler> list = errorMapping.get(key);
        if ( list != null )
        {
            int index = 0;
            final Iterator<ServletHandler> i = list.iterator();
            while ( i.hasNext() )
            {
                final ServletHandler handler = i.next();
                if ( handler.getServletInfo().equals(info) )
                {
                    final List<ServletHandler> newList = new ArrayList<ServletHandler>(list);
                    newList.remove(handler);

                    if ( index == 0 )
                    {
                        handler.destroy();

                        index++;
                        while ( index < list.size() )
                        {
                            final ServletHandler next = list.get(index);
                            ErrorRegistrationStatus nextStatus = null;
                            for(final ErrorRegistrationStatus s : this.status)
                            {
                                if ( s.handler.getServletInfo().equals(next.getServletInfo()) )
                                {
                                    nextStatus = s;
                                    break;
                                }
                            }
                            this.removeReason(nextStatus, code, exception, DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE);
                            final int reason = next.init();
                            this.addReason(nextStatus, code, exception, reason);
                            if ( reason == -1 )
                            {
                                break;
                            }
                            else
                            {
                                newList.remove(next);
                            }
                        }
                    }
                    if ( newList.isEmpty() )
                    {
                        errorMapping.remove(key);
                    }
                    else
                    {
                        errorMapping.put(key, newList);
                    }

                    break;
                }
                index++;
            }
        }
    }

    /**
     * Get the servlet handling the error (error code or exception).
     * If an exception is provided, a handler for the exception is searched first.
     * If no handler is found (or no exception provided) a handler for the error
     * code is searched.
     * @param exception Optional exception
     * @param errorCode Error code
     * @return The servlet handling the error or {@code null}
     */
    public ServletHandler get(final Throwable exception, final int errorCode)
    {
        ServletHandler errorHandler = this.get(exception);
        if (errorHandler != null)
        {
            return errorHandler;
        }

        return get(errorCode);
    }

    /**
     * Get the servlet handling the error code
     * @param errorCode Error code
     * @return The servlet handling the error or {@code null}
     */
    private ServletHandler get(final long errorCode)
    {
        final List<ServletHandler> list = this.errorMapping.get(String.valueOf(errorCode));
        if ( list != null )
        {
            return list.get(0);
        }
        return null;
    }

    /**
     * Get the servlet handling the exception
     * @param exception Error exception
     * @return The servlet handling the error or {@code null}
     */
    private ServletHandler get(final Throwable exception)
    {
        if (exception == null)
        {
            return null;
        }

        ServletHandler servletHandler = null;
        Class<?> throwableClass = exception.getClass();
        while ( servletHandler == null && throwableClass != null )
        {
            final List<ServletHandler> list = this.errorMapping.get(throwableClass.getName());
            if ( list != null )
            {
                servletHandler = list.get(0);
            }
            if ( servletHandler == null )
            {
                throwableClass = throwableClass.getSuperclass();
                if ( !Throwable.class.isAssignableFrom(throwableClass) )
                {
                    throwableClass = null;
                }
            }

        }
        return servletHandler;
    }

    /**
     * Get DTOs for error pages.
     * @param dto The servlet context DTO
     * @param failedErrorPageDTOs The failed error page DTOs
     */
    public void getRuntimeInfo(final ServletContextDTO dto,
            final Collection<FailedErrorPageDTO> failedErrorPageDTOs)
    {
        final List<ErrorPageDTO> errorPageDTOs = new ArrayList<ErrorPageDTO>();
        final List<ErrorRegistrationStatus> statusList = this.status;
        for(final ErrorRegistrationStatus status : statusList)
        {
            for(final Map.Entry<Integer, ErrorRegistration> entry : status.reasonMapping.entrySet())
            {
                final ErrorPageDTO state = ErrorPageDTOBuilder.build(status.getHandler(), entry.getKey());
                state.errorCodes = Arrays.copyOf(entry.getValue().errorCodes, entry.getValue().errorCodes.length);
                state.exceptions = Arrays.copyOf(entry.getValue().exceptions, entry.getValue().exceptions.length);

                if ( entry.getKey() == -1 )
                {
                    errorPageDTOs.add(state);
                }
                else
                {
                    failedErrorPageDTOs.add((FailedErrorPageDTO)state);
                }
            }
        }
        if ( !errorPageDTOs.isEmpty() )
        {
            dto.errorPageDTOs = errorPageDTOs.toArray(new ErrorPageDTO[errorPageDTOs.size()]);
        }
    }
}
