/*
 *
 * 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.qpid.server.model.adapter;

import java.lang.reflect.Type;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.qpid.server.model.AuthenticationProvider;
import org.apache.qpid.server.model.Broker;
import org.apache.qpid.server.model.ConfiguredObject;
import org.apache.qpid.server.model.Connection;
import org.apache.qpid.server.model.KeyStore;
import org.apache.qpid.server.model.LifetimePolicy;
import org.apache.qpid.server.model.Port;
import org.apache.qpid.server.model.Protocol;
import org.apache.qpid.server.model.State;
import org.apache.qpid.server.model.Statistics;
import org.apache.qpid.server.model.Transport;
import org.apache.qpid.server.model.TrustStore;
import org.apache.qpid.server.model.VirtualHost;
import org.apache.qpid.server.model.VirtualHostAlias;
import org.apache.qpid.server.security.access.Operation;
import org.apache.qpid.server.util.MapValueConverter;
import org.apache.qpid.server.util.ParameterizedTypeImpl;
import org.apache.qpid.server.configuration.IllegalConfigurationException;
import org.apache.qpid.server.configuration.updater.TaskExecutor;

public class PortAdapter extends AbstractAdapter implements Port
{
    @SuppressWarnings("serial")
    public static final Map<String, Type> ATTRIBUTE_TYPES = Collections.unmodifiableMap(new HashMap<String, Type>(){{
        put(NAME, String.class);
        put(PROTOCOLS, new ParameterizedTypeImpl(Set.class, Protocol.class));
        put(TRANSPORTS, new ParameterizedTypeImpl(Set.class, Transport.class));
        put(TRUST_STORES, new ParameterizedTypeImpl(Set.class, String.class));
        put(KEY_STORE, String.class);
        put(PORT, Integer.class);
        put(TCP_NO_DELAY, Boolean.class);
        put(RECEIVE_BUFFER_SIZE, Integer.class);
        put(SEND_BUFFER_SIZE, Integer.class);
        put(NEED_CLIENT_AUTH, Boolean.class);
        put(WANT_CLIENT_AUTH, Boolean.class);
        put(BINDING_ADDRESS, String.class);
        put(STATE, State.class);
        put(AUTHENTICATION_PROVIDER, String.class);
    }});

    private final Broker _broker;
    private AuthenticationProvider _authenticationProvider;
    private AtomicReference<State> _state;

    public PortAdapter(UUID id, Broker broker, Map<String, Object> attributes, Map<String, Object> defaults, TaskExecutor taskExecutor)
    {
        super(id, defaults, MapValueConverter.convert(attributes, ATTRIBUTE_TYPES), taskExecutor);
        _broker = broker;
        State state = MapValueConverter.getEnumAttribute(State.class, STATE, attributes, State.INITIALISING);

        Collection<Protocol> protocols = getProtocols();
        boolean rmiRegistry = protocols != null && protocols.contains(Protocol.RMI);
        if (!rmiRegistry)
        {
            String authProvider = (String)getAttribute(Port.AUTHENTICATION_PROVIDER);
            if (authProvider == null)
            {
                throw new IllegalConfigurationException("An authentication provider must be specified for port : " + getName());
            }
            _authenticationProvider = broker.findAuthenticationProviderByName(authProvider);

            if(_authenticationProvider == null)
            {
                throw new IllegalConfigurationException("The authentication provider '" + authProvider + "' could not be found for port : " + getName());
            }
        }

        _state = new AtomicReference<State>(state);
        addParent(Broker.class, broker);
    }

    @Override
    public String getBindingAddress()
    {
        return (String)getAttribute(BINDING_ADDRESS);
    }

    @Override
    public int getPort()
    {
        return (Integer)getAttribute(PORT);
    }

    @SuppressWarnings("unchecked")
    @Override
    public Collection<Transport> getTransports()
    {
        return (Collection<Transport>)getAttribute(TRANSPORTS);
    }

    @Override
    public void addTransport(Transport transport)
            throws IllegalStateException, AccessControlException, IllegalArgumentException
    {
        throw new IllegalStateException();
    }

    @Override
    public Transport removeTransport(Transport transport)
            throws IllegalStateException, AccessControlException, IllegalArgumentException
    {
        throw new IllegalStateException();
    }

    @SuppressWarnings("unchecked")
    @Override
    public Collection<Protocol> getProtocols()
    {
        return (Collection<Protocol>)getAttribute(PROTOCOLS);
    }

    @Override
    public void addProtocol(Protocol protocol)
            throws IllegalStateException, AccessControlException, IllegalArgumentException
    {
        throw new IllegalStateException();
    }

    @Override
    public Protocol removeProtocol(Protocol protocol)
            throws IllegalStateException, AccessControlException, IllegalArgumentException
    {
        throw new IllegalStateException();
    }

    @Override
    public Collection<VirtualHostAlias> getVirtualHostBindings()
    {
        List<VirtualHostAlias> aliases = new ArrayList<VirtualHostAlias>();
        for(VirtualHost vh : _broker.getVirtualHosts())
        {
            for(VirtualHostAlias alias : vh.getAliases())
            {
                if(alias.getPort().equals(this))
                {
                    aliases.add(alias);
                }
            }
        }
        return Collections.unmodifiableCollection(aliases);
    }

    @Override
    public Collection<Connection> getConnections()
    {
        return null;
    }

    @Override
    public String getName()
    {
        return (String)getAttribute(NAME);
    }

    @Override
    public String setName(String currentName, String desiredName) throws IllegalStateException, AccessControlException
    {
        throw new IllegalStateException();
    }

    @Override
    public State getActualState()
    {
        return _state.get();
    }

    @Override
    public boolean isDurable()
    {
        return false;
    }

    @Override
    public void setDurable(boolean durable)
            throws IllegalStateException, AccessControlException, IllegalArgumentException
    {
        throw new IllegalStateException();
    }

    @Override
    public LifetimePolicy getLifetimePolicy()
    {
        return LifetimePolicy.PERMANENT;
    }

    @Override
    public LifetimePolicy setLifetimePolicy(LifetimePolicy expected, LifetimePolicy desired)
            throws IllegalStateException, AccessControlException, IllegalArgumentException
    {
        throw new IllegalStateException();
    }

    @Override
    public long getTimeToLive()
    {
        return 0;
    }

    @Override
    public long setTimeToLive(long expected, long desired)
            throws IllegalStateException, AccessControlException, IllegalArgumentException
    {
        throw new IllegalStateException();
    }

    @Override
    public Statistics getStatistics()
    {
        return NoStatistics.getInstance();
    }

    @Override
    public <C extends ConfiguredObject> Collection<C> getChildren(Class<C> clazz)
    {
        if(clazz == Connection.class)
        {
            return (Collection<C>) getConnections();
        }
        else
        {
            return Collections.emptySet();
        }
    }

    @Override
    public <C extends ConfiguredObject> C createChild(Class<C> childClass, Map<String, Object> attributes, ConfiguredObject... otherParents)
    {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object getAttribute(String name)
    {
        if(ID.equals(name))
        {
            return getId();
        }
        else if(STATE.equals(name))
        {
            return getActualState();
        }
        else if(DURABLE.equals(name))
        {
            return isDurable();
        }
        else if(LIFETIME_POLICY.equals(name))
        {
            return getLifetimePolicy();
        }
        else if(TIME_TO_LIVE.equals(name))
        {
            return getTimeToLive();
        }
        else if(CREATED.equals(name))
        {

        }
        else if(UPDATED.equals(name))
        {

        }
        return super.getAttribute(name);
    }

    @Override
    public Collection<String> getAttributeNames()
    {
        return AVAILABLE_ATTRIBUTES;
    }

    @Override
    public boolean setState(State currentState, State desiredState)
    {
        State state = _state.get();
        if (desiredState == State.DELETED)
        {
            if (state == State.STOPPED || state == State.QUIESCED)
            {
                return _state.compareAndSet(state, State.DELETED);
            }
            else
            {
                throw new IllegalStateException("Cannot delete port in " + state + " state");
            }
        }
        else if (desiredState == State.ACTIVE)
        {
            if ((state == State.INITIALISING || state == State.QUIESCED) && _state.compareAndSet(state, State.ACTIVE))
            {
                onActivate();
                return true;
            }
            else
            {
                throw new IllegalStateException("Cannot activate port in " + state + " state");
            }
        }
        else if (desiredState == State.QUIESCED)
        {
            if (state == State.INITIALISING && _state.compareAndSet(state, State.QUIESCED))
            {
                return true;
            }
        }
        else if (desiredState == State.STOPPED)
        {
            if (_state.compareAndSet(state, State.STOPPED))
            {
                onStop();
                return true;
            }
            else
            {
                throw new IllegalStateException("Cannot stop port in " + state + " state");
            }
        }
        return false;
    }

    protected void onActivate()
    {
        // no-op: expected to be overridden by subclass
    }

    protected void onStop()
    {
        // no-op: expected to be overridden by subclass
    }

    @Override
    public AuthenticationProvider getAuthenticationProvider()
    {
        return _authenticationProvider;
    }

    @Override
    protected void changeAttributes(Map<String, Object> attributes)
    {
        if (getActualState() == State.ACTIVE && !_broker.isManagementMode())
        {
            throw new IllegalStateException("Cannot change attributes for an active port outside of Management Mode");
        }
        Map<String, Object> converted = MapValueConverter.convert(attributes, ATTRIBUTE_TYPES);

        Map<String, Object> merged =  new HashMap<String, Object>(getDefaultAttributes());
        merged.putAll(getActualAttributes());
        merged.putAll(converted);

        @SuppressWarnings("unchecked")
        Collection<Transport> transports = (Collection<Transport>)merged.get(TRANSPORTS);
        @SuppressWarnings("unchecked")
        Collection<Protocol> protocols = (Collection<Protocol>)merged.get(PROTOCOLS);
        Boolean needClientCertificate = (Boolean)merged.get(NEED_CLIENT_AUTH);
        Boolean wantClientCertificate = (Boolean)merged.get(WANT_CLIENT_AUTH);
        boolean requiresCertificate = (needClientCertificate != null && needClientCertificate.booleanValue())
                || (wantClientCertificate != null && wantClientCertificate.booleanValue());

        String keyStoreName = (String) merged.get(KEY_STORE);
        boolean hasKeyStore = keyStoreName != null;
        if(keyStoreName != null)
        {
            if (_broker.findKeyStoreByName(keyStoreName) == null)
            {
                throw new IllegalConfigurationException("Can't find key store with name '" + keyStoreName + "' for port " + getName());
            }
        }

        Set<String> trustStoreNames = (Set<String>) merged.get(TRUST_STORES);
        boolean hasTrustStore = trustStoreNames != null && !trustStoreNames.isEmpty();
        if(hasTrustStore)
        {
            for (String trustStoreName : trustStoreNames)
            {
                if (_broker.findTrustStoreByName(trustStoreName) == null)
                {
                    throw new IllegalConfigurationException("Cannot find trust store with name '" + trustStoreName + "'");
                }
            }
        }

        boolean usesSsl = transports != null && transports.contains(Transport.SSL);
        if (usesSsl)
        {
            if (keyStoreName == null)
            {
                throw new IllegalConfigurationException("Can't create port which requires SSL but has no key store configured.");
            }

            if (!hasTrustStore && requiresCertificate)
            {
                throw new IllegalConfigurationException("Can't create port which requests SSL client certificates but has no trust store configured.");
            }
        }
        else
        {
            if (requiresCertificate)
            {
                throw new IllegalConfigurationException("Can't create port which requests SSL client certificates but doesn't use SSL transport.");
            }
        }

        if (protocols != null && protocols.contains(Protocol.HTTPS) && !hasKeyStore)
        {
            throw new IllegalConfigurationException("Can't create port which requires SSL but has no key store configured.");
        }

        if (protocols != null && protocols.contains(Protocol.RMI) && usesSsl)
        {
            throw new IllegalConfigurationException("Can't create RMI Registry port which requires SSL.");
        }

        String authenticationProviderName = (String)merged.get(AUTHENTICATION_PROVIDER);
        if (authenticationProviderName != null)
        {
            Collection<AuthenticationProvider> providers = _broker.getAuthenticationProviders();
            AuthenticationProvider provider = null;
            for (AuthenticationProvider p : providers)
            {
                if (p.getName().equals(authenticationProviderName))
                {
                    provider = p;
                    break;
                }
            }

            if (provider == null)
            {
                throw new IllegalConfigurationException("Cannot find authentication provider with name '"
                        + authenticationProviderName + "'");
            }
        }
        else
        {
            if (protocols != null && !protocols.contains(Protocol.RMI))
            {
                throw new IllegalConfigurationException("An authentication provider must be specified");
            }
        }

        super.changeAttributes(converted);
    }

    @Override
    protected void authoriseSetDesiredState(State currentState, State desiredState) throws AccessControlException
    {
        if(desiredState == State.DELETED)
        {
            if (!_broker.getSecurityManager().authoriseConfiguringBroker(getName(), Port.class, Operation.DELETE))
            {
                throw new AccessControlException("Deletion of port is denied");
            }
        }
    }

    @Override
    protected void authoriseSetAttribute(String name, Object expected, Object desired) throws AccessControlException
    {
        if (!_broker.getSecurityManager().authoriseConfiguringBroker(getName(), Port.class, Operation.UPDATE))
        {
            throw new AccessControlException("Setting of port attributes is denied");
        }
    }

    @Override
    protected void authoriseSetAttributes(Map<String, Object> attributes) throws AccessControlException
    {
        if (!_broker.getSecurityManager().authoriseConfiguringBroker(getName(), Port.class, Operation.UPDATE))
        {
            throw new AccessControlException("Setting of port attributes is denied");
        }
    }

    @Override
    public KeyStore getKeyStore()
    {
        String keyStoreName = (String)getAttribute(Port.KEY_STORE);
        KeyStore keyStore = _broker.findKeyStoreByName(keyStoreName);

        if (keyStoreName != null && keyStore == null)
        {
            throw new IllegalConfigurationException("Can't find key store with name '" + keyStoreName + "' for port " + getName());
        }

        return keyStore;
    }

    @Override
    public Collection<TrustStore> getTrustStores()
    {
        Set<String> trustStoreNames = (Set<String>) getAttribute(TRUST_STORES);
        boolean hasTrustStoreName = trustStoreNames != null && !trustStoreNames.isEmpty();

        final Collection<TrustStore> trustStores = new ArrayList<TrustStore>();
        if(hasTrustStoreName)
        {
            for (String name : trustStoreNames)
            {
                TrustStore trustStore = _broker.findTrustStoreByName(name);
                if (trustStore == null)
                {
                    throw new IllegalConfigurationException("Can't find trust store with name '" + name + "' for port " + getName());
                }

                trustStores.add(trustStore);
            }
        }

        return trustStores;
    }
}
