/*
 * 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.
 *
 * Copyright 2012-2018 the original author or authors.
 */
package org.assertj.core.api;

import static org.assertj.core.error.ShouldStartWith.shouldStartWith;
import static org.assertj.core.internal.CommonValidations.checkIsNotNull;
import static org.assertj.core.util.Lists.newArrayList;

import java.util.AbstractList;
import java.util.Iterator;
import java.util.List;
import java.util.stream.BaseStream;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;

import org.assertj.core.internal.Failures;
import org.assertj.core.util.Lists;
import org.assertj.core.util.VisibleForTesting;

/**
 * Concrete assertions for {@link List}s without any final methods to allow proxying.
 * 
 * @author Gaël LHEZ
 * @since 2.5.1 / 3.5.1
 */
public class ProxyableListAssert<ELEMENT> extends
    FactoryBasedNavigableListAssert<ProxyableListAssert<ELEMENT>, List<? extends ELEMENT>, ELEMENT, ObjectAssert<ELEMENT>> {

  public ProxyableListAssert(List<? extends ELEMENT> actual) {
    super(actual, ProxyableListAssert.class, new ObjectAssertFactory<>());
  }

  @Override
  protected <ELEMENT2> AbstractListAssert<?, List<? extends ELEMENT2>, ELEMENT2, ObjectAssert<ELEMENT2>> newListAssertInstance(List<? extends ELEMENT2> newActual) {
    return new ProxyableListAssert<>(newActual);
  }

  // constructors are used indirectly by assumeThat/ soft assertThat methods taking streams

  protected ProxyableListAssert(Stream<? extends ELEMENT> actual) {
    this(actual == null ? null : new ProxyableListAssert.ListFromStream<>(actual));
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  protected ProxyableListAssert(IntStream actual) {
    this(actual == null ? null : new ProxyableListAssert.ListFromStream(actual));
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  protected ProxyableListAssert(LongStream actual) {
    this(actual == null ? null : new ProxyableListAssert.ListFromStream(actual));
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  protected ProxyableListAssert(DoubleStream actual) {
    this(actual == null ? null : new ProxyableListAssert.ListFromStream(actual));
  }

  @Override
  protected ProxyableListAssert<ELEMENT> newAbstractIterableAssert(Iterable<? extends ELEMENT> iterable) {
    return new ProxyableListAssert<>(newArrayList(iterable));
  }

  @Override
  public ProxyableListAssert<ELEMENT> isEqualTo(Object expected) {
    if (actual instanceof ProxyableListAssert.ListFromStream && asListFromStream().stream == expected) {
      return myself;
    }
    return super.isEqualTo(expected);
  }

  @Override
  public ProxyableListAssert<ELEMENT> isInstanceOf(Class<?> type) {
    if (actual instanceof ProxyableListAssert.ListFromStream) {
      objects.assertIsInstanceOf(info, asListFromStream().stream, type);
      return myself;
    }
    return super.isInstanceOf(type);
  }

  @Override
  public ProxyableListAssert<ELEMENT> isInstanceOfAny(Class<?>... types) {
    if (actual instanceof ProxyableListAssert.ListFromStream) {
      objects.assertIsInstanceOfAny(info, asListFromStream().stream, types);
      return myself;
    }
    return super.isInstanceOfAny(types);
  }

  @Override
  public ProxyableListAssert<ELEMENT> isOfAnyClassIn(Class<?>... types) {
    if (actual instanceof ProxyableListAssert.ListFromStream) {
      objects.assertIsOfAnyClassIn(info, asListFromStream().stream, types);
      return myself;
    }
    return super.isOfAnyClassIn(types);
  }

  @Override
  public ProxyableListAssert<ELEMENT> isExactlyInstanceOf(Class<?> type) {
    if (actual instanceof ProxyableListAssert.ListFromStream) {
      objects.assertIsExactlyInstanceOf(info, asListFromStream().stream, type);
      return myself;
    }
    return super.isExactlyInstanceOf(type);
  }

  @Override
  public ProxyableListAssert<ELEMENT> isNotInstanceOf(Class<?> type) {
    if (actual instanceof ProxyableListAssert.ListFromStream) {
      objects.assertIsNotInstanceOf(info, asListFromStream().stream, type);
      return myself;
    }
    return super.isNotInstanceOf(type);
  }

  @Override
  public ProxyableListAssert<ELEMENT> isNotInstanceOfAny(Class<?>... types) {
    if (actual instanceof ProxyableListAssert.ListFromStream) {
      objects.assertIsNotInstanceOfAny(info, asListFromStream().stream, types);
      return myself;
    }
    return super.isNotInstanceOfAny(types);
  }

  @Override
  public ProxyableListAssert<ELEMENT> isNotOfAnyClassIn(Class<?>... types) {
    if (actual instanceof ProxyableListAssert.ListFromStream) {
      objects.assertIsNotOfAnyClassIn(info, asListFromStream().stream, types);
      return myself;
    }
    return super.isNotOfAnyClassIn(types);
  }

  @Override
  public ProxyableListAssert<ELEMENT> isNotExactlyInstanceOf(Class<?> type) {
    if (actual instanceof ProxyableListAssert.ListFromStream) {
      objects.assertIsNotExactlyInstanceOf(info, asListFromStream().stream, type);
      return myself;
    }
    return super.isNotExactlyInstanceOf(type);
  }

  @Override
  public ProxyableListAssert<ELEMENT> isSameAs(Object expected) {
    if (actual instanceof ProxyableListAssert.ListFromStream) {
      objects.assertSame(info, asListFromStream().stream, expected);
      return myself;
    }
    return super.isSameAs(expected);
  }

  @Override
  public ProxyableListAssert<ELEMENT> isNotSameAs(Object expected) {
    if (actual instanceof ProxyableListAssert.ListFromStream) {
      objects.assertNotSame(info, asListFromStream().stream, expected);
      return myself;
    }
    return super.isNotSameAs(expected);
  }

  @Override
  public ProxyableListAssert<ELEMENT> startsWith(@SuppressWarnings("unchecked") ELEMENT... sequence) {
    if (!(actual instanceof ProxyableListAssert.ListFromStream)) {
      return super.startsWith(sequence);
    }
    objects.assertNotNull(info, actual);
    checkIsNotNull(sequence);
    // NO SUPPORT FOR infinite streams as it prevents chaining other assertions afterward, it requires to consume the
    // Stream partially, if you chain another assertion, the stream is already consumed.
    Iterator<? extends ELEMENT> iterator = asListFromStream().stream().iterator();
    if (sequence.length == 0 && iterator.hasNext()) throw new AssertionError("actual is not empty");
    int i = 0;
    while (iterator.hasNext()) {
      if (i >= sequence.length) break;
      if (iterables.getComparisonStrategy().areEqual(iterator.next(), sequence[i++])) continue;
      throw actualDoesNotStartWithSequence(info, sequence);
    }
    if (sequence.length > i) {
      // sequence has more elements than actual
      throw actualDoesNotStartWithSequence(info, sequence);
    }
    return myself;
  }

  private AssertionError actualDoesNotStartWithSequence(AssertionInfo info, Object[] sequence) {
    return Failures.instance()
                   .failure(info, shouldStartWith("Stream under test", sequence, iterables.getComparisonStrategy()));
  }

  @SuppressWarnings("rawtypes")
  private ProxyableListAssert.ListFromStream asListFromStream() {
    return (ProxyableListAssert.ListFromStream) actual;
  }

  @VisibleForTesting
  static class ListFromStream<ELEMENT, STREAM extends BaseStream<ELEMENT, STREAM>> extends AbstractList<ELEMENT> {
    private BaseStream<ELEMENT, STREAM> stream;
    private List<ELEMENT> list;

    public ListFromStream(BaseStream<ELEMENT, STREAM> stream) {
      this.stream = stream;
    }

    @Override
    public Stream<ELEMENT> stream() {
      initList();
      return list.stream();
    }

    private List<ELEMENT> initList() {
      if (list == null) list = Lists.newArrayList(stream.iterator());
      return list;
    }

    @Override
    public int size() {
      initList();
      return list.size();
    }

    @Override
    public ELEMENT get(int index) {
      initList();
      return list.get(index);
    }

  }

}
