/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * 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.android.dx.dex.file;

import com.android.dex.util.ExceptionWithContext;
import com.android.dx.util.AnnotatedOutput;

/** An item in a Dalvik file which is referenced by absolute offset. */
public abstract class OffsettedItem extends Item implements Comparable<OffsettedItem> {
  /** {@code > 0;} alignment requirement */
  private final int alignment;

  /**
   * {@code >= -1;} the size of this instance when written, in bytes, or {@code -1} if not yet known
   */
  private int writeSize;

  /** {@code null-ok;} section the item was added to, or {@code null} if not yet added */
  private Section addedTo;

  /**
   * {@code >= -1;} assigned offset of the item from the start of its section, or {@code -1} if not
   * yet assigned
   */
  private int offset;

  /**
   * Constructs an instance. The offset is initially unassigned.
   *
   * @param alignment {@code > 0;} output alignment requirement; must be a power of 2
   * @param writeSize {@code >= -1;} the size of this instance when written, in bytes, or {@code -1}
   *     if not immediately known
   */
  public OffsettedItem(int alignment, int writeSize) {
    Section.validateAlignment(alignment);

    if (writeSize < -1) {
      throw new IllegalArgumentException("writeSize < -1");
    }

    this.alignment = alignment;
    this.writeSize = writeSize;
    this.addedTo = null;
    this.offset = -1;
  }

  /**
   * Gets the absolute offset of the given item, returning {@code 0} if handed {@code null}.
   *
   * @param item {@code null-ok;} the item in question
   * @return {@code >= 0;} the item's absolute offset, or {@code 0} if {@code item == null}
   */
  public static int getAbsoluteOffsetOr0(OffsettedItem item) {
    if (item == null) {
      return 0;
    }

    return item.getAbsoluteOffset();
  }

  /**
   * {@inheritDoc}
   *
   * <p>Comparisons for this class are defined to be type-major (if the types don't match then the
   * objects are not equal), with {@link #compareTo0} deciding same-type comparisons.
   */
  @Override
  public final boolean equals(Object other) {
    if (this == other) {
      return true;
    }

    OffsettedItem otherItem = (OffsettedItem) other;
    ItemType thisType = itemType();
    ItemType otherType = otherItem.itemType();

    if (thisType != otherType) {
      return false;
    }

    return (compareTo0(otherItem) == 0);
  }

  /**
   * {@inheritDoc}
   *
   * <p>Comparisons for this class are defined to be class-major (if the classes don't match then
   * the objects are not equal), with {@link #compareTo0} deciding same-class comparisons.
   */
  @Override
  public final int compareTo(OffsettedItem other) {
    if (this == other) {
      return 0;
    }

    ItemType thisType = itemType();
    ItemType otherType = other.itemType();

    if (thisType != otherType) {
      return thisType.compareTo(otherType);
    }

    return compareTo0(other);
  }

  /**
   * Sets the write size of this item. This may only be called once per instance, and only if the
   * size was unknown upon instance creation.
   *
   * @param writeSize {@code > 0;} the write size, in bytes
   */
  public final void setWriteSize(int writeSize) {
    if (writeSize < 0) {
      throw new IllegalArgumentException("writeSize < 0");
    }

    if (this.writeSize >= 0) {
      throw new UnsupportedOperationException("writeSize already set");
    }

    this.writeSize = writeSize;
  }

  /**
   * {@inheritDoc}
   *
   * @throws UnsupportedOperationException thrown if the write size is not yet known
   */
  @Override
  public final int writeSize() {
    if (writeSize < 0) {
      throw new UnsupportedOperationException("writeSize is unknown");
    }

    return writeSize;
  }

  /** {@inheritDoc} */
  @Override
  public final void writeTo(DexFile file, AnnotatedOutput out) {
    out.alignTo(alignment);

    try {
      if (writeSize < 0) {
        throw new UnsupportedOperationException("writeSize is unknown");
      }
      out.assertCursor(getAbsoluteOffset());
    } catch (RuntimeException ex) {
      throw ExceptionWithContext.withContext(ex, "...while writing " + this);
    }

    writeTo0(file, out);
  }

  /**
   * Gets the relative item offset. The offset is from the start of the section which the instance
   * was written to.
   *
   * @return {@code >= 0;} the offset
   * @throws RuntimeException thrown if the offset is not yet known
   */
  public final int getRelativeOffset() {
    if (offset < 0) {
      throw new RuntimeException("offset not yet known");
    }

    return offset;
  }

  /**
   * Gets the absolute item offset. The offset is from the start of the file which the instance was
   * written to.
   *
   * @return {@code >= 0;} the offset
   * @throws RuntimeException thrown if the offset is not yet known
   */
  public final int getAbsoluteOffset() {
    if (offset < 0) {
      throw new RuntimeException("offset not yet known");
    }

    return addedTo.getAbsoluteOffset(offset);
  }

  /**
   * Indicates that this item has been added to the given section at the given offset. It is only
   * valid to call this method once per instance.
   *
   * @param addedTo {@code non-null;} the section this instance has been added to
   * @param offset {@code >= 0;} the desired offset from the start of the section where this
   *     instance was placed
   * @return {@code >= 0;} the offset that this instance should be placed at in order to meet its
   *     alignment constraint
   */
  public final int place(Section addedTo, int offset) {
    if (addedTo == null) {
      throw new NullPointerException("addedTo == null");
    }

    if (offset < 0) {
      throw new IllegalArgumentException("offset < 0");
    }

    if (this.addedTo != null) {
      throw new RuntimeException("already written");
    }

    int mask = alignment - 1;
    offset = (offset + mask) & ~mask;

    this.addedTo = addedTo;
    this.offset = offset;

    place0(addedTo, offset);

    return offset;
  }

  /**
   * Gets the alignment requirement of this instance. An instance should only be written when so
   * aligned.
   *
   * @return {@code > 0;} the alignment requirement; must be a power of 2
   */
  public final int getAlignment() {
    return alignment;
  }

  /**
   * Gets the absolute offset of this item as a string, suitable for including in annotations.
   *
   * @return {@code non-null;} the offset string
   */
  public final String offsetString() {
    return '[' + Integer.toHexString(getAbsoluteOffset()) + ']';
  }

  /**
   * Gets a short human-readable string representing this instance.
   *
   * @return {@code non-null;} the human form
   */
  public abstract String toHuman();

  /**
   * Compares this instance to another which is guaranteed to be of the same class. The default
   * implementation of this method is to throw an exception (unsupported operation). If a particular
   * class needs to actually sort, then it should override this method.
   *
   * @param other {@code non-null;} instance to compare to
   * @return {@code -1}, {@code 0}, or {@code 1}, depending on the sort order of this instance and
   *     the other
   */
  protected int compareTo0(OffsettedItem other) {
    throw new UnsupportedOperationException("unsupported");
  }

  /**
   * Does additional work required when placing an instance. The default implementation of this
   * method is a no-op. If a particular class needs to do something special, then it should override
   * this method. In particular, if this instance did not know its write size up-front, then this
   * method is responsible for setting it.
   *
   * @param addedTo {@code non-null;} the section this instance has been added to
   * @param offset {@code >= 0;} the offset from the start of the section where this instance was
   *     placed
   */
  protected void place0(Section addedTo, int offset) {
    // This space intentionally left blank.
  }

  /**
   * Performs the actual write of the contents of this instance to the given data section. This is
   * called by {@link #writeTo}, which will have taken care of ensuring alignment.
   *
   * @param file {@code non-null;} the file to use for reference
   * @param out {@code non-null;} where to write to
   */
  protected abstract void writeTo0(DexFile file, AnnotatedOutput out);
}
