include "ListInternal.aes"

namespace List =

  function is_empty(l : list('a)) : bool = switch(l)
    [] => true
    _  => false

  function first(l : list('a)) : option('a) = switch(l)
    []   => None
    h::_ => Some(h)

  function tail(l : list('a)) : option(list('a)) = switch(l)
    []   => None
    _::t => Some(t)

  function last(l : list('a)) : option('a) = switch(l)
    []  => None
    [x] => Some(x)
    _::t => last(t)

  function find(p : 'a => bool, l : list('a)) : option('a) = switch(l)
    []   => None
    h::t => if(p(h)) Some(h) else find(p, t)

  function find_indices(p : 'a => bool, l : list('a)) : list(int) = find_indices_(p, l, 0, [])
  private function find_indices_( p : 'a => bool
                                , l : list('a)
                                , n : int
                                , acc : list(int)
                                ) : list(int) = switch(l)
    []   => reverse(acc)
    h::t => find_indices_(p, t, n+1, if(p(h)) n::acc else acc)

  function nth(n : int, l : list('a)) : option('a) =
    switch(l)
      []   => None
      h::t => if(n == 0) Some(h) else nth(n-1, t)

  /* Unsafe version of `nth` */
  function get(n : int, l : list('a)) : 'a =
    switch(l)
      [] => abort(if(n < 0) "Negative index get" else "Out of index get")
      h::t => if(n == 0) h else get(n-1, t)


  function length(l : list('a)) : int = length_(l, 0)
  private function length_(l : list('a), acc : int) : int = switch(l)
    []   => acc
    _::t => length_(t, acc + 1)


  function from_to(a : int, b : int) : list(int) = [a..b]

  function from_to_step(a : int, b : int, s : int) : list(int) = from_to_step_(a, b, s, [])
  private function from_to_step_(a, b, s, acc) =
    if (a > b) reverse(acc) else from_to_step_(a + s, b, s, a :: acc)


  /* Unsafe. Replaces `n`th element of `l` with `e`. Crashes on over/underflow */
  function replace_at(n : int, e : 'a, l : list('a)) : list('a) =
    if(n<0) abort("insert_at underflow") else replace_at_(n, e, l, [])
  private function replace_at_(n : int, e : 'a, l : list('a), acc : list('a)) : list('a) =
   switch(l)
     []   => abort("replace_at overflow")
     h::t => if (n == 0) reverse(e::acc) ++ t
             else replace_at_(n-1, e, t, h::acc)

  /* Unsafe. Adds `e` to `l` to be its `n`th element. Crashes on over/underflow */
  function insert_at(n : int, e : 'a, l : list('a)) : list('a) =
    if(n<0) abort("insert_at underflow") else insert_at_(n, e, l, [])
  private function insert_at_(n : int, e : 'a, l : list('a), acc : list('a)) : list('a) =
   if (n == 0) reverse(e::acc) ++ l
   else switch(l)
     []   => abort("insert_at overflow")
     h::t => insert_at_(n-1, e, t, h::acc)

  function insert_by(cmp : (('a, 'a) => bool), x : 'a, l : list('a)) : list('a) =
    insert_by_(cmp, x, l, [])
  private function insert_by_(cmp : (('a, 'a) => bool), x : 'a, l : list('a), acc : list('a)) : list('a) =
    switch(l)
      []   => reverse(x::acc)
      h::t =>
        if(cmp(x, h)) // x < h
          reverse(acc) ++ (x::l)
        else
          insert_by_(cmp, x, t, h::acc)


  function foldr(cons : ('a, 'b) => 'b, nil : 'b, l : list('a)) : 'b = switch(l)
    []   => nil
    h::t => cons(h, foldr(cons, nil, t))

  function foldl(rcons : ('b, 'a) => 'b, acc : 'b, l : list('a)) : 'b = switch(l)
    []   => acc
    h::t => foldl(rcons, rcons(acc, h), t)

  function foreach(l : list('a), f : 'a => unit) : unit =
    switch(l)
     []    => ()
     e::l' =>
       f(e)
       foreach(l', f)

  function reverse(l : list('a)) : list('a) = foldl((lst, el) => el :: lst, [], l)

  function map(f : 'a => 'b, l : list('a)) : list('b) = map_(f, l, [])
  private function map_(f : 'a => 'b, l : list('a), acc : list('b)) : list('b) = switch(l)
    []   => reverse(acc)
    h::t => map_(f, t, f(h)::acc)

  function flat_map(f : 'a => list('b), l : list('a)) : list('b) =
    ListInternal.flat_map(f, l)

  function filter(p : 'a => bool, l : list('a)) : list('a) = filter_(p, l, [])
  private function filter_(p : 'a => bool, l : list('a), acc : list('a)) : list('a) = switch(l)
    []   => reverse(acc)
    h::t => filter_(p, t, if(p(h)) h::acc else acc)

  /* Take `n` first elements */
  function take(n : int, l : list('a)) : list('a) =
    if(n < 0) abort("Take negative number of elements") else take_(n, l, [])
  private function take_(n : int, l : list('a), acc : list('a)) : list('a) =
    if(n == 0) reverse(acc)
    else switch(l)
      []   => reverse(acc)
      h::t => take_(n-1, t, h::acc)

  /* Drop `n` first elements */
  function drop(n : int, l : list('a)) : list('a) =
   if(n < 0) abort("Drop negative number of elements")
   elif (n == 0) l
   else switch(l)
      []   => []
      h::t => drop(n-1, t)

  /* Get the longest prefix of a list in which every element matches predicate `p` */
  function take_while(p : 'a => bool, l : list('a)) : list('a) = take_while_(p, l, [])
  private function take_while_(p : 'a => bool, l : list('a), acc : list('a)) : list('a) = switch(l)
    []   => reverse(acc)
    h::t => if(p(h)) take_while_(p, t, h::acc) else reverse(acc)

  /* Drop elements from `l` until `p` holds */
  function drop_while(p : 'a => bool, l : list('a)) : list('a) = switch(l)
    []   => []
    h::t => if(p(h)) drop_while(p, t) else l

  /* Splits list into two lists of elements that respectively match and don't match predicate `p` */
  function partition(p : 'a => bool, l : list('a)) : (list('a) * list('a)) = partition_(p, l, [], [])
  private function partition_( p : 'a => bool
                             , l : list('a)
                             , acc_t : list('a)
                             , acc_f : list('a)
                             ) : (list('a) * list('a)) = switch(l)
    []   => (reverse(acc_t), reverse(acc_f))
    h::t => if(p(h)) partition_(p, t, h::acc_t, acc_f) else partition_(p, t, acc_t, h::acc_f)


  function flatten(ll : list(list('a))) : list('a) = foldr((l1, l2) => l1 ++ l2, [], ll)

  function all(p : 'a => bool, l : list('a)) : bool = switch(l)
    []   => true
    h::t => if(p(h)) all(p, t) else false

  function any(p : 'a => bool, l : list('a)) : bool = switch(l)
    []   => false
    h::t => if(p(h)) true else any(p, t)

  function sum(l : list(int)) : int = foldl ((a, b) => a + b, 0, l)

  function product(l : list(int)) : int = foldl((a, b) => a * b, 1, l)


  /* Zips two list by applying bimapping function on respective elements. Drops longer tail. */
  function zip_with(f : ('a, 'b) => 'c, l1 : list('a), l2 : list('b)) : list('c) = zip_with_(f, l1, l2, [])
  private function zip_with_( f : ('a, 'b) => 'c
                            , l1 : list('a)
                            , l2 : list('b)
                            , acc : list('c)
                            ) : list('c) = switch ((l1, l2))
    (h1::t1, h2::t2) => zip_with_(f, t1, t2, f(h1, h2)::acc)
    _ => reverse(acc)

  /* Zips two lists into list of pairs. Drops longer tail. */
  function zip(l1 : list('a), l2 : list('b)) : list('a * 'b) = zip_with((a, b) => (a, b), l1, l2)

  function unzip(l : list('a * 'b)) : list('a) * list('b) = unzip_(l, [], [])
  private function unzip_( l     : list('a * 'b)
                         , acc_l : list('a)
                         , acc_r : list('b)
                         ) : (list('a) * list('b)) = switch(l)
    []               => (reverse(acc_l), reverse(acc_r))
    (left, right)::t => unzip_(t, left::acc_l, right::acc_r)


  // TODO: Improve?
  function sort(lesser_cmp : ('a, 'a) => bool, l : list('a)) : list('a) = switch(l)
    []   => []
    h::t => switch (partition((x) => lesser_cmp(x, h), t))
      (lesser, bigger) => sort(lesser_cmp, lesser) ++ h::sort(lesser_cmp, bigger)


  function intersperse(delim : 'a, l : list('a)) : list('a) = intersperse_(delim, l, [])
  private function intersperse_(delim : 'a, l : list('a), acc : list('a)) : list('a) = switch(l)
    []   => reverse(acc)
    [e]  => reverse(e::acc)
    h::t => intersperse_(delim, t, delim::h::acc)


  function enumerate(l : list('a)) : list(int * 'a) = enumerate_(l, 0, [])
  private function enumerate_(l : list('a), n : int, acc : list(int * 'a)) : list(int * 'a) = switch(l)
    []   => reverse(acc)
    h::t => enumerate_(t, n + 1, (n, h)::acc)

