/*
 * This file is part of adventure, licensed under the MIT License.
 *
 * Copyright (c) 2017-2020 KyoriPowered
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package net.kyori.adventure.text;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * An abstract implementation of a component builder.
 *
 * @param <C> the component type
 * @param <B> the builder type
 */
abstract class AbstractComponentBuilder<C extends BuildableComponent<C, B>, B extends ComponentBuilder<C, B>> implements ComponentBuilder<C, B> {
  /**
   * The list of children.
   *
   * <p>This list is set to {@link AbstractComponent#EMPTY_COMPONENT_LIST an empty list of components}
   * by default to prevent unnecessary list creation for components with no children.</p>
   */
  protected List<Component> children = AbstractComponent.EMPTY_COMPONENT_LIST;
  /*
   * We maintain two separate fields here - a style, and style builder. If we're creating this component builder from
   * another component, or someone provides a style via style(Style), then we don't need a builder - unless someone later
   * calls one of the style modification methods in this builder, at which time we'll convert 'style' to a style builder.
   */
  /**
   * The style.
   */
  private @Nullable Style style;
  /**
   * The style builder.
   */
  private [email protected] Builder styleBuilder;

  protected AbstractComponentBuilder() {
  }

  protected AbstractComponentBuilder(final @NonNull C component) {
    final List<Component> children = component.children();
    if(!children.isEmpty()) {
      this.children = new ArrayList<>(children);
    }
    if(component.hasStyling()) {
      this.style = component.style();
    }
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B append(final @NonNull Component component) {
    if(component == TextComponent.empty()) return (B) this;
    this.prepareChildren();
    this.children.add(component);
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B append(final @NonNull [email protected] components) {
    boolean prepared = false;
    for(int i = 0, length = components.length; i < length; i++) {
      final Component component = components[i];
      if(component != TextComponent.empty()) {
        if(!prepared) {
          this.prepareChildren();
          prepared = true;
        }
        this.children.add(component);
      }
    }
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B append(final @NonNull Iterable<? extends Component> components) {
    boolean prepared = false;
    for(final Component component : components) {
      if(component != TextComponent.empty()) {
        if(!prepared) {
          this.prepareChildren();
          prepared = true;
        }
        this.children.add(component);
      }
    }
    return (B) this;
  }

  private void prepareChildren() {
    if(this.children == AbstractComponent.EMPTY_COMPONENT_LIST) {
      this.children = new ArrayList<>();
    }
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B applyDeep(final @NonNull Consumer<? super ComponentBuilder<?, ?>> consumer) {
    this.apply(consumer);
    if(this.children == AbstractComponent.EMPTY_COMPONENT_LIST) {
      return (B) this;
    }
    final ListIterator<Component> it = this.children.listIterator();
    while(it.hasNext()) {
      final Component child = it.next();
      if(!(child instanceof BuildableComponent<?, ?>)) {
        continue;
      }
      final ComponentBuilder<?, ?> childBuilder = ((BuildableComponent<?, ?>) child).toBuilder();
      childBuilder.applyDeep(consumer);
      it.set(childBuilder.build());
    }
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B mapChildren(final @NonNull Function<BuildableComponent<? ,?>, ? extends BuildableComponent<? ,?>> function) {
    if(this.children == AbstractComponent.EMPTY_COMPONENT_LIST) {
      return (B) this;
    }
    final ListIterator<Component> it = this.children.listIterator();
    while(it.hasNext()) {
      final Component child = it.next();
      if(!(child instanceof BuildableComponent<?, ?>)) {
        continue;
      }
      final BuildableComponent<?, ?> mappedChild = function.apply((BuildableComponent<?, ?>) child);
      if(child == mappedChild) {
        continue;
      }
      it.set(mappedChild);
    }
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B mapChildrenDeep(final @NonNull Function<BuildableComponent<? ,?>, ? extends BuildableComponent<? ,?>> function) {
    if(this.children == AbstractComponent.EMPTY_COMPONENT_LIST) {
      return (B) this;
    }
    final ListIterator<Component> it = this.children.listIterator();
    while(it.hasNext()) {
      final Component child = it.next();
      if(!(child instanceof BuildableComponent<?, ?>)) {
        continue;
      }
      final BuildableComponent<?, ?> mappedChild = function.apply((BuildableComponent<?, ?>) child);
      if(mappedChild.children().isEmpty()) {
        if(child == mappedChild) {
          continue;
        }
        it.set(mappedChild);
      } else {
        final ComponentBuilder<?, ?> builder = mappedChild.toBuilder();
        builder.mapChildrenDeep(function);
        it.set(builder.build());
      }
    }
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B style(final @NonNull Style style) {
    this.style = style;
    this.styleBuilder = null;
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B style(final @NonNull Consumer<Style.Builder> consumer) {
    consumer.accept(this.styleBuilder());
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B color(final @Nullable TextColor color) {
    this.styleBuilder().color(color);
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B colorIfAbsent(final @Nullable TextColor color) {
    this.styleBuilder().colorIfAbsent(color);
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B decoration(final @NonNull TextDecoration decoration, final [email protected] State state) {
    this.styleBuilder().decoration(decoration, state);
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B clickEvent(final @Nullable ClickEvent event) {
    this.styleBuilder().clickEvent(event);
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B hoverEvent(final @Nullable HoverEvent<?> event) {
    this.styleBuilder().hoverEvent(event);
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B insertion(final @Nullable String insertion) {
    this.styleBuilder().insertion(insertion);
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B mergeStyle(final @NonNull Component that, final @NonNull Set<Style.Merge> merges) {
    this.styleBuilder().merge(that.style(), merges);
    return (B) this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public @NonNull B resetStyle() {
    this.style = null;
    this.styleBuilder = null;
    return (B) this;
  }

  private [email protected] Builder styleBuilder() {
    if(this.styleBuilder == null) {
      if(this.style != null) {
        this.styleBuilder = this.style.toBuilder();
        this.style = null;
      } else {
        this.styleBuilder = Style.builder();
      }
    }
    return this.styleBuilder;
  }

  protected final boolean hasStyle() {
    return this.styleBuilder != null || this.style != null;
  }

  protected @NonNull Style buildStyle() {
    if(this.styleBuilder != null) {
      return this.styleBuilder.build();
    } else if(this.style != null) {
      return this.style;
    } else {
      return Style.empty();
    }
  }
}