/*
 *  @author Ranides Atterwim <ranides@gmail.com>
 *  @copyright Ranides Atterwim
 *  @license WTFPL
 *  @url http://ranides.net/projects/assira
 */

package net.ranides.assira.collection;

import java.util.*;
import net.ranides.assira.generic.Function;

/**
 * Operacje na listach.
 * @author ranides
 */
@SuppressWarnings({"PMD.ShortVar"})
public final class ListUtils {

    private ListUtils() {
        // utility class
    }
    
    /**
     * Pobiera index-ty element z listy, lub zwraca wartość domyślną, jeśli
     * lista ma mniejszy rozmiar niż konieczne
     *
     * <div class="message-note">{@code null} safe method</div>
     * @param <T>
     * @param values
     * @param index
     * @param ddefault
     * @return
     */
    public static <T> T get(List<T> values, int index, T ddefault) {
        if(null == values) { return ddefault; }
        return index>=0 && index < values.size() ? values.get(index) : ddefault;
    }


    /**
     * Pobiera index-ty element z listy, lub zwraca null, jeśli
     * lista ma mniejszy rozmiar niż konieczne
     *
     * <div class="message-note">{@code null} safe method</div>
     * @param <T>
     * @param values
     * @param index
     * @return
     */
    public static <T> T get(List<T> values, int index) {
        return get(values, index, null);
    }


    /**
     * Wyszukuje i zwraca pierwszy obiekt o klasie T, lub null, jeśli takiej wartości brak
     *
     * <div class="message-note">{@code null} safe method</div>
     * @param <T>
     * @param values lista, w której obiekt klasy jest szukany
     * @param clazz szukana klasa
     * @return
     */
    public static <T> T first(List<?> values, Class<T> clazz) {
        if(null == values) { return null; }
        for (int i=0, n=values.size(); i<n; i++) {
            if( clazz.isInstance(values.get(i)) ) { return clazz.cast(values.get(i)); }
        }
        return null;
    }


    /**
     * Wyszukuje i zwraca pierwszy obiekt z listy, lub null, jeśli lista pusta.
     *
     * <div class="message-note">{@code null} safe method</div>
     * @param <T>
     * @param values
     * @return
     */
    public static <T> T first(List<T> values) {
        if(null == values) { return null; }
        return values.isEmpty() ? null : values.get(0);
    }

    /**
     * Wyszukuje i zwraca ostatni obiekt o klasie T, lub null, jeśli takiej wartości brak.
     *
     * <div class="message-note">{@code null} safe method</div>
     * @param <T>
     * @param values lista, w której obiekt klasy jest szukany
     * @param clazz szukana klasa
     * @return
     */
    public static <T> T last(List<?> values, Class<T> clazz) {
        if(null == values) { return null; }
        for (int i=values.size()-1; i>=0; i--) {
            if( clazz.isInstance(values.get(i)) ) { return clazz.cast(values.get(i)); }
        }
        return null;
    }

    /**
     * Zwraca ostatni obiekt z listy, lub null, jeśli lista jest pusta.
     *
     * <div class="message-note">{@code null} safe method</div>
     * @param <T>
     * @param values lista, w której obiekt klasy jest szukany
     * @return
     */
    public static <T> T last(List<T> values) {
        if(null == values || values.isEmpty()) { return null; }
        return values.get(values.size()-1);
    }

    /**
     * Zwraca rozmiar listy.
     * <div class="message-note">{@code null} safe method</div>
     * @param <T>
     * @param values
     * @return
     */
    public static <T> int size(List<T> values) {
        return null == values ? 0 : values.size();
    }

    /**
     * Sprawdza czy lista jest pusta.
     * <div class="message-note">{@code null} safe method</div>
     * @param <T>
     * @param values
     * @return
     */
    public static <T> boolean isEmpty(List<T> values) {
        return 0 == size(values);
    }

    /**
     * Porównuje rozmiary list
     * <div class="message-note">{@code null} safe method</div>
     * @param <T1>
     * @param <T2>
     * @param a
     * @param b
     * @return
     * <table>
     *  <tr><td>ujemne</td><td> - jeśli a &lt; b</td></tr>
     *  <tr><td>dodatnie</td><td> - jeśli a &gt; b </td></tr>
     *  <tr><td>dodatnie</td><td> - jeśli a == b </td></tr>
     * </table>
     */
    public static <T1,T2> int compareSize(List<T1> a, List<T2> b) {
        return size(b) - size(a);
    }

    /**
     * Sprawdza, czy listy mają ten sam rozmiar.
     * Listy, które są równe {@code null} traktowane są jako puste (rozmiar 0).
     *
     * <div class="message-note">{@code null} safe method</div>
     * @param <T1>
     * @param <T2>
     * @param a
     * @param b
     * @return
     */
    public static <T1,T2> boolean equalSize(List<T1> a, List<T2> b) {
        return size(a) == size(b);
    }


    /**
     * Przycina listę, jeśli lista ma rozmiar większy od podanej granicy.
     * @param <T>
     * @param list
     * @param size maksymalny rozmiar listy
     * @return
     * "view" listy podanej jako argument. Modyfikacja obciętej listy
     * ma wpływ na kolekcję źródłową.
     */
    public static <T> List<T> clip(List<T> list, int size) {
        return list.subList(0, Math.min(list.size(), size));
    }


    /**
     * Konwertuje listę elementów na tablicę o podanym typie, dokonując po drodze
     * odpowiednich rzutowań i unbox'owań, jeśli są konieczne. Zwrócona tablica jest
     * niezależna od listy.
     * @param <Auto>
     * @param type
     * @param source
     * @return
     * @throws ClassCastException jeśli konwersja elementów jest niemożliwa do przeprowadzenia
     */
    public static <Auto> Auto toArray(Class<?> type, List<?> source) {
        return (Auto)ArrayUtils.toArray(type, source);
    }

    /**
     * Usuwa pierwszy element z listy i przesuwa wszystkie pozostałe wartości w lewo.
     * Uwaga! Ostatni element po operacji {@code shift} jest zduplikowany!
     * @param <T>
     * @param list
     * @return usunięta wartość
     */
    public static <T> T shift(List<T> list)  {
        if(list.isEmpty()) {
            throw new EmptyCollectionException();
        }
        if(list.size() == 1 ) {
            return list.get(0);
        }
        if(list instanceof LinkedList) {
            LinkedList<T> linked = (LinkedList<T>)list;
            linked.addLast(linked.getLast());
            return linked.removeFirst();
        } else {
            T result = list.remove(0);
            list.add(list.get(list.size()-1));
            return result;
        }
    }

    /**
     * Usuwa ostatni element z tablicy i przesuwa wszystkie pozostałe wartości w prawo.
     * Uwaga! Pierwszy element po operacji {@code unshift} jest zduplikowany!
     * @param <T>
     * @param list
     * @return usunięta wartość
     */
    public static <T> T unshift(List<T> list)  {
        if(list.isEmpty()) {
            throw new EmptyCollectionException();
        }
        if(list.size() == 1 ) {
            return list.get(0);
        }
        if(list instanceof LinkedList) {
            LinkedList<T> linked = (LinkedList<T>)list;
            linked.addFirst(linked.getFirst());
            return linked.removeLast();
        } else {
            T result = list.remove(list.size()-1);
            list.add(0, list.get(0));
            return result;
        }
    }

    /**
     * Tworzy nową listę i kopiuje do niej elementy znajdujące się w obiekcie {@code iterable}
     * @param <T>
     * @param iterable
     * @return
     */
    public static <T> List<T> asList(Iterable<T> iterable) {
        return asList(iterable.iterator());
    }

    /**
     * Tworzy nową listę i kopiuje do niej elementy zwrócone przez podany iterator.
     * @param <T>
     * @param iterator
     * @return
     */
    public static <T> List<T> asList(Iterator<T> iterator) {
        List<T> list = new ArrayList<>();
        while(iterator.hasNext()) { list.add(iterator.next()); }
        return list;
    }

    /**
     * Tworzy listę, której nie można modyfikować, kopiując do niej podane wartości.
     * @param <T>
     * @param values
     * @return
     */
    public static <T> List<T> constCopy(T[] values) {
        return constCopy(Arrays.asList(values));
    }

    /**
     * Tworzy listę, której nie można modyfikować, kopiując do niej podane wartości.
     * @param <T>
     * @param values
     * @return
     */
    public static <T> List<T> constCopy(Collection<? extends T> values) {
        return new ConstList<>(values);
    }
    
    /**
     * Tworzy nową listę o podanym rozmiarze. Elementy są generowane przez
     * podaną funkcję w momencie tworzenia listy.
     * @param <T>
     * @param size
     * @param function
     * @return 
     */
    public static <T> List<T> produce(final int size, final Function<T, Integer> function) {
        List<T> ret = new ArrayList<>(size);
        for(int i=0; i<size; i++) {
            ret.add(function.apply(i));
        }
        return ret;
    }

    private static class ConstList<T> extends AbstractList<T> {

        private final Object[] array;

        public ConstList(T[] values) {
            this.array = ArrayUtils.arraycopy(values);
        }

        public ConstList(Collection<? extends T> values) {
            this.array = values.toArray();
        }

        @Override
        @SuppressWarnings("unchecked")
        public T get(int index) {
            return (T)array[index];
        }

        @Override
        public int size() {
            return array.length;
        }

    }



}
