/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community 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://opensource.org/licenses/ecl2.txt
 *
 * 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.opencastproject.util.data;

import static java.util.Arrays.asList;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.opencastproject.util.data.Arrays.array;
import static org.opencastproject.util.data.Collections.iterator;
import static org.opencastproject.util.data.Collections.list;
import static org.opencastproject.util.data.Collections.repeat;
import static org.opencastproject.util.data.Iterators.constant;
import static org.opencastproject.util.data.Iterators.intRangeE;
import static org.opencastproject.util.data.Monadics.IteratorMonadic;
import static org.opencastproject.util.data.Monadics.mlazy;
import static org.opencastproject.util.data.Monadics.mlist;
import static org.opencastproject.util.data.Option.none;
import static org.opencastproject.util.data.Option.some;
import static org.opencastproject.util.data.Tuple.tuple;

import org.opencastproject.util.data.functions.Booleans;
import org.opencastproject.util.data.functions.Functions;

import org.junit.Test;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;

public class MonadicsTest {

  @Test
  public void testMap() {
    List<Integer> list = asList(1, 2, 3);
    List<String> mapped = mlist(list).map(new Function<Integer, Integer>() {
      @Override
      public Integer apply(Integer a) {
        return a * a;
      }
    }).map(new Function<Integer, String>() {
      @Override
      public String apply(Integer a) {
        return a + " " + a;
      }
    }).value();
    assertEquals("1 1", mapped.get(0));
    assertEquals("4 4", mapped.get(1));
    assertEquals("9 9", mapped.get(2));
  }

  @Test
  public void testFlatMap() {
    List<Integer> mapped = mlist(new Integer[]{1, 2, 3}).flatMap(new Function<Integer, Collection<Integer>>() {
      @Override public Collection<Integer> apply(Integer a) {
        return asList(a, a);
      }
    }).value();
    assertEquals(6, mapped.size());
    assertEquals(new Integer(1), mapped.get(0));
    assertEquals(new Integer(2), mapped.get(2));
    assertEquals(new Integer(3), mapped.get(4));
  }

  @Test
  public void testFlatMap2() {
    final List<Object> l = list((Object) 1);
    final List<String> r1 = mlist(l).bind(new Function<Object, Option<String>>() {
      @Override public Option<String> apply(Object o) {
        return some("x");
      }
    }).value();
    assertEquals(1, r1.size());
    assertEquals("x", r1.get(0));
    final List<String> r2 = mlist(l).bind(new Function<Object, Option<String>>() {
      @Override public Option<String> apply(Object o) {
        return none();
      }
    }).value();
    assertEquals(0, r2.size());
  }

  @Test
  public void testFoldl() {
    String fold = mlist(new Integer[]{1, 2, 3}).foldl("", new Function2<String, Integer, String>() {
      @Override
      public String apply(String s, Integer a) {
        return s + a + a;
      }
    });
    assertEquals("112233", fold);
  }

  @Test
  public void testReducel() {
    String fold = mlist("a", "b", "c").reducel(new Function2<String, String, String>() {
      @Override
      public String apply(String a, String b) {
        return a + "," + b;
      }
    });
    assertEquals("a,b,c", fold);
  }

  @Test(expected = RuntimeException.class)
  public void testReducelError() {
    mlist(new String[]{}).reducel(new Function2<String, String, String>() {
      @Override
      public String apply(String a, String b) {
        return a + "," + b;
      }
    });
  }

  @Test
  public void testFlatten() {
    List<Integer> mapped = mlist(list(list(1, 2), list(3, 4))).flatMap(Functions.<List<Integer>>identity()).value();
    assertEquals(4, mapped.size());
    List<Integer> mapped2 = mlist(some(1), Option.<Integer>option(null), some(3), Option.<Integer>none()).flatMap(Functions.<Option<Integer>>identity()).value();
    assertEquals(2, mapped2.size());
  }

  @Test
  public void testTakeArray() {
    assertTrue(mlist(array(1, 2, 3, 4, 5)).take(0).value().isEmpty());
    assertEquals(3, mlist(array(1, 2, 3, 4, 5)).take(3).value().size());
    assertEquals(5, mlist(array(1, 2, 3, 4, 5)).take(5).value().size());
    assertEquals(5, mlist(array(1, 2, 3, 4, 5)).take(10).value().size());
  }

  @Test
  public void testTakeList() {
    assertTrue(mlist(asList(1, 2, 3, 4, 5)).take(0).value().isEmpty());
    assertEquals(3, mlist(asList(1, 2, 3, 4, 5)).take(3).value().size());
    assertEquals(5, mlist(asList(1, 2, 3, 4, 5)).take(5).value().size());
    assertEquals(5, mlist(asList(1, 2, 3, 4, 5)).take(10).value().size());
  }

  @Test
  public void testTakeIterator() {
    assertTrue(mlist(asList(1, 2, 3, 4, 5).iterator()).take(0).value().isEmpty());
    assertEquals(3, mlist(asList(1, 2, 3, 4, 5).iterator()).take(3).value().size());
    assertEquals(5, mlist(asList(1, 2, 3, 4, 5).iterator()).take(5).value().size());
    assertEquals(5, mlist(asList(1, 2, 3, 4, 5).iterator()).take(10).value().size());
  }

  @Test
  public void testDropArray() {
    assertTrue(mlist(array(1, 2, 3, 4, 5)).drop(10).value().isEmpty());
    assertEquals(3, mlist(array(1, 2, 3, 4, 5)).drop(2).value().size());
    assertEquals(1, mlist(array(1, 2, 3, 4, 5)).drop(4).value().size());
    assertEquals(5, mlist(array(1, 2, 3, 4, 5)).drop(0).value().size());
  }

  @Test
  public void testLazyMap() {
    final boolean[] applied = {false};
    IteratorMonadic<Integer> im = mlazy(asList(1, 2, 3, 4, 5))
            .map(new Function<Integer, Integer>() {
              @Override
              public Integer apply(Integer i) {
                applied[0] = true;
                return i * i;
              }
            })
            .map(new Function<Integer, Integer>() {
              @Override
              public Integer apply(Integer i) {
                applied[0] = true;
                return i + i;
              }
            });
    assertFalse(applied[0]);
    im.value();
    assertFalse(applied[0]);
    List<Integer> eval = im.eval();
    assertTrue(applied[0]);
    assertArrayEquals(new Integer[]{2, 8, 18, 32, 50}, eval.toArray(new Integer[]{}));
    // test empty input
    assertTrue(Collections.toList(
            mlazy(Collections.<Integer>nil()).map(Functions.<Integer>identity()).value()
    ).isEmpty());
  }

  @Test
  public void testLazyMapIndex() {
    List<Integer> eval = mlazy(asList(1, 2, 3, 4, 5)).mapIndex(new Function2<Integer, Integer, Integer>() {
      @Override
      public Integer apply(Integer n, Integer i) {
        return n + i;
      }
    }).eval();
    assertArrayEquals(new Integer[]{1, 3, 5, 7, 9}, eval.toArray(new Integer[]{}));
  }

  @Test
  public void testLazyFlatMap() {
    List<Integer> eval = mlazy(asList(1, 2, 3))
            .flatMap(new Function<Integer, Iterator<Integer>>() {
              @Override
              public Iterator<Integer> apply(Integer integer) {
                if (integer >= 2) {
                  return asList(1, 2, 3).iterator();
                } else {
                  return java.util.Collections.<Integer>emptyList().iterator();
                }
              }
            })
            .flatMap(new Function<Integer, Iterator<Integer>>() {
              @Override
              public Iterator<Integer> apply(Integer integer) {
                return asList(integer * integer).iterator();
              }
            })
            .eval();
    assertArrayEquals(array(1, 4, 9, 1, 4, 9), Collections.toArray(Integer.class, eval));
  }

  @Test
  public void testLazyFlatMap2() {
    List<Integer> eval = mlazy(asList(1, 2, 3))
            .flatMap(new Function<Integer, Iterator<Integer>>() {
              @Override
              public Iterator<Integer> apply(Integer integer) {
                if (integer != 2) {
                  return asList(1, 2, 3).iterator();
                } else {
                  return java.util.Collections.<Integer>emptyList().iterator();
                }
              }
            })
            .flatMap(new Function<Integer, Iterator<Integer>>() {
              @Override
              public Iterator<Integer> apply(Integer integer) {
                return asList(integer * integer).iterator();
              }
            })
            .eval();
    assertArrayEquals(array(1, 4, 9, 1, 4, 9), Collections.toArray(Integer.class, eval));
  }

  @Test
  public void testLazyFlatMapDoubling() {
    List<Integer> eval = mlazy(asList(1, 2, 3, 4, 5)).flatMap(MonadicsTest.<Integer>twice()).eval();
    assertArrayEquals(new Integer[]{1, 1, 2, 2, 3, 3, 4, 4, 5, 5}, eval.toArray(new Integer[]{}));
  }

  @Test
  public void testLazyFlatMapKeepSize() {
    List<Integer> eval = mlazy(asList(1))
            .flatMap(new Function<Integer, Iterator<Integer>>() {
              @Override
              public Iterator<Integer> apply(Integer integer) {
                return Option.<Integer>some(2).iterator();
              }
            })
            .eval();
    assertArrayEquals(new Integer[]{2}, eval.toArray(new Integer[0]));
  }

  @Test
  public void testLazyFlatMapTimes() {
    List<Integer> eval = mlazy(asList(1, 2, 3, 4)).flatMap(times).eval();
    assertArrayEquals(new Integer[]{1, 2, 2, 3, 3, 3, 4, 4, 4, 4}, eval.toArray(new Integer[0]));
  }

  @Test
  public void testLazyFlatMapEmptyInput() {
    assertTrue(mlazy(java.util.Collections.<Integer>emptyList()).flatMap(times).eval().isEmpty());
  }

  @Test
  public void testLazyFlatMapEmptyOutput() {
    // test empty output
    List<Integer> eval = mlazy(asList(1, 2, 3))
            .flatMap(new Function<Integer, Iterator<Integer>>() {
              @Override
              public Iterator<Integer> apply(Integer integer) {
                return Option.<Integer>none().iterator();
              }
            })
            .eval();
    assertTrue(eval.isEmpty());
  }

  @Test
  public void testLazyFlatMapHasNext() {
    Iterator<Integer> ints = mlazy(asList(1, 2)).flatMap(MonadicsTest.<Integer>twice()).value();
    // test correctness of hasNext()
    assertTrue(ints.hasNext());
    assertTrue(ints.hasNext());
    assertTrue(ints.hasNext());
    assertTrue(ints.hasNext());
    assertTrue(ints.hasNext());
  }

  @Test
  public void testLazyFlatMapMultiple() {
    List<Integer> eval = mlazy(asList(1, 2))
            .flatMap(MonadicsTest.<Integer>twice())
            .flatMap(MonadicsTest.<Integer>twice())
            .eval();
    assertArrayEquals(new Integer[]{1, 1, 1, 1, 2, 2, 2, 2}, eval.toArray(new Integer[]{}));
  }

  @Test
  public void testLazyEachEmpty() {
    final boolean[] run = {false};
    mlazy(java.util.Collections.emptyList())
            .each(new Effect<Object>() {
              @Override
              public void run(Object o) {
                run[0] = true;
              }
            })
            .eval();
    assertFalse(run[0]);
  }

  @Test
  public void testLazyEach() {
    final int[] sum = {0};
    mlazy(asList(1, 2, 3, 4, 5))
            .each(new Effect<Integer>() {
              @Override
              public void run(Integer o) {
                sum[0] += o;
              }
            })
            .eval();
    assertEquals(15, sum[0]);
  }

  @Test
  public void testLazyEachIndexEmpty() {
    final boolean[] run = {false};
    mlazy(java.util.Collections.emptyList())
            .eachIndex(new Effect2<Object, Integer>() {
              @Override
              public void run(Object o, Integer i) {
                run[0] = true;
              }
            })
            .eval();
    assertFalse(run[0]);
  }

  @Test
  public void testLazyEachIndex() {
    final int[] sum = {0};
    mlazy(asList(1, 2, 3, 4, 5))
            .eachIndex(new Effect2<Integer, Integer>() {
              @Override
              public void run(Integer o, Integer i) {
                sum[0] += (o * i);
              }
            })
            .eval();
    assertEquals(40, sum[0]);
  }

  @Test
  public void testLazyTake() {
    assertTrue(mlazy(asList(1, 2, 3, 4, 5)).take(0).eval().isEmpty());
    assertEquals(3, mlazy(asList(1, 2, 3, 4, 5)).take(3).eval().size());
    assertEquals(5, mlazy(asList(1, 2, 3, 4, 5)).take(5).eval().size());
    assertEquals(5, mlazy(asList(1, 2, 3, 4, 5)).take(10).eval().size());
    assertEquals(100, mlazy(constant(10)).take(100).eval().size());
  }

  @Test
  public void testLazyFilter() {
    assertEquals(5, mlazy(intRangeE(0, 10)).filter(Booleans.lt(5)).eval().size());
    assertEquals(10, mlazy(intRangeE(0, 10)).filter(Booleans.lt(100)).eval().size());
    assertEquals(0, mlazy(intRangeE(0, 10)).filter(Booleans.lt(0)).eval().size());
  }

  @Test
  public void testLazyExists() {
    assertTrue(mlazy(intRangeE(0, 10)).exists(Booleans.gt(5)));
    assertFalse(mlazy(intRangeE(0, 10)).exists(Booleans.gt(9)));
  }

  @Test
  public void testZip() {
    {
      List<Tuple<String, Integer>> r = mlist(new String[]{"a", "b", "c"}).zip(list(1, 2, 3, 4, 5)).value();
      assertEquals(3, r.size());
      assertEquals(tuple("b", 2), r.get(1));
    }
    {
      List<Tuple<String, Integer>> r = mlist(new String[]{"a", "b", "c"}).zip(Collections.<Integer>nil()).value();
      assertEquals(0, r.size());
    }
    {
      List<Tuple<String, Integer>> r = mlist(list("a", "b", "c")).zip(list(1, 2)).value();
      assertEquals(2, r.size());
      assertEquals(tuple("b", 2), r.get(1));
    }
    {
      List<Tuple<String, Integer>> r = mlist(list("a", "b", "c").iterator()).zip(list(1, 2)).value();
      assertEquals(2, r.size());
      assertEquals(tuple("b", 2), r.get(1));
    }
    {
      List<Tuple<String, Integer>> r = mlist(Collections.<String>nil().iterator()).zip(list(1, 2)).value();
      assertEquals(0, r.size());
    }
  }

  @Test
  public void testConcat() {
    // does not compile
    // final List<Integer> a = mlist(1, 2, 3).concat(list(4, "5")).value();
    final List<Integer> a = mlist(1, 2, 3).concat(list(4, 5)).value();
    assertEquals(5, a.size());
    assertEquals(4, (Object) a.get(3)); // Object cast because of overloading ambiguity
    final List<Object> b = mlist((Object) 1).concat(Collections.<Object>list("x")).value();
    assertEquals(2, b.size());
    assertEquals(1, b.get(0));
    assertEquals("x", b.get(1));
  }

  private static <A> Function<A, Iterator<A>> twice() {
    return new Function<A, Iterator<A>>() {
      @Override
      public Iterator<A> apply(A a) {
        return iterator(a, a);
      }
    };
  }

  private static Function<Integer, Iterator<Integer>> times = new Function<Integer, Iterator<Integer>>() {
    @Override
    public Iterator<Integer> apply(Integer a) {
      return repeat(a, a);
    }
  };
}
