/**
 * 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.cxf.binding.corba;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;

import org.apache.cxf.binding.corba.runtime.CorbaDSIServant;
import org.apache.cxf.binding.corba.utils.CorbaBindingHelper;
import org.apache.cxf.binding.corba.utils.CorbaUtils;
import org.apache.cxf.binding.corba.utils.OrbConfig;
import org.apache.cxf.binding.corba.wsdl.AddressType;
import org.apache.cxf.binding.corba.wsdl.PolicyType;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.message.Message;
import org.apache.cxf.service.model.BindingInfo;
import org.apache.cxf.service.model.EndpointInfo;
import org.apache.cxf.transport.Conduit;
import org.apache.cxf.transport.MessageObserver;
import org.apache.cxf.transport.MultiplexDestination;
import org.apache.cxf.ws.addressing.AttributedURIType;
import org.apache.cxf.ws.addressing.EndpointReferenceType;
import org.apache.cxf.ws.addressing.EndpointReferenceUtils;
import org.omg.CORBA.ORB;
import org.omg.CORBA.Policy;
import org.omg.PortableServer.Current;
import org.omg.PortableServer.IdAssignmentPolicyValue;
import org.omg.PortableServer.IdUniquenessPolicyValue;
import org.omg.PortableServer.LifespanPolicyValue;
import org.omg.PortableServer.POA;
import org.omg.PortableServer.POAHelper;
import org.omg.PortableServer.POAManager;
import org.omg.PortableServer.POAPackage.ObjectNotActive;
import org.omg.PortableServer.RequestProcessingPolicyValue;
import org.omg.PortableServer.Servant;
import org.omg.PortableServer.ThreadPolicyValue;

public class CorbaDestination implements MultiplexDestination {

    private static final String IOR_SHARED_KEY = "ior:shared-key";
    private static final Logger LOG = LogUtils.getL7dLogger(CorbaDestination.class);

    private AddressType address;
    private EndpointReferenceType reference;
    private ORB orb;
    private BindingInfo binding;
    private EndpointInfo endpointInfo;
    private OrbConfig orbConfig;
    private MessageObserver incomingObserver;
    private CorbaTypeMap typeMap;
    private byte[] objectId;
    private POA bindingPOA;
    private String poaName;
    private String serviceId;
    private boolean isPersistent;
    private org.omg.CORBA.Object obj;

    public CorbaDestination(EndpointInfo ei, OrbConfig config) {
        this(ei, config, null);
    }

    public CorbaDestination(EndpointInfo ei, OrbConfig config, CorbaTypeMap tm) {
        address = ei.getExtensor(AddressType.class);
        binding = ei.getBinding();
        reference = new EndpointReferenceType();
        AttributedURIType addr = new AttributedURIType();
        addr.setValue(address.getLocation());
        reference.setAddress(addr);
        endpointInfo = ei;
        orbConfig = config;
        if (tm != null) {
            typeMap = tm;
        } else {
            typeMap = TypeMapCache.get(binding.getService());
        }
        PolicyType policy = ei.getExtensor(PolicyType.class);
        if (policy != null) {
            poaName = policy.getPoaname();
            isPersistent = policy.isPersistent();
            serviceId = policy.getServiceid();
        }
    }

    public OrbConfig getOrbConfig() {
        return orbConfig;
    }

    public EndpointReferenceType getAddress() {
        return reference;
    }

    public Conduit getBackChannel(Message inMessage)
        throws IOException {
        return new CorbaServerConduit(endpointInfo, reference, obj,
                                      orb, orbConfig, typeMap);
    }

    public BindingInfo getBindingInfo() {
        return binding;
    }

    public EndpointInfo getEndPointInfo() {
        return endpointInfo;
    }

    public CorbaTypeMap getCorbaTypeMap() {
        return typeMap;
    }

    public void shutdown() {
        deactivate();
        if (orb != null) {
            try {
                // Ask for the ORB to be destroyed.  If another destination is using it, we'll
                // simply decrement a use count, but not destroy the ORB so that we don't break the
                // other CorbaDestination.
                if (CorbaUtils.isIOR(getDestinationAddress())) {
                    CorbaBindingHelper.destroyORB(IOR_SHARED_KEY, orb);
                } else {
                    CorbaBindingHelper.destroyORB(getDestinationAddress(), orb);
                }
            } catch (Exception ex) {
                throw new CorbaBindingException(ex);
            }
            orb = null;
        }
    }

    public ORB getORB(List<String> orbArgs,
                      String location,
                      java.util.Properties props) {
        // See if an ORB has already been created for the given address. If so,
        // we'll simply use it
        // so that we don't try re-create another ORB on the same host and port.
        if (CorbaUtils.isIOR(location)) {
            location = IOR_SHARED_KEY;
        }
        orb = CorbaBindingHelper.getAddressSpecificORB(location, props, orbArgs);

        // Get the binding helper to remember that we need this ORB kept alive, even if another
        // destination tries to destroy it.
        CorbaBindingHelper.keepORBAlive(location);

        return orb;

    }

    protected ORB getOrb() {
        return orb;
    }

    protected AddressType getAddressType() {
        return address;
    }

    public synchronized void setMessageObserver(MessageObserver observer) {
        if (observer != incomingObserver) {
            MessageObserver old = incomingObserver;
            incomingObserver = observer;
            if (observer != null) {
                if (old == null) {
                    activate();
                }
            } else {
                if (old != null) {
                    deactivate();
                }
            }
        }
    }

    public void activate() {
        java.util.Properties props = new java.util.Properties();
        Properties configSpecifiedOrbProperties = orbConfig.getOrbProperties();
        props.putAll(configSpecifiedOrbProperties);
        if (orbConfig.getOrbClass() != null) {
            props.put("org.omg.CORBA.ORBClass", orbConfig.getOrbClass());
        }
        if (orbConfig.getOrbSingletonClass() != null) {
            props.put("org.omg.CORBA.ORBSingletonClass", orbConfig
                .getOrbSingletonClass());
        }

        String location = getDestinationAddress();

        if (!CorbaUtils.isValidURL(location)) {
            throw new CorbaBindingException(
                    "Invalid addressing specified for CORBA port location");
        }

        LOG.info("Service address retrieved: " + location);


        final URI addressURI;
        try {
            addressURI = new URI(location);
        } catch (java.net.URISyntaxException ex) {
            throw new CorbaBindingException(
                    "Unable to create ORB with address " + address);
        }


        List<String> orbArgs = new ArrayList<>(orbConfig.getOrbArgs());

        String scheme = addressURI.getScheme();
        // A corbaloc address gives us host and port information to use when
        // setting up the
        // endpoint for the ORB. Other types of references will just create ORBs
        // on the
        // host and port used when no preference has been specified.
        if (poaName != null) {
            poaName = poaName.replace('.', '_');
        }
        if ("corbaloc".equals(scheme)) {
            if (poaName == null) {
                poaName = getEndPointInfo().getName().getLocalPart().replace('.', '_');
            }
            setCorbaLocArgs(addressURI, orbArgs);
        } else if ("corbaname".equals(scheme)) {
            int idx = location.indexOf('#');
            if (idx != -1) {
                serviceId = location.substring(idx + 1);
            }
        }

        if (isPersistent) {
            if (poaName == null) {
                throw new CorbaBindingException(
                        "POA name missing for corba port "
                                + "with a persistent policy");
            }
        } else {
            poaName = CorbaUtils.getUniquePOAName(getEndPointInfo()
                    .getService().getName(), getEndPointInfo().getName()
                    .getLocalPart(), poaName).replace('.', '_');
        }

        orb = getORB(orbArgs, location, props);


        try {
            POA rootPOA = POAHelper.narrow(orb.resolve_initial_references("RootPOA"));
            POAManager poaManager = rootPOA.the_POAManager();
            try {
                bindingPOA = rootPOA.find_POA(poaName, false);
            } catch (org.omg.PortableServer.POAPackage.AdapterNonExistent ex) {
                // do nothing
            }

            // When using object references, we can run into a situation where
            // we are implementing
            // multiple instances of the same port type such that we would end
            // up using the same
            // poaname for each when persistance is used. Handle this case by
            // not throwing an
            // exception at this point during the activation, we should see an
            // exception if we try
            // an activate two objects with the same servant ID instead.
            if (bindingPOA != null && !isPersistent && serviceId == null) {
                throw new CorbaBindingException(
                        "Corba Port activation failed because the poa "
                                + poaName + " already exists");
            } else if (bindingPOA == null) {
                bindingPOA = createPOA(poaName, rootPOA, poaManager);
            }

            if (bindingPOA == null) {
                throw new CorbaBindingException("Unable to create CXF CORBA Binding POA");
            }

            CorbaDSIServant servant = new CorbaDSIServant();
            servant.init(orb, bindingPOA, this, incomingObserver, typeMap);
            if (serviceId != null) {
                objectId = serviceId.getBytes();
                try {
                    bindingPOA.activate_object_with_id(objectId, servant);
                } catch (org.omg.PortableServer.POAPackage.ObjectAlreadyActive ex) {
                    if (!isPersistent) {
                        throw new CorbaBindingException("Object "
                                                        + serviceId
                                                        + " already active for non-persistent poa");
                    }
                }
            } else {
                objectId = bindingPOA.activate_object(servant);
            }
            bindingPOA.set_servant(servant);
            obj = bindingPOA.id_to_reference(objectId);
            orbConfig.exportObjectReference(orb, obj, location, address);

            populateEpr(orb.object_to_string(obj));
            LOG.info("Object Reference: " + orb.object_to_string(obj));
            poaManager.activate();
        } catch (Exception ex) {
            throw new CorbaBindingException("Unable to activate CORBA servant", ex);
        }
    }

    private void populateEpr(String ior) {
        AttributedURIType addr = new AttributedURIType();
        addr.setValue(ior);
        reference.setAddress(addr);
    }

    public String getDestinationAddress() {
        // We should check the endpoint first for an address.  This allows object references
        // to use the address that is associated with their endpoint instead of the single
        // address for a particular port type that is listed in the wsdl.  Otherwise, for all
        // object references we want to create, we would need to add the address to the wsdl
        // file before running the application.
        String location = null;
        if (endpointInfo != null) {
            location = endpointInfo.getAddress();
        }

        if (location == null) {
            location = address.getLocation();
        }

        return location;
    }

    public MessageObserver getMessageObserver() {
        return incomingObserver;
    }

    public void deactivate() {
        if (orb != null) {
            if (bindingPOA == null) {
                throw new CorbaBindingException("Corba Port deactivation failed because the poa is null");
            }

            try {
                bindingPOA.deactivate_object(objectId);
            } catch (ObjectNotActive ona) {
                LOG.info("Caught ObjectNotActive exception: " + ona
                         + " during deactivate_object() call on POA: " + bindingPOA);
            } catch (Exception ex) {
                throw new CorbaBindingException("Unable to deactivate CORBA servant", ex);
            }
        }
    }

    private void setCorbaLocArgs(URI addressURI, List<String> orbArgs) {
        String schemeSpecificPart = addressURI.getSchemeSpecificPart();
        int keyIndex = schemeSpecificPart.indexOf('/');
        String corbaAddr = schemeSpecificPart.substring(0, keyIndex);
        String key = schemeSpecificPart.substring(keyIndex + 1);

        int index = corbaAddr.indexOf(':');
        String protocol = "iiop";
        if (index != 0) {
            protocol = corbaAddr.substring(0, index);
        }
        int oldIndex = index;
        index = corbaAddr.indexOf(':', oldIndex + 1);
        String host = corbaAddr.substring(oldIndex + 1, index);
        String port = corbaAddr.substring(index + 1);

        orbArgs.add("-ORB" + key + ":" + protocol + ":host");
        orbArgs.add(host);
        orbArgs.add("-ORB" + key + ":" + protocol + ":port");
        orbArgs.add(port);
        orbArgs.add("-ORBpoa:" + poaName + ":direct_persistent");
        orbArgs.add("true");
        orbArgs.add("-ORBpoa:" + poaName + ":well_known_address");
        orbArgs.add(key);
        isPersistent = true;
        serviceId = key;
    }

    protected POA createPOA(String name, POA parentPOA, POAManager poaManager) {
        List<Policy> policies = new ArrayList<>();
        policies.add(parentPOA
                .create_thread_policy(ThreadPolicyValue.ORB_CTRL_MODEL));

        if (isPersistent) {
            policies.add(parentPOA
                    .create_lifespan_policy(LifespanPolicyValue.PERSISTENT));
        } else {
            policies.add(parentPOA
                    .create_lifespan_policy(LifespanPolicyValue.TRANSIENT));
        }

        if (serviceId != null) {
            policies.add(parentPOA
                         .create_id_assignment_policy(IdAssignmentPolicyValue.USER_ID));

        }

        policies.add(parentPOA.create_id_uniqueness_policy(IdUniquenessPolicyValue.MULTIPLE_ID));
        RequestProcessingPolicyValue value = RequestProcessingPolicyValue.USE_DEFAULT_SERVANT;
        policies.add(parentPOA.create_request_processing_policy(value));

        orbConfig.addPOAPolicies(orb, name, parentPOA, poaManager, policies);

        Policy[] policyList = policies.toArray(new Policy[0]);

        try {
            return parentPOA.create_POA(name, poaManager, policyList);
        } catch (Exception ex) {
            throw new CorbaBindingException(
                    "Could not create POA during activation", ex);
        }
    }
    public EndpointReferenceType getAddressWithId(String id) {
        if (bindingPOA == null) {
            throw new CorbaBindingException(
                 "getAddressWithId failed because the poa is null");
        }
        final EndpointReferenceType ref;
        try {
            Servant servant = bindingPOA.id_to_servant(objectId);
            org.omg.CORBA.Object objRef
                = bindingPOA.create_reference_with_id(id.getBytes(),
                                               servant._all_interfaces(bindingPOA, objectId)[0]);
            AddressType addr = new AddressType();
            orbConfig.exportObjectReference(orb, objRef,
                                            address.getLocation(),
                                            addr);
            ref = EndpointReferenceUtils.getEndpointReference(addr.getLocation());
            EndpointInfo ei = getEndPointInfo();
            if (ei.getService() != null) {
                EndpointReferenceUtils.setServiceAndPortName(ref, ei.getService().getName(),
                                                             ei.getName().getLocalPart());
            }
        } catch (Exception e) {
            throw new CorbaBindingException("Failed to getAddressWithId, reason:" + e.toString(), e);
        }
        return ref;
    }

    public String getId(Map<String, Object> contextMap) {
        try {
            Current currentPoa = (Current) orb
                .resolve_initial_references("POACurrent");
            byte[] idBytes = currentPoa.get_object_id();
            return new String(idBytes);
        } catch (Exception e) {
            throw new CorbaBindingException("Unable to getId, current is unavailable, reason: "
                                             + e, e);
        }
    }

}
