/*
 * Copyright (c) 2010, Rickard Öberg. All Rights Reserved.
 *
 * 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 org.qi4j.functional;

import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;

/**
 * Utility functions. Combine these with methods in Iterables, for example. See FunctionsTest for usages.
 */
public final class Functions
{
    public static <A, B, C> Function2<Function<? super B, C>, Function<A, B>, Function<A, C>> compose()
    {
        return new Function2<Function<? super B, C>, Function<A, B>, Function<A, C>>()
        {
            @Override
            public Function<A, C> map( Function<? super B, C> bcFunction, Function<A, B> abFunction )
            {
                return compose( bcFunction, abFunction );
            }
        };
    }

    /**
     * compose(F1(M,T),F2(F,M)) = F1(F2(F)) -&gt; T
     *
     * @param outer The outer/encapsulating function
     * @param inner The inner/encapsulated function
     *
     * @return A function that is a composition of an outer and inner function.
     */
    public static <FROM, MIDDLE, TO> Function<FROM, TO> compose( final Function<? super MIDDLE, TO> outer,
                                                                 final Function<FROM, MIDDLE> inner
    )
    {
        return new Function<FROM, TO>()
        {
            @Override
            public TO map( FROM from )
            {
                return outer.map( inner.map( from ) );
            }
        };
    }

    public static <TO, FROM extends TO> Function<FROM, TO> identity()
    {
        return new Function<FROM, TO>()
        {
            @Override
            public TO map( FROM from )
            {
                return from;
            }
        };
    }

    public static <FROM, TO> Function<FROM, TO> fromMap( final Map<FROM, TO> map )
    {
        return new Function<FROM, TO>()
        {
            @Override
            public TO map( FROM from )
            {
                return map.get( from );
            }
        };
    }

    public static <T> Function<T, T> withDefault( final T defaultValue )
    {
        return new Function<T, T>()
        {
            @Override
            public T map( T from )
            {
                if( from == null )
                {
                    return defaultValue;
                }
                else
                {
                    return from;
                }
            }
        };
    }

    public static Function<Number, Long> longSum()
    {
        return new Function<Number, Long>()
        {
            long sum;

            @Override
            public Long map( Number number )
            {
                sum += number.longValue();
                return sum;
            }
        };
    }

    public static Function<Number, Integer> intSum()
    {
        return new Function<Number, Integer>()
        {
            int sum;

            @Override
            public Integer map( Number number )
            {
                sum += number.intValue();
                return sum;
            }
        };
    }

    /**
     * Count the number of items in an iterable that matches a given specification.
     *
     * Sample usage: last( map( count( in( "X" ) ), iterable( "X","Y","X","X","Y" ) ) )
     * Returns: 3
     *
     * @param specification The items that adhere to the Specification is counted.
     * @param <T> The type of the items.
     *
     * @return A Function that can count items adhering to a Specification.
     */
    public static <T> Function<T, Integer> count( final Specification<T> specification )
    {
        return new Function<T, Integer>()
        {
            int count;

            @Override
            public Integer map( T item )
            {
                if( specification.satisfiedBy( item ) )
                {
                    count++;
                }

                return count;
            }
        };
    }

    /**
     * Find out the index of an item matching a given specification in an iterable.
     * Returns -1 if it is not found.
     *
     * @param specification The Specification that specifies what to look for.
     * @param <T> The type of the items.
     *
     * @return A Function that will provide the 'index' where the Specifcation is fulfilled. The Function will
     * return -1 if the current item doesn't fulfill the Specification.
     */
    public static <T> Function<T, Integer> indexOf( final Specification<T> specification )
    {
        return new Function<T, Integer>()
        {
            int index = -1;
            int current = 0;

            @Override
            public Integer map( T item )
            {
                if( index == -1 && specification.satisfiedBy( item ) )
                {
                    index = current;
                }

                current++;

                return index;
            }
        };
    }

    /**
     * Find out the index of an item in an iterable.
     *
     * @param item The item to look for.
     * @param iterable The Iterable to search.
     * @param <T> The type of the items.
     *
     * @return The index in the Iterable where the item is located.
     */
    @SuppressWarnings( "unchecked" )
    public static <T> int indexOf( T item, Iterable<T> iterable )
    {
        return Iterables.first( Iterables.filter( Specifications.not( Specifications.in( -1 ) ),
                                                  Iterables.map( indexOf( Specifications.in( item ) ), iterable ) ) );
    }

    /**
     * Only apply given function on objects that satisfies the given specification.
     *
     * @param specification A Specification that specifies what should be included in the filtered result.
     * @param function The function to be applied to items that fulfills the Specification
     * @param <T> The type of the items.
     *
     * @return A Function that performs the filter operation when applied to Iterables.
     */
    public static <T> Function<T, T> filteredMap( final Specification<T> specification, final Function<T, T> function )
    {
        return new Function<T, T>()
        {
            @Override
            public T map( T from )
            {
                return specification.satisfiedBy( from ) ? function.map( from ) : from;
            }
        };
    }

    /**
     * Creates a comparator that takes a function as input. The returned comparator will use the
     * function once for each item in the list to be sorted by Collections.sort.
     *
     * This should be used if the function to generate the sort key from an object is expensive, so
     * that it is not done many times for each item in a list.
     *
     * @param comparableFunction The Function that the Comparator will delegate to.
     * @param <T>                The generic type to be used.
     *
     * @return A comparator that uses a Function for the compare operation.
     */
    @SuppressWarnings( "raw" )
    public static <T> Comparator<T> comparator( final Function<T, Comparable> comparableFunction )
    {
        return new Comparator<T>()
        {
            Map<T, Comparable> compareKeys = new HashMap<>();

            @Override
            @SuppressWarnings( "unchecked" )
            public int compare( T o1, T o2 )
            {
                Comparable key1 = compareKeys.get( o1 );
                if( key1 == null )
                {
                    key1 = comparableFunction.map( o1 );
                    compareKeys.put( o1, key1 );
                }

                Comparable key2 = compareKeys.get( o2 );
                if( key2 == null )
                {
                    key2 = comparableFunction.map( o2 );
                    compareKeys.put( o2, key2 );
                }

                return key1.compareTo( key2 );
            }
        };
    }

    private Functions()
    {
    }
}
