// GPars - Groovy Parallel Systems
//
// Copyright © 2008-11  The original author or authors
//
// Licensed 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 groovyx.gpars.util;

import groovy.lang.Closure;
import groovyx.gpars.ReactorMessagingRunnable;
import groovyx.gpars.dataflow.DataflowVariable;
import groovyx.gpars.scheduler.Pool;
import org.codehaus.groovy.runtime.InvokerInvocationException;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;

/**
 * Handy methods build PA from different types
 *
 * @author Vaclav Pech
 *         Date: 24th Oct 2010
 */
@SuppressWarnings({"UtilityClass", "AbstractClassWithoutAbstractMethods", "AbstractClassNeverImplemented", "StaticMethodOnlyUsedInOneClass"})
public abstract class PAUtils {
    public static <T> Collection<T> createCollection(final Iterable<T> object) {
        final Collection<T> collection = new ArrayList<T>();
        for (final T item : object) {
            collection.add(item);
        }
        return collection;
    }

    public static <T> Collection<T> createCollection(final Iterator<T> iterator) {
        final Collection<T> collection = new ArrayList<T>();
        while (iterator.hasNext()) {
            collection.add(iterator.next());
        }
        return collection;
    }

    public static String[] createArray(final CharSequence value) {
        final String[] chars = new String[value.length()];
        for (int i = 0; i < value.length(); i++) {
            chars[i] = String.valueOf(value.charAt(i));
        }
        return chars;
    }

    public static <K, V> Map.Entry<K, V>[] createArray(final Map<K, V> map) {
        @SuppressWarnings({"unchecked"})
        final Map.Entry<K, V>[] result = new Map.Entry[map.size()];
        int i = 0;
        for (final Map.Entry<K, V> entry : map.entrySet()) {
            result[i] = entry;
            i++;
        }
        return result;
    }

    /**
     * If the passed-in closure expects two arguments, it is considered to be a map-iterative code and is then wrapped
     * with a single-argument closure, which unwraps the key:value pairs for the original closure.
     * If the supplied closure doesn't expect two arguments, it is returned unchanged.
     *
     * @param cl The closure to use for parallel methods
     * @return The original or an unwrapping closure
     */
    public static <T> Closure<T> buildClosureForMaps(final Closure<T> cl) {
        if (cl.getMaximumNumberOfParameters() == 2) return new Closure<T>(cl.getOwner()) {
            private static final long serialVersionUID = -7502769124461342939L;

            @Override
            public T call(final Object arguments) {
                @SuppressWarnings({"unchecked"})
                final Map.Entry<Object, Object> entry = (Map.Entry<Object, Object>) arguments;
                return cl.call(entry.getKey(), entry.getValue());
            }

            @Override
            public T call(final Object[] args) {
                return this.call(args[0]);
            }
        };
        return cl;
    }

    /**
     * If the passed-in closure expects three arguments, it is considered to be a map-iterative_with_index code and is then wrapped
     * with a two-argument closure, which unwraps the key:value pairs for the original closure.
     * If the supplied closure doesn't expect three arguments, it is returned unchanged.
     *
     * @param cl The closure to use for parallel methods
     * @return The original or an unwrapping closure
     */
    public static <T> Closure<T> buildClosureForMapsWithIndex(final Closure<T> cl) {
        if (cl.getMaximumNumberOfParameters() == 3) return new Closure<T>(cl.getOwner()) {
            private static final long serialVersionUID = 4777456744250574403L;

            @SuppressWarnings({"rawtypes", "RawUseOfParameterizedType"})
            @Override
            public Class[] getParameterTypes() {
                return new Class[]{Map.Entry.class, Integer.class};
            }

            @Override
            public int getMaximumNumberOfParameters() {
                return 2;
            }

            @Override
            public T call(final Object[] args) {
                @SuppressWarnings({"unchecked"})
                final Map.Entry<Object, Object> entry = (Map.Entry<Object, Object>) args[0];
                final Integer index = (Integer) args[1];
                return cl.call(entry.getKey(), entry.getValue(), index);
            }
        };

        return cl;
    }

    /**
     * Builds a resulting map out of an map entry collection
     *
     * @param result The collection containing map entries
     * @return A corresponding map instance
     */
    public static <K, V> Map<K, V> buildResultMap(final Collection<Map.Entry<K, V>> result) {
        final Map<K, V> map = new HashMap<K, V>(result.size());
        for (final Map.Entry<K, V> item : result) {
            map.put(item.getKey(), item.getValue());
        }
        return map;
    }


    /**
     * Builds a comparator depending on the number of arguments accepted by the supplied closure.
     *
     * @param handler The one or two argument closure to build a comparator on
     * @return A new Comparator to use
     */
    public static Comparator<Object> createComparator(final Closure<Object> handler) {
        if (handler.getMaximumNumberOfParameters() == 2) return new Comparator<Object>() {
            @Override
            public int compare(final Object o1, final Object o2) {
                return (Integer) handler.call(o1, o2);
            }
        };
        else return new Comparator<Object>() {
            @SuppressWarnings({"unchecked"})
            @Override
            public int compare(final Object o1, final Object o2) {
                return ((Comparable<Object>) handler.call(o1)).compareTo(handler.call(o2));
            }
        };
    }

    /**
     * Creates a closure that will insert elements in the appropriate group
     *
     * @param cl  The distinction closure
     * @param map The map of groups to contribute to
     * @return null
     */
    public static <K, T> Closure<Object> createGroupByClosure(final Closure<K> cl, final ConcurrentMap<K, List<T>> map) {
        return new Closure<Object>(cl.getOwner(), cl.getDelegate()) {
            private static final long serialVersionUID = 5495474569312257163L;

            @Override
            @SuppressWarnings({"unchecked"})
            public Object call(final Object arguments) {
                final K result = cl.call(arguments);
                final List<T> localList = new ArrayList<T>();
                localList.add((T) arguments);
                final List<T> myList = Collections.synchronizedList(localList);
                final Collection<T> list = map.putIfAbsent(result, myList);
                if (list != null) list.add((T) arguments);
                return null;
            }
        };
    }

    /**
     * Performs a single step in the evaluation of parameters passed into an asynchronous function
     *
     * @param pool             The thread pool to use
     * @param args             The list of original arguments
     * @param current          The index of the current argument to evaluate
     * @param soFarArgs        A list of arguments evaluated so far
     * @param result           The DFV expecting the function result to be bound to once calculated
     * @param original         The original non-asynchronous function to invoke once all arguments are available
     * @param pooledThreadFlag Indicates, whether we now run in a pooled thread so we don't have to schedule the original function invocation, once all arguments have been bound
     */
    @SuppressWarnings({"unchecked"})
    public static <T> void evaluateArguments(final Pool pool, final Object[] args, final int current, final List<Object> soFarArgs,
                                             final DataflowVariable<Object> result, final Closure<T> original, final boolean pooledThreadFlag) {
        if (current == args.length) {
            if (pooledThreadFlag) {
                try {
                    final Object call = original.call(soFarArgs.toArray(new Object[soFarArgs.size()]));
                    result.leftShift(call);
                } catch (InvokerInvocationException e) {
                    result.bind(e.getCause());
                } catch (Exception all) {
                    result.bind(all);
                } catch (Error error) {
                    result.bind(error);
                    throw error;
                }
            } else {
                pool.execute(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            result.leftShift(original.call(soFarArgs.toArray(new Object[soFarArgs.size()])));
                        } catch (InvokerInvocationException e) {
                            result.bind(e.getCause());
                        } catch (Exception all) {
                            result.bind(all);
                        } catch (Error error) {
                            result.bind(error);
                            throw error;
                        }
                    }
                });
            }
        } else {
            final Object currentArgument = args[current];
            if (currentArgument instanceof DataflowVariable) {
                final DataflowVariable<Object> variable = (DataflowVariable<Object>) currentArgument;
                if (variable.isBound()) {
                    Object currentValue = null;
                    try {
                        currentValue = variable.getVal();
                    } catch (InterruptedException e) {
                        throw new RuntimeException("Interrupted while processing arguments", e);
                    }
                    if (currentValue instanceof Throwable) result.leftShift(currentValue);
                    else {
                        soFarArgs.add(currentValue);
                        evaluateArguments(pool, args, current + 1, soFarArgs, result, original, pooledThreadFlag);
                    }
                } else {
                    variable.whenBound(pool, new ReactorMessagingRunnable() {
                        @Override
                        protected Object doRun(final Object argument) {
                            if (argument instanceof Throwable) result.leftShift(argument);
                            else {
                                soFarArgs.add(argument);
                                evaluateArguments(pool, args, current + 1, soFarArgs, result, original, true);
                            }
                            return null;
                        }
                    });
                }
            } else {
                soFarArgs.add(currentArgument);
                evaluateArguments(pool, args, current + 1, soFarArgs, result, original, pooledThreadFlag);
            }
        }
    }
}
