/*
 * 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.whiteboard;

import static org.osgi.service.servlet.runtime.dto.DTOConstants.FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING;
import static org.osgi.service.servlet.runtime.dto.DTOConstants.FAILURE_REASON_SERVICE_NOT_GETTABLE;
import static org.osgi.service.servlet.runtime.dto.DTOConstants.FAILURE_REASON_SERVLET_CONTEXT_FAILURE;
import static org.osgi.service.servlet.runtime.dto.DTOConstants.FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE;
import static org.osgi.service.servlet.runtime.dto.DTOConstants.FAILURE_REASON_UNKNOWN;
import static org.osgi.service.servlet.runtime.dto.DTOConstants.FAILURE_REASON_VALIDATION_FAILED;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.felix.http.base.internal.logger.SystemLogger;
import org.apache.felix.http.base.internal.runtime.AbstractInfo;
import org.apache.felix.http.base.internal.runtime.dto.FailedDTOHolder;
import org.osgi.framework.ServiceReference;

public class FailureStateHandler {

    private static final class FailureStatus
    {
        public final Map<Integer, Set<Long>> reasonToContextsMapping = new ConcurrentHashMap<>();
    }

    private final Map<AbstractInfo<?>, FailureStatus> serviceFailures = new ConcurrentHashMap<>();

    /**
     * Remove all failures.
     */
    public void clear()
    {
        this.serviceFailures.clear();
    }

    public void addFailure(final AbstractInfo<?> info, final int reason, final Exception ex)
    {
        this.addFailure(info, 0, reason, ex);
    }

    public void addFailure(final AbstractInfo<?> info, final int reason)
    {
        this.addFailure(info, 0, reason);
    }

    public void addFailure(final AbstractInfo<?> info, final long contextId, final int reason)
    {
        this.addFailure(info, contextId, reason, null);
    }

    public void addFailure(final AbstractInfo<?> info, final long contextId, final int reason, final Exception ex)
    {
        final String type = info.getType();
        final String serviceInfo;
        final ServiceReference<?> ref = info.getServiceReference();
        if ( ref == null ) {
            serviceInfo = " with id " + String.valueOf(info.getServiceId());
        } else {
            serviceInfo = "";
        }
        if ( reason == FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING )
        {
            SystemLogger.LOGGER.debug(SystemLogger.formatMessage(ref, "Ignoring unmatching " + type + " service" + serviceInfo));
        }
        else if ( reason == FAILURE_REASON_SHADOWED_BY_OTHER_SERVICE )
        {
            SystemLogger.LOGGER.debug(SystemLogger.formatMessage(ref, "Ignoring shadowed " + type + " service" + serviceInfo));
        }
        else if ( reason == FAILURE_REASON_SERVICE_NOT_GETTABLE )
        {
            SystemLogger.LOGGER.error(SystemLogger.formatMessage(ref, "Ignoring ungettable " + type + " service" + serviceInfo), ex);
        }
        else if ( reason == FAILURE_REASON_VALIDATION_FAILED )
        {
            SystemLogger.LOGGER.debug(SystemLogger.formatMessage(ref, "Ignoring invalid " + type + " service" + serviceInfo));
        }
        else if ( reason == FAILURE_REASON_NO_SERVLET_CONTEXT_MATCHING )
        {
            SystemLogger.LOGGER.debug(SystemLogger.formatMessage(ref, "Ignoring unmatched " + type + " service" + serviceInfo));
        }
        else if ( reason == FAILURE_REASON_SERVLET_CONTEXT_FAILURE )
        {
            SystemLogger.LOGGER.debug(SystemLogger.formatMessage(ref,  "Servlet context " + String.valueOf(contextId) + " failure: Ignoring " + type + " service" + serviceInfo));
        }
        else if ( reason == FAILURE_REASON_UNKNOWN)
        {
            SystemLogger.LOGGER.error(SystemLogger.formatMessage(ref, "Exception while registering " + type + " service" + serviceInfo), ex);
        }

        FailureStatus status = serviceFailures.get(info);
        if ( status == null )
        {
            // we don't need to sync the add operation, that's taken care of by the caller.
            status = new FailureStatus();
            this.serviceFailures.put(info, status);
        }
        Set<Long> contexts = status.reasonToContextsMapping.get(reason);
        if ( contexts == null )
        {
            contexts = new HashSet<>();
        }
        else
        {
            contexts = new HashSet<>(contexts);
        }
        contexts.add(contextId);
        status.reasonToContextsMapping.put(reason, contexts);
    }

    public boolean remove(final AbstractInfo<?> info)
    {
        return remove(info, 0);
    }

    public boolean removeAll(final AbstractInfo<?> info)
    {
        final boolean result = remove(info, 0);
        this.serviceFailures.remove(info);
        return result;
    }

    public boolean remove(final AbstractInfo<?> info, final long contextId)
    {
        final FailureStatus status = serviceFailures.get(info);
        if ( status != null )
        {
            final Iterator<Map.Entry<Integer, Set<Long>>> i = status.reasonToContextsMapping.entrySet().iterator();
            while ( i.hasNext() )
            {
                final Map.Entry<Integer, Set<Long>> entry = i.next();
                if ( entry.getValue().contains(contextId) )
                {
                    if ( entry.getValue().size() == 1 )
                    {
                        i.remove();
                    }
                    else
                    {
                        final Set<Long> set = new HashSet<>(entry.getValue());
                        set.remove(contextId);
                        entry.setValue(set);
                    }
                    return true;
                }
            }
        }
        return false;
    }

    public void getRuntimeInfo(final FailedDTOHolder failedDTOHolder) {
        for(final Map.Entry<AbstractInfo<?>, FailureStatus> entry : this.serviceFailures.entrySet() )
        {
            final Iterator<Map.Entry<Integer, Set<Long>>> i = entry.getValue().reasonToContextsMapping.entrySet().iterator();
            while ( i.hasNext() )
            {
                final Map.Entry<Integer, Set<Long>> status = i.next();

                for(final long contextId : status.getValue())
                {
                    failedDTOHolder.add(entry.getKey(), contextId, status.getKey());
                }
            }
        }

    }
}
