/*
 * 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.ignite.internal.processors.platform.services;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteException;
import org.apache.ignite.IgniteServices;
import org.apache.ignite.internal.binary.BinaryRawReaderEx;
import org.apache.ignite.internal.binary.BinaryRawWriterEx;
import org.apache.ignite.internal.processors.platform.PlatformAbstractTarget;
import org.apache.ignite.internal.processors.platform.PlatformContext;
import org.apache.ignite.internal.processors.platform.PlatformTarget;
import org.apache.ignite.internal.processors.platform.cluster.PlatformClusterNodeFilterImpl;
import org.apache.ignite.internal.processors.platform.dotnet.PlatformDotNetService;
import org.apache.ignite.internal.processors.platform.dotnet.PlatformDotNetServiceImpl;
import org.apache.ignite.internal.processors.platform.utils.PlatformFutureUtils;
import org.apache.ignite.internal.processors.platform.utils.PlatformUtils;
import org.apache.ignite.internal.processors.platform.utils.PlatformWriterBiClosure;
import org.apache.ignite.internal.processors.platform.utils.PlatformWriterClosure;
import org.apache.ignite.internal.processors.service.GridServiceProxy;
import org.apache.ignite.internal.util.IgniteUtils;
import org.apache.ignite.internal.util.typedef.T3;
import org.apache.ignite.lang.IgniteFuture;
import org.apache.ignite.lang.IgnitePredicate;
import org.apache.ignite.services.Service;
import org.apache.ignite.services.ServiceConfiguration;
import org.apache.ignite.services.ServiceDeploymentException;
import org.apache.ignite.services.ServiceDescriptor;
import org.jetbrains.annotations.NotNull;

/**
 * Interop services.
 */
public class PlatformServices extends PlatformAbstractTarget {
    /** */
    private static final int OP_DOTNET_DEPLOY = 1;

    /** */
    private static final int OP_DOTNET_DEPLOY_MULTIPLE = 2;

    /** */
    private static final int OP_DOTNET_SERVICES = 3;

    /** */
    private static final int OP_INVOKE = 4;

    /** */
    private static final int OP_DESCRIPTORS = 5;

    /** */
    private static final int OP_WITH_ASYNC = 6;

    /** */
    private static final int OP_WITH_SERVER_KEEP_BINARY = 7;

    /** */
    private static final int OP_SERVICE_PROXY = 8;

    /** */
    private static final int OP_CANCEL = 9;

    /** */
    private static final int OP_CANCEL_ALL = 10;

    /** */
    private static final int OP_DOTNET_DEPLOY_ASYNC = 11;

    /** */
    private static final int OP_DOTNET_DEPLOY_MULTIPLE_ASYNC = 12;

    /** */
    private static final int OP_CANCEL_ASYNC = 13;

    /** */
    private static final int OP_CANCEL_ALL_ASYNC = 14;

    /** */
    private static final int OP_DOTNET_DEPLOY_ALL = 15;

    /** */
    private static final int OP_DOTNET_DEPLOY_ALL_ASYNC = 16;

    /** */
    private static final byte PLATFORM_JAVA = 0;

    /** */
    private static final byte PLATFORM_DOTNET = 1;

    /** */
    private static final CopyOnWriteConcurrentMap<T3<Class, String, Integer>, Method> SVC_METHODS
        = new CopyOnWriteConcurrentMap<>();

    /** Future result writer. */
    private static final PlatformFutureUtils.Writer RESULT_WRITER = new ServiceDeploymentResultWriter();

    /** */
    private final IgniteServices services;

    /** Server keep binary flag. */
    private final boolean srvKeepBinary;

    /**
     * Ctor.
     *
     * @param platformCtx Context.
     * @param services Services facade.
     * @param srvKeepBinary Server keep binary flag.
     */
    public PlatformServices(PlatformContext platformCtx, IgniteServices services, boolean srvKeepBinary) {
        super(platformCtx);

        assert services != null;

        this.services = services;
        this.srvKeepBinary = srvKeepBinary;
    }

    /**
     * Finds a service descriptor by name.
     *
     * @param name Service name.
     * @return Descriptor or null.
     */
    private ServiceDescriptor findDescriptor(String name) {
        for (ServiceDescriptor d : services.serviceDescriptors())
            if (d.name().equals(name))
                return d;

        return null;
    }

    /** {@inheritDoc} */
    @Override public long processInStreamOutLong(int type, BinaryRawReaderEx reader)
        throws IgniteCheckedException {
        switch (type) {
            case OP_DOTNET_DEPLOY_ASYNC: {
                readAndListenFuture(reader, dotnetDeployAsync(reader, services), RESULT_WRITER);

                return TRUE;
            }

            case OP_DOTNET_DEPLOY_MULTIPLE_ASYNC: {
                readAndListenFuture(reader, dotnetDeployMultipleAsync(reader), RESULT_WRITER);

                return TRUE;
            }

            case OP_CANCEL: {
                services.cancel(reader.readString());

                return TRUE;
            }

            case OP_CANCEL_ASYNC: {
                readAndListenFuture(reader, services.cancelAsync(reader.readString()));

                return TRUE;
            }

            case OP_CANCEL_ALL_ASYNC: {
                readAndListenFuture(reader, services.cancelAllAsync());

                return TRUE;
            }

            case OP_DOTNET_DEPLOY_ALL_ASYNC: {
                readAndListenFuture(reader, dotnetDeployAllAsync(reader, services), RESULT_WRITER);

                return TRUE;
            }

            default:
                return super.processInStreamOutLong(type, reader);
        }
    }

    /** {@inheritDoc} */
    @Override public void processInStreamOutStream(int type, BinaryRawReaderEx reader, BinaryRawWriterEx writer)
        throws IgniteCheckedException {
        switch (type) {
            case OP_DOTNET_SERVICES: {
                Collection<Service> svcs = services.services(reader.readString());

                PlatformUtils.writeNullableCollection(writer, svcs,
                    new PlatformWriterClosure<Service>() {
                        @Override public void write(BinaryRawWriterEx writer, Service svc) {
                            writer.writeLong(((PlatformService) svc).pointer());
                        }
                    },
                    new IgnitePredicate<Service>() {
                        @Override public boolean apply(Service svc) {
                            return svc instanceof PlatformDotNetService;
                        }
                    }
                );

                return;
            }

            case OP_DOTNET_DEPLOY: {
                try {
                    dotnetDeploy(reader, services);
                    writeDeploymentResult(writer, null);
                }
                catch (Exception e) {
                    writeDeploymentResult(writer, e);
                }

                return;
            }

            case OP_DOTNET_DEPLOY_MULTIPLE: {
                try {
                    dotnetDeployMultiple(reader);

                    writeDeploymentResult(writer, null);
                }
                catch (Exception e) {
                    writeDeploymentResult(writer, e);
                }

                return;
            }

            case OP_DOTNET_DEPLOY_ALL: {
                try {
                    dotnetDeployAll(reader, services);

                    writeDeploymentResult(writer, null);
                }
                catch (Exception e) {
                    writeDeploymentResult(writer, e);
                }

                return;
            }

            default:
                super.processInStreamOutStream(type, reader, writer);
        }
    }

    /** {@inheritDoc} */
    @Override public PlatformTarget processInObjectStreamOutObjectStream(int type, PlatformTarget arg,
        BinaryRawReaderEx reader, BinaryRawWriterEx writer) throws IgniteCheckedException {
        switch (type) {
            case OP_INVOKE: {
                assert arg != null;
                assert arg instanceof ServiceProxyHolder;

                String mthdName = reader.readString();

                Object[] args;

                if (reader.readBoolean()) {
                    args = new Object[reader.readInt()];

                    for (int i = 0; i < args.length; i++)
                        args[i] = reader.readObjectDetached();
                }
                else
                    args = null;

                try {
                    Object result = ((ServiceProxyHolder)arg).invoke(mthdName, srvKeepBinary, args);

                    PlatformUtils.writeInvocationResult(writer, result, null);
                }
                catch (Exception e) {
                    PlatformUtils.writeInvocationResult(writer, null, e);
                }

                return null;
            }
        }

        return super.processInObjectStreamOutObjectStream(type, arg, reader, writer);
    }

    /** {@inheritDoc} */
    @Override public void processOutStream(int type, BinaryRawWriterEx writer) throws IgniteCheckedException {
        switch (type) {
            case OP_DESCRIPTORS: {
                Collection<ServiceDescriptor> descs = services.serviceDescriptors();

                PlatformUtils.writeCollection(writer, descs, new PlatformWriterClosure<ServiceDescriptor>() {
                    @Override public void write(BinaryRawWriterEx writer, ServiceDescriptor d) {
                        writer.writeString(d.name());
                        writer.writeString(d.cacheName());
                        writer.writeInt(d.maxPerNodeCount());
                        writer.writeInt(d.totalCount());
                        writer.writeUuid(d.originNodeId());
                        writer.writeObject(d.affinityKey());

                        // Write platform. There are only 2 platforms now.
                        byte platform = d.serviceClass().equals(PlatformDotNetServiceImpl.class)
                            ? PLATFORM_DOTNET : PLATFORM_JAVA;
                        writer.writeByte(platform);

                        Map<UUID, Integer> top = d.topologySnapshot();

                        PlatformUtils.writeMap(writer, top, new PlatformWriterBiClosure<UUID, Integer>() {
                            @Override public void write(BinaryRawWriterEx writer, UUID key, Integer val) {
                                writer.writeUuid(key);
                                writer.writeInt(val);
                            }
                        });
                    }
                });

                return;
            }

            default:
                super.processOutStream(type, writer);
        }
    }

    /** {@inheritDoc} */
    @Override public PlatformTarget processOutObject(int type) throws IgniteCheckedException {
        switch (type) {
            case OP_WITH_ASYNC:
                if (services.isAsync())
                    return this;

                return new PlatformServices(platformCtx, services.withAsync(), srvKeepBinary);

            case OP_WITH_SERVER_KEEP_BINARY:
                return srvKeepBinary ? this : new PlatformServices(platformCtx, services, true);
        }

        return super.processOutObject(type);
    }

    /** {@inheritDoc} */
    @Override public long processInLongOutLong(int type, long val) throws IgniteCheckedException {
        switch (type) {
            case OP_CANCEL_ALL:
                services.cancelAll();

                return TRUE;
        }

        return super.processInLongOutLong(type, val);
    }

    /** {@inheritDoc} */
    @Override public PlatformTarget processInStreamOutObject(int type, BinaryRawReaderEx reader) throws IgniteCheckedException {
        switch (type) {
            case OP_SERVICE_PROXY: {
                String name = reader.readString();
                boolean sticky = reader.readBoolean();

                ServiceDescriptor d = findDescriptor(name);

                if (d == null)
                    throw new IgniteException("Failed to find deployed service: " + name);

                Object proxy = PlatformService.class.isAssignableFrom(d.serviceClass())
                    ? services.serviceProxy(name, PlatformService.class, sticky)
                    : new GridServiceProxy<>(services.clusterGroup(), name, Service.class, sticky, 0,
                        platformCtx.kernalContext());

                return new ServiceProxyHolder(proxy, d.serviceClass(), platformContext());
            }
        }
        return super.processInStreamOutObject(type, reader);
    }

    /**
     * Deploys multiple dotnet services.
     *
     * @param reader Binary reader.
     */
    private void dotnetDeployMultiple(BinaryRawReaderEx reader) {
        String name = reader.readString();
        Object svc = reader.readObjectDetached();
        int totalCnt = reader.readInt();
        int maxPerNodeCnt = reader.readInt();

        services.deployMultiple(name, new PlatformDotNetServiceImpl(svc, platformCtx, srvKeepBinary),
                totalCnt, maxPerNodeCnt);
    }

    /**
     * Asynchronously deploys multiple dotnet services.
     *
     * @param reader Binary reader.
     * @return Future of the operation.
     */
    private IgniteFuture<Void> dotnetDeployMultipleAsync(BinaryRawReaderEx reader) {
        String name = reader.readString();
        Object svc = reader.readObjectDetached();
        int totalCnt = reader.readInt();
        int maxPerNodeCnt = reader.readInt();

        return services.deployMultipleAsync(name, new PlatformDotNetServiceImpl(svc, platformCtx, srvKeepBinary),
            totalCnt, maxPerNodeCnt);
    }

    /**
     * Deploys dotnet service.
     *
     * @param reader Binary reader.
     * @param services Services.
     */
    private void dotnetDeploy(BinaryRawReaderEx reader, IgniteServices services) {
        ServiceConfiguration cfg = dotnetConfiguration(reader);

        services.deploy(cfg);
    }

    /**
     * Deploys dotnet service asynchronously.
     *
     * @param reader Binary reader.
     * @param services Services.
     * @return Future of the operation.
     */
    private IgniteFuture<Void> dotnetDeployAsync(BinaryRawReaderEx reader, IgniteServices services) {
        ServiceConfiguration cfg = dotnetConfiguration(reader);

        return services.deployAsync(cfg);
    }

    /**
     * Deploys a collection of dotnet services.
     *
     * @param reader Binary reader.
     * @param services Services.
     */
    private void dotnetDeployAll(BinaryRawReaderEx reader, IgniteServices services) {
        Collection<ServiceConfiguration> cfgs = dotnetConfigurations(reader);

        services.deployAll(cfgs);
    }

    /**
     * Deploys a collection of dotnet services asynchronously.
     *
     * @param reader Binary reader.
     * @param services Services.
     * @return Future of the operation.
     */
    private IgniteFuture<Void> dotnetDeployAllAsync(BinaryRawReaderEx reader, IgniteServices services) {
        Collection<ServiceConfiguration> cfgs = dotnetConfigurations(reader);

        return services.deployAllAsync(cfgs);
    }

    /**
     * Read the dotnet service configuration.
     *
     * @param reader Binary reader,
     * @return Service configuration.
     */
    @NotNull private ServiceConfiguration dotnetConfiguration(BinaryRawReaderEx reader) {
        ServiceConfiguration cfg = new ServiceConfiguration();

        cfg.setName(reader.readString());
        cfg.setService(new PlatformDotNetServiceImpl(reader.readObjectDetached(), platformCtx, srvKeepBinary));
        cfg.setTotalCount(reader.readInt());
        cfg.setMaxPerNodeCount(reader.readInt());
        cfg.setCacheName(reader.readString());
        cfg.setAffinityKey(reader.readObjectDetached());

        Object filter = reader.readObjectDetached();

        if (filter != null)
            cfg.setNodeFilter(platformCtx.createClusterNodeFilter(filter));

        return cfg;
    }

    /**
     * Reads the collection of dotnet service configurations.
     *
     * @param reader Binary reader,
     * @return Service configuration.
     */
    @NotNull private Collection<ServiceConfiguration> dotnetConfigurations(BinaryRawReaderEx reader) {
        int numServices = reader.readInt();

        List<ServiceConfiguration> cfgs = new ArrayList<>(numServices);

        for (int i = 0; i < numServices; i++) {
            cfgs.add(dotnetConfiguration(reader));
        }

        return cfgs;
    }

    /**
     * Finds a suitable method in a class.
     *
     * @param clazz Class.
     * @param mthdName Name.
     * @param args Args.
     * @return Method.
     * @throws NoSuchMethodException On error.
     */
    public static Method getMethod(Class<?> clazz, String mthdName, Object[] args) throws NoSuchMethodException {
        return ServiceProxyHolder.getMethod(clazz, mthdName, args);
    }

    /**
     * Proxy holder.
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private static class ServiceProxyHolder extends PlatformAbstractTarget {
        /** */
        private final Object proxy;

        /** */
        private final Class serviceClass;

        /** */
        private static final Map<Class<?>, Class<?>> PRIMITIVES_TO_WRAPPERS = new HashMap<>();

        /*
          Class initializer.
         */
        static {
            PRIMITIVES_TO_WRAPPERS.put(boolean.class, Boolean.class);
            PRIMITIVES_TO_WRAPPERS.put(byte.class, Byte.class);
            PRIMITIVES_TO_WRAPPERS.put(char.class, Character.class);
            PRIMITIVES_TO_WRAPPERS.put(double.class, Double.class);
            PRIMITIVES_TO_WRAPPERS.put(float.class, Float.class);
            PRIMITIVES_TO_WRAPPERS.put(int.class, Integer.class);
            PRIMITIVES_TO_WRAPPERS.put(long.class, Long.class);
            PRIMITIVES_TO_WRAPPERS.put(short.class, Short.class);
        }

        /**
         * Ctor.
         *
         * @param proxy Proxy object.
         * @param clazz Proxy class.
         * @param ctx Platform context.
         */
        private ServiceProxyHolder(Object proxy, Class clazz, PlatformContext ctx) {
            super(ctx);

            assert proxy != null;
            assert clazz != null;

            this.proxy = proxy;
            serviceClass = clazz;
        }

        /**
         * Invokes the proxy.
         *
         * @param mthdName Method name.
         * @param srvKeepBinary Binary flag.
         * @param args Args.
         * @return Invocation result.
         * @throws IgniteCheckedException On error.
         * @throws NoSuchMethodException On error.
         */
        public Object invoke(String mthdName, boolean srvKeepBinary, Object[] args)
            throws IgniteCheckedException, NoSuchMethodException {
            if (proxy instanceof PlatformService)
                return ((PlatformService)proxy).invokeMethod(mthdName, srvKeepBinary, args);
            else {
                assert proxy instanceof GridServiceProxy;

                // Deserialize arguments for Java service when not in binary mode
                if (!srvKeepBinary)
                    args = PlatformUtils.unwrapBinariesInArray(args);

                Method mtd = getMethod(serviceClass, mthdName, args);

                // Convert Object[] to T[] when required:
                // Ignite loses array item types when passing arguments through GridServiceProxy.
                for (int i = 0; i < args.length; i++) {
                    Object arg = args[i];

                    if (arg instanceof Object[]) {
                        Class<?> parameterType = mtd.getParameterTypes()[i];

                        if (parameterType.isArray() && parameterType != Object[].class) {
                            Object[] arr = (Object[])arg;
                            Object newArg = Array.newInstance(parameterType.getComponentType(), arr.length);

                            for (int j = 0; j < arr.length; j++)
                                Array.set(newArg, j, arr[j]);

                            args[i] = newArg;
                        }
                    }
                }

                try {
                    return ((GridServiceProxy)proxy).invokeMethod(mtd, args);
                }
                catch (Throwable t) {
                    throw IgniteUtils.cast(t);
                }
            }
        }

        /**
         * Finds a suitable method in a class
         *
         * @param clazz Class.
         * @param mthdName Name.
         * @param args Args.
         * @return Method.
         * @throws NoSuchMethodException On error.
         */
        private static Method getMethod(Class clazz, String mthdName, Object[] args) throws NoSuchMethodException {
            assert clazz != null;
            assert mthdName != null;
            assert args != null;

            T3<Class, String, Integer> cacheKey = new T3<>(clazz, mthdName, args.length);
            Method res = SVC_METHODS.get(cacheKey);

            if (res != null)
                return res;

            Method[] allMethods = clazz.getMethods();

            List<Method> methods = new ArrayList<>(allMethods.length);

            // Filter by name and param count
            for (Method m : allMethods)
                if (m.getName().equals(mthdName) && m.getParameterTypes().length == args.length)
                    methods.add(m);

            if (methods.size() == 1) {
                res = methods.get(0);

                // Update cache only when there is a single method with a given name and arg count.
                SVC_METHODS.put(cacheKey, res);

                return res;
            }

            if (methods.isEmpty())
                throw new NoSuchMethodException("Could not find proxy method '" + mthdName + "' in class " + clazz);

            // Filter by param types
            for (int i = 0; i < methods.size(); i++)
                if (!areMethodArgsCompatible(methods.get(i).getParameterTypes(), args))
                    methods.remove(i--);

            if (methods.size() == 1)
                return methods.get(0);

            if (methods.isEmpty())
                throw new NoSuchMethodException("Could not find proxy method '" + mthdName + "' in class " + clazz);

            throw new NoSuchMethodException("Ambiguous proxy method '" + mthdName + "' in class " + clazz);
        }

        /**
         * Determines whether specified method arguments are compatible with given method parameter definitions.
         *
         * @param argTypes Method arg types.
         * @param args Method args.
         * @return Whether specified args are compatible with argTypes.
         */
        private static boolean areMethodArgsCompatible(Class[] argTypes, Object[] args) {
            for (int i = 0; i < args.length; i++) {
                // arg is always of a primitive wrapper class, and argTypes can have actual primitive
                Object arg = args[i];
                Class argType = wrap(argTypes[i]);

                if (arg != null && !argType.isAssignableFrom(arg.getClass()))
                    return false;
            }

            return true;
        }

        /**
         * Gets corresponding wrapper for a primitive type.
         * @param c Class to convert.
         *
         * @return Primitive wrapper, or the same class.
         */
        private static Class wrap(Class c) {
            return c.isPrimitive() ? PRIMITIVES_TO_WRAPPERS.get(c) : c;
        }
    }

    /**
     * Concurrent map.
     */
    private static class CopyOnWriteConcurrentMap<K, V> {
        /** */
        private volatile Map<K, V> map = new HashMap<>();

        /**
         * Gets a value.
         *
         * @param key Key.
         * @return Value.
         */
        public V get(K key) {
            return map.get(key);
        }

        /**
         * Puts a value.
         *
         * @param key Key.
         * @param val Value.
         */
        public void put(K key, V val) {
            synchronized (this) {
                if (map.containsKey(key))
                    return;

                Map<K, V> map0 = new HashMap<>(map);

                map0.put(key, val);

                map = map0;
            }
        }
    }

    /**
     * Writes an EventBase.
     */
    private static class ServiceDeploymentResultWriter implements PlatformFutureUtils.Writer {
        /** <inheritDoc /> */
        @Override public void write(BinaryRawWriterEx writer, Object obj, Throwable err) {
            writeDeploymentResult(writer, err);
        }

        /** <inheritDoc /> */
        @Override public boolean canWrite(Object obj, Throwable err) {
            return true;
        }
    }

    /**
     * Writes a service deployment result for dotnet code.
     *
     * @param writer Writer.
     * @param err Error.
      */
    private static void writeDeploymentResult(BinaryRawWriterEx writer, Throwable err) {
        PlatformUtils.writeInvocationResult(writer, null, err);

        Collection<ServiceConfiguration> failedCfgs = null;

        if (err instanceof ServiceDeploymentException)
            failedCfgs = ((ServiceDeploymentException)err).getFailedConfigurations();

        // write a collection of failed service configurations
        PlatformUtils.writeNullableCollection(writer, failedCfgs, new PlatformWriterClosure<ServiceConfiguration>() {
            @Override public void write(BinaryRawWriterEx writer, ServiceConfiguration svcCfg) {
                writeFailedConfiguration(writer, svcCfg);
            }
        });
    }

    /**
     * Writes a failed service configuration for dotnet code.
     *
     * @param w Writer
     * @param svcCfg Service configuration
     */
    private static void writeFailedConfiguration(BinaryRawWriterEx w, ServiceConfiguration svcCfg) {
        Object dotnetSvc = null;
        Object dotnetFilter = null;
        w.writeString(svcCfg.getName());
        if (svcCfg.getService() instanceof PlatformDotNetServiceImpl)
            dotnetSvc = ((PlatformDotNetServiceImpl)svcCfg.getService()).getInternalService();

        w.writeObjectDetached(dotnetSvc);
        w.writeInt(svcCfg.getTotalCount());
        w.writeInt(svcCfg.getMaxPerNodeCount());
        w.writeString(svcCfg.getCacheName());
        w.writeObjectDetached(svcCfg.getAffinityKey());

        if (svcCfg.getNodeFilter() instanceof PlatformClusterNodeFilterImpl)
            dotnetFilter = ((PlatformClusterNodeFilterImpl)svcCfg.getNodeFilter()).getInternalPredicate();
        w.writeObjectDetached(dotnetFilter);
    }
}
