package org.opentest4j.reporting.tooling.spi.htmlreport;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 * Immutable implementation of {@link Section}.
 * <p>
 * Use the builder to create immutable instances:
 * {@code new Section.Builder()}.
 */
@SuppressWarnings({"all"})
final class ImmutableSection implements Section {
  private final String title;
  private final String metaInfo;
  private final List<Block<?>> blocks;
  private final int order;

  private ImmutableSection(ImmutableSection.Builder builder) {
    this.title = builder.title;
    this.metaInfo = builder.metaInfo;
    this.blocks = createUnmodifiableList(true, builder.blocks);
    this.order = builder.orderIsSet()
        ? builder.order
        : Section.super.getOrder();
  }

  private ImmutableSection(
      String title,
      String metaInfo,
      List<Block<?>> blocks,
      int order) {
    this.title = title;
    this.metaInfo = metaInfo;
    this.blocks = blocks;
    this.order = order;
  }

  /**
   * @return The value of the {@code title} attribute
   */
  @Override
  public String getTitle() {
    return title;
  }

  /**
   * @return The value of the {@code metaInfo} attribute
   */
  @Override
  public Optional<String> getMetaInfo() {
    return Optional.ofNullable(metaInfo);
  }

  /**
   * @return The value of the {@code blocks} attribute
   */
  @Override
  public List<Block<?>> getBlocks() {
    return blocks;
  }

  /**
   * @return The value of the {@code order} attribute
   */
  @Override
  public int getOrder() {
    return order;
  }

  /**
   * Copy the current immutable object by setting a value for the {@link Section#getTitle() title} attribute.
   * An equals check used to prevent copying of the same value by returning {@code this}.
   * @param value A new value for title
   * @return A modified copy or the {@code this} object
   */
  public final ImmutableSection withTitle(String value) {
    String newValue = Objects.requireNonNull(value, "title");
    if (this.title.equals(newValue)) return this;
    return new ImmutableSection(newValue, this.metaInfo, this.blocks, this.order);
  }

  /**
   * Copy the current immutable object by setting a <em>present</em> value for the optional {@link Section#getMetaInfo() metaInfo} attribute.
   * @param value The value for metaInfo
   * @return A modified copy or {@code this} if not changed
   */
  public final ImmutableSection withMetaInfo(String value) {
    String newValue = Objects.requireNonNull(value, "metaInfo");
    if (Objects.equals(this.metaInfo, newValue)) return this;
    return new ImmutableSection(this.title, newValue, this.blocks, this.order);
  }

  /**
   * Copy the current immutable object by setting an optional value for the {@link Section#getMetaInfo() metaInfo} attribute.
   * An equality check is used on inner value to prevent copying of the same value by returning {@code this}.
   * @param optional An optional value for metaInfo
   * @return A modified copy or {@code this} if not changed
   */
  public final ImmutableSection withMetaInfo(Optional<String> optional) {
    String value = optional.orElse(null);
    if (Objects.equals(this.metaInfo, value)) return this;
    return new ImmutableSection(this.title, value, this.blocks, this.order);
  }

  /**
   * Copy the current immutable object with elements that replace the content of {@link Section#getBlocks() blocks}.
   * @param elements The elements to set
   * @return A modified copy of {@code this} object
   */
  @SafeVarargs @SuppressWarnings("varargs")
  public final ImmutableSection withBlocks(Block<?>... elements) {
    List<Block<?>> newValue = createUnmodifiableList(false, createSafeList(Arrays.asList(elements), true, false));
    return new ImmutableSection(this.title, this.metaInfo, newValue, this.order);
  }

  /**
   * Copy the current immutable object with elements that replace the content of {@link Section#getBlocks() blocks}.
   * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}.
   * @param elements An iterable of blocks elements to set
   * @return A modified copy or {@code this} if not changed
   */
  public final ImmutableSection withBlocks(Iterable<? extends Block<?>> elements) {
    if (this.blocks == elements) return this;
    List<Block<?>> newValue = createUnmodifiableList(false, createSafeList(elements, true, false));
    return new ImmutableSection(this.title, this.metaInfo, newValue, this.order);
  }

  /**
   * Copy the current immutable object by setting a value for the {@link Section#getOrder() order} attribute.
   * A value equality check is used to prevent copying of the same value by returning {@code this}.
   * @param value A new value for order
   * @return A modified copy or the {@code this} object
   */
  public final ImmutableSection withOrder(int value) {
    if (this.order == value) return this;
    return new ImmutableSection(this.title, this.metaInfo, this.blocks, value);
  }

  /**
   * This instance is equal to all instances of {@code ImmutableSection} that have equal attribute values.
   * @return {@code true} if {@code this} is equal to {@code another} instance
   */
  @Override
  public boolean equals(Object another) {
    if (this == another) return true;
    return another instanceof ImmutableSection
        && equalsByValue((ImmutableSection) another);
  }

  private boolean equalsByValue(ImmutableSection another) {
    return title.equals(another.title)
        && Objects.equals(metaInfo, another.metaInfo)
        && blocks.equals(another.blocks)
        && order == another.order;
  }

  /**
   * Computes a hash code from attributes: {@code title}, {@code metaInfo}, {@code blocks}, {@code order}.
   * @return hashCode value
   */
  @Override
  public int hashCode() {
    int h = 5381;
    h += (h << 5) + title.hashCode();
    h += (h << 5) + Objects.hashCode(metaInfo);
    h += (h << 5) + blocks.hashCode();
    h += (h << 5) + order;
    return h;
  }

  /**
   * Prints the immutable value {@code Section} with attribute values.
   * @return A string representation of the value
   */
  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder("Section{");
    builder.append("title=").append(title);
    if (metaInfo != null) {
      builder.append(", ");
      builder.append("metaInfo=").append(metaInfo);
    }
    builder.append(", ");
    builder.append("blocks=").append(blocks);
    builder.append(", ");
    builder.append("order=").append(order);
    return builder.append("}").toString();
  }

  /**
   * Creates an immutable copy of a {@link Section} value.
   * Uses accessors to get values to initialize the new immutable instance.
   * If an instance is already immutable, it is returned as is.
   * @param instance The instance to copy
   * @return A copied immutable Section instance
   */
  public static Section copyOf(Section instance) {
    if (instance instanceof ImmutableSection) {
      return (ImmutableSection) instance;
    }
    return new Section.Builder()
        .title(instance.getTitle())
        .metaInfo(instance.getMetaInfo())
        .addAllBlocks(instance.getBlocks())
        .order(instance.getOrder())
        .build();
  }

  /**
   * Builds instances of type {@link Section Section}.
   * Initialize attributes and then invoke the {@link #build()} method to create an
   * immutable instance.
   * <p><em>{@code Builder} is not thread-safe and generally should not be stored in a field or collection,
   * but instead used immediately to create instances.</em>
   */
  public static class Builder {
    private static final long INIT_BIT_TITLE = 0x1L;
    private static final long OPT_BIT_ORDER = 0x1L;
    private long initBits = 0x1L;
    private long optBits;

    private String title;
    private String metaInfo;
    private List<Block<?>> blocks = new ArrayList<Block<?>>();
    private int order;

    /**
     * Creates a builder for {@link Section Section} instances.
     * <pre>
     * new Section.Builder()
     *    .title(String) // required {@link Section#getTitle() title}
     *    .metaInfo(Optional&lt;String&gt;) // optional {@link Section#getMetaInfo() metaInfo}
     *    .addBlock|addAllBlocks(org.opentest4j.reporting.tooling.spi.htmlreport.Block&lt;?&gt;) // {@link Section#getBlocks() blocks} elements
     *    .order(int) // optional {@link Section#getOrder() order}
     *    .build();
     * </pre>
     */
    public Builder() {
      if (!(this instanceof Section.Builder)) {
        throw new UnsupportedOperationException("Use: new Section.Builder()");
      }
    }

    /**
     * Initializes the value for the {@link Section#getTitle() title} attribute.
     * @param title The value for title 
     * @return {@code this} builder for use in a chained invocation
     */
    public final Section.Builder title(String title) {
      this.title = Objects.requireNonNull(title, "title");
      initBits &= ~INIT_BIT_TITLE;
      return (Section.Builder) this;
    }

    /**
     * Initializes the optional value {@link Section#getMetaInfo() metaInfo} to metaInfo.
     * @param metaInfo The value for metaInfo
     * @return {@code this} builder for chained invocation
     */
    public final Section.Builder metaInfo(String metaInfo) {
      this.metaInfo = Objects.requireNonNull(metaInfo, "metaInfo");
      return (Section.Builder) this;
    }

    /**
     * Initializes the optional value {@link Section#getMetaInfo() metaInfo} to metaInfo.
     * @param metaInfo The value for metaInfo
     * @return {@code this} builder for use in a chained invocation
     */
    public final Section.Builder metaInfo(Optional<String> metaInfo) {
      this.metaInfo = metaInfo.orElse(null);
      return (Section.Builder) this;
    }

    /**
     * Adds one element to {@link Section#getBlocks() blocks} list.
     * @param element A blocks element
     * @return {@code this} builder for use in a chained invocation
     */
    public final Section.Builder addBlock(Block<?> element) {
      this.blocks.add(Objects.requireNonNull(element, "blocks element"));
      return (Section.Builder) this;
    }

    /**
     * Adds elements to {@link Section#getBlocks() blocks} list.
     * @param elements An array of blocks elements
     * @return {@code this} builder for use in a chained invocation
     */
    @SafeVarargs @SuppressWarnings("varargs")
    public final Section.Builder addBlocks(Block<?>... elements) {
      for (Block<?> element : elements) {
        this.blocks.add(Objects.requireNonNull(element, "blocks element"));
      }
      return (Section.Builder) this;
    }


    /**
     * Sets or replaces all elements for {@link Section#getBlocks() blocks} list.
     * @param elements An iterable of blocks elements
     * @return {@code this} builder for use in a chained invocation
     */
    public final Section.Builder blocks(Iterable<? extends Block<?>> elements) {
      this.blocks.clear();
      return addAllBlocks(elements);
    }

    /**
     * Adds elements to {@link Section#getBlocks() blocks} list.
     * @param elements An iterable of blocks elements
     * @return {@code this} builder for use in a chained invocation
     */
    public final Section.Builder addAllBlocks(Iterable<? extends Block<?>> elements) {
      for (Block<?> element : elements) {
        this.blocks.add(Objects.requireNonNull(element, "blocks element"));
      }
      return (Section.Builder) this;
    }

    /**
     * Initializes the value for the {@link Section#getOrder() order} attribute.
     * <p><em>If not set, this attribute will have a default value as returned by the initializer of {@link Section#getOrder() order}.</em>
     * @param order The value for order 
     * @return {@code this} builder for use in a chained invocation
     */
    public final Section.Builder order(int order) {
      this.order = order;
      optBits |= OPT_BIT_ORDER;
      return (Section.Builder) this;
    }

    /**
     * Builds a new {@link Section Section}.
     * @return An immutable instance of Section
     * @throws java.lang.IllegalStateException if any required attributes are missing
     */
    public Section build() {
      if (initBits != 0) {
        throw new IllegalStateException(formatRequiredAttributesMessage());
      }
      return new ImmutableSection(this);
    }

    private boolean orderIsSet() {
      return (optBits & OPT_BIT_ORDER) != 0;
    }

    private String formatRequiredAttributesMessage() {
      List<String> attributes = new ArrayList<>();
      if ((initBits & INIT_BIT_TITLE) != 0) attributes.add("title");
      return "Cannot build Section, some of required attributes are not set " + attributes;
    }
  }

  private static <T> List<T> createSafeList(Iterable<? extends T> iterable, boolean checkNulls, boolean skipNulls) {
    ArrayList<T> list;
    if (iterable instanceof Collection<?>) {
      int size = ((Collection<?>) iterable).size();
      if (size == 0) return Collections.emptyList();
      list = new ArrayList<>(size);
    } else {
      list = new ArrayList<>();
    }
    for (T element : iterable) {
      if (skipNulls && element == null) continue;
      if (checkNulls) Objects.requireNonNull(element, "element");
      list.add(element);
    }
    return list;
  }

  private static <T> List<T> createUnmodifiableList(boolean clone, List<? extends T> list) {
    switch(list.size()) {
    case 0: return Collections.emptyList();
    case 1: return Collections.singletonList(list.get(0));
    default:
      if (clone) {
        return Collections.unmodifiableList(new ArrayList<>(list));
      } else {
        if (list instanceof ArrayList<?>) {
          ((ArrayList<?>) list).trimToSize();
        }
        return Collections.unmodifiableList(list);
      }
    }
  }
}
