/*
 * Copyright 2000-2009 JetBrains s.r.o.
 *
 * 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.
 */

package com.intellij.formatting;

import org.jetbrains.annotations.Nullable;

import java.util.*;

public class WrapImpl extends Wrap {
  /**
   * The block where the wrap needs to happen if the CHOP wrap mode is used and the chain of blocks exceeds the right margin.
   */
  private LeafBlockWrapper myChopStartBlock = null;
  private int myWrapOffset = -1;
  private int myFlags;

  private static final Set<WrapImpl> emptyParentsSet = Collections.emptySet();
  private Set<WrapImpl> myParents = emptyParentsSet;
  private Map<WrapImpl, Collection<LeafBlockWrapper>> myIgnoredWraps;

  private static final int IGNORE_PARENT_WRAPS_MASK = 1;
  private static final int ACTIVE_MASK = 2;
  private static final int WRAP_FIRST_ELEMENT_MASK = 4;
  private static final int TYPE_MASK = 0x18;
  private static final int TYPE_SHIFT = 3;
  private static final Type[] myTypes = Type.values();


  public boolean isChildOf(@Nullable final WrapImpl wrap, LeafBlockWrapper leaf) {
    if (getIgnoreParentWraps()) return false;
    if (leaf != null && myIgnoredWraps != null) {
      Collection<LeafBlockWrapper> leaves = myIgnoredWraps.get(wrap);
      if (leaves != null && leaves.contains(leaf)) {
        return false;
      }
    }
    for (WrapImpl parent : myParents) {
      if (parent == wrap) return true;
      if (parent.isChildOf(wrap, leaf)) return true;
    }
    return false;
  }

  /**
   * Allows to register given wrap as a parent of the current wrap.
   * <p/>
   * {@code 'Parent'} wrap registration here means that {@link #isChildOf(WrapImpl, LeafBlockWrapper)} returns
   * {@code 'true'} if given wrap is used as a {@code 'parent'} argument.
   *
   * @param parent    parent wrap to register for the current wrap
   */
  void registerParent(@Nullable WrapImpl parent) {
    if (parent == this) return;
    if (parent == null) return;
    if (parent.isChildOf(this, null)) return;
    if (myParents == emptyParentsSet) myParents = new HashSet<>(5);
    myParents.add(parent);
  }

  /**
   * Resets the following state of the current wrap object:
   * <ul>
   *   <li>'{@link #getChopStartBlock() firstEntry}' property value is set to {@code null};</li>
   *   <li>'{@link #getWrapOffset() firstPosition}' property value is set to {@code '-1'};</li>
   *   <li>'{@link #isActive() isActive}' property value is set to {@code 'false'};</li>
   * </ul>
   */
  public void reset() {
    myChopStartBlock = null;
    myWrapOffset = -1;
    myFlags &=~ ACTIVE_MASK;
  }

  /**
   * Allows to check if single wrap is {@link #registerParent(WrapImpl) registered} for the current wrap and return
   * it in case of success.
   *
   * @return    single wrap registered as a parent of the current wrap if any;
   *            {@code null} if no wraps or more than one wrap is registered as a parent for the current wrap
   */
  public WrapImpl getParent(){
    if (myParents != null && myParents.size() == 1) {
      return myParents.iterator().next();
    }

    return null;
  }

  public final boolean getIgnoreParentWraps() {
    return (myFlags & IGNORE_PARENT_WRAPS_MASK) != 0;
  }

  /**
   * Allows to mark given wrap as {@code 'ignored'} for the given block. I.e. 'false' will be returned
   * for subsequent calls to {@link #isChildOf(WrapImpl, LeafBlockWrapper)} with the same arguments.
   *
   * @param wrap          target wrap
   * @param currentBlock  target block for which given wrap should be ignored
   */
  public void ignoreParentWrap(@Nullable final WrapImpl wrap, final LeafBlockWrapper currentBlock) {
    if (myIgnoredWraps == null) {
      myIgnoredWraps = new HashMap<>(5);
    }
    myIgnoredWraps.putIfAbsent(wrap, new HashSet<>(2));
    myIgnoredWraps.get(wrap).add(currentBlock);
  }

  public enum Type{
    DO_NOT_WRAP, WRAP_AS_NEEDED, CHOP_IF_NEEDED, WRAP_ALWAYS
  }

  public LeafBlockWrapper getChopStartBlock() {
    return myChopStartBlock;
  }

  /**
   * Performs the following changes at wrap object state:
   * <ul>
   *   <li>'{@link #getChopStartBlock() firstEntry}' property value is dropped (set to {@code null})</li>
   *   <li>'{@link #isActive() isActive}' property value is set (to {@code true})</li>
   * </ul>
   */
  public void setActive() {
    myChopStartBlock = null;
    myFlags |= ACTIVE_MASK;
  }

  /**
   * Applies given value to the '{@link #getWrapOffset() firstPosition}' property value if it's value is undefined at the moment
   * (has negative value).
   *
   * @param startOffset   new '{@link #getWrapOffset() firstPosition}' property value to use if current value is undefined (negative)
   */
  public void setWrapOffset(final int startOffset) {
    if (myWrapOffset < 0) {
      myWrapOffset = startOffset;
    }
  }

  /**
   * @return    '{@link #getWrapOffset() firstPosition}' property value defined previously via {@link #setWrapOffset(int)} if any;
   *            {@code '-1'} otherwise
   */
  public int getWrapOffset() {
    return myWrapOffset;
  }

  public WrapImpl(WrapType type, boolean wrapFirstElement) {
    Type myType;

    switch(type) {
        case NORMAL: myType = Type.WRAP_AS_NEEDED;break;
        case NONE: myType= Type.DO_NOT_WRAP;break;
        case ALWAYS: myType = Type.WRAP_ALWAYS; break;
        case CHOP_DOWN_IF_LONG:
        default: myType = Type.CHOP_IF_NEEDED;
    }

    myFlags |= (wrapFirstElement ? WRAP_FIRST_ELEMENT_MASK:0) | (myType.ordinal() << TYPE_SHIFT);
  }

  public final Type getType() {
    return myTypes[(myFlags & TYPE_MASK) >>> TYPE_SHIFT];
  }

  /**
   * Allows to check if current wrap object is configured to wrap first element. This property is defined at
   * {@link #WrapImpl(WrapType, boolean) constructor} during object initialization and can't be changed later.
   *
   * @return    {@code 'wrapFirstElement'} property value
   */
  public final boolean isWrapFirstElement() {
    return (myFlags & WRAP_FIRST_ELEMENT_MASK) != 0;
  }

  public void saveChopBlock(LeafBlockWrapper current) {
    if (myChopStartBlock == null) {
      myChopStartBlock = current;
    }
  }

  public final boolean isActive() {
    return (myFlags & ACTIVE_MASK) != 0;
  }

  public String toString() {
    return getType().toString();
  }
  
  /**
   * Allows to instruct current wrap to ignore all parent wraps, i.e. all calls to {@link #isChildOf(WrapImpl, LeafBlockWrapper)}
   * return {@code 'false'} after invocation of this method.
   */
  @Override
  public void ignoreParentWraps() {
    myFlags |= IGNORE_PARENT_WRAPS_MASK;
  }
}
