/*
 * Copyright (c) 2002 - 2014 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */

/*
 * CRTData.java
 *
 * Created on 7. Juni 2005, 11:48
 */

package com.ibm.wala.sourcepos;

import com.ibm.wala.sourcepos.InvalidRangeException.Cause;

/**
 * This class represents an entry in the CharacterRangeTable.
 *
 * @see CRTFlags
 * @see CRTable
 * @author Siegfried Weber
 * @author Juergen Graf &lt;juergen.graf@gmail.com&gt;
 */
public final class CRTData {

  private static final String WARN_INVALID_PC_RANGE =
      "Error at CRT entry %1$s: the program counter start index (%2$s) must be greater or equal than the end index (%3$s).";
  private static final String WARN_INVALID_START_LINE_NUMBER =
      "Error at CRT entry %1$s: the line number of the source start position must not be 0.";
  private static final String WARN_INVALID_START_COLUMN_NUMBER =
      "Error at CRT entry %1$s: the column number of the source start position not be 0.";
  private static final String WARN_INVALID_END_LINE_NUMBER =
      "Error at CRT entry %1$s: the line number of the source end position must not be 0.";
  private static final String WARN_INVALID_END_COLUMN_NUMBER =
      "Error at CRT entry %1$s: the column number of the source end position must not be 0.";
  private static final String WARN_END_BEFORE_START =
      "Error at CRT entry %1$s: the source end position (%3$s) is before the source start position (%2$s).";
  private static final String WARN_START_UNDEFINED =
      "Error at CRT entry %1$s: the source start position is undefined.";
  private static final String WARN_END_UNDEFINED =
      "Error at CRT entry %1$s: the source end position is undefined.";

  /** start index in the code array of the code attribute */
  private final int pc_start_index;
  /** end index in the code array of the code attribute */
  private final int pc_end_index;
  /** positions in the source file */
  private final Range source_positions;
  /**
   * flags
   *
   * @see CRTFlags
   */
  private final CRTFlags flags;

  /**
   * Creates a new instance of CRTData. {@code source_start_position} and {@code
   * source_end_position} can be {@code 0} to show that a range in the code array of the code
   * attribute has no source positions.
   *
   * @param pc_start_index start index in the code array of the code attribute as unsigned short
   * @param pc_end_index end index in the code array of the code attribute as unsigned short
   * @param source_start_position start position in the source file as unsigned int
   * @param source_end_position end position in the source file as unsigned int
   * @param flags flags defined in {@link CRTFlags}
   * @throws InvalidCRTDataException if a parameter violates one of the following conditions:
   *     <ul>
   *       <li>{@code pc_start_index < pc_end_index}
   *       <li>{@code source_start_position} is a valid position.
   *       <li>{@code source_end_position} is a valid position.
   *       <li>{@code source_start_position <= source_end_position}
   *       <li>{@code source_start_position} and {@code source_end_position} must be a valid range.
   *       <li>{@code flags} must contain valid flags.
   *     </ul>
   */
  CRTData(
      short pc_start_index,
      short pc_end_index,
      int source_start_position,
      int source_end_position,
      short flags)
      throws InvalidCRTDataException {
    this.pc_start_index = pc_start_index & 0xFFFF;
    this.pc_end_index = pc_end_index & 0xFFFF;
    if (pc_start_index > pc_end_index)
      throw new InvalidCRTDataException(
          WARN_INVALID_PC_RANGE, this.pc_start_index, this.pc_end_index);

    Position source_start = null;
    try {
      source_start = new Position(source_start_position);
    } catch (InvalidPositionException e) {
      switch (e.getThisCause()) {
        case LINE_NUMBER_ZERO:
          throw new InvalidCRTDataException(WARN_INVALID_START_LINE_NUMBER);
        case COLUMN_NUMBER_ZERO:
          throw new InvalidCRTDataException(WARN_INVALID_START_COLUMN_NUMBER);
        default:
          assert false;
      }
    }

    Position source_end = null;
    try {
      source_end = new Position(source_end_position);
    } catch (InvalidPositionException e) {
      switch (e.getThisCause()) {
        case LINE_NUMBER_ZERO:
          throw new InvalidCRTDataException(WARN_INVALID_END_LINE_NUMBER);
        case COLUMN_NUMBER_ZERO:
          throw new InvalidCRTDataException(WARN_INVALID_END_COLUMN_NUMBER);
        default:
          assert false;
      }
    }

    Range range = null;
    try {
      range = new Range(source_start, source_end);
    } catch (InvalidRangeException e) {
      final Cause cause = e.getThisCause();
      switch (cause) {
        case END_BEFORE_START:
          throw new InvalidCRTDataException(
              WARN_END_BEFORE_START, source_start.toString(), source_end.toString());
        case START_UNDEFINED:
          throw new InvalidCRTDataException(WARN_START_UNDEFINED);
        case END_UNDEFINED:
          throw new InvalidCRTDataException(WARN_END_UNDEFINED);
        default:
          throw new UnsupportedOperationException(
              String.format("cannot convert %s into an InvalidCRTDataException", cause));
      }
    } finally {
      this.source_positions = range;
    }

    this.flags = new CRTFlags(flags);
  }

  public final CRTFlags getFlags() {
    return this.flags;
  }

  /**
   * Tests whether the given index lies within the range of this data.
   *
   * @param pc the index to test
   * @return whether the given index lies within the range of this data or not.
   */
  public final boolean isInRange(int pc) {
    return pc_start_index <= pc && pc <= pc_end_index;
  }

  /**
   * Tests whether the given data is consistently with this data. To be consistently with another
   * data this data has to be equal or more precise. Otherwise the datas are contradictory.
   *
   * @param d the data to test with
   * @return whether the given data is consistently.
   */
  public final boolean matches(CRTData d) {
    return d != null && (isMorePrecise(d) || d.isMorePrecise(this));
  }

  /**
   * Tests whether this data is equal to or more precise than the given data. This data is equal or
   * more precise if the program counter range and the source range lie within the range of the
   * given data. If this data or the parameter have no source positions, only program counter range
   * decides.
   *
   * @param d the data to test with
   * @return whether this data is equal to or more precise than the given one.
   */
  public final boolean isMorePrecise(CRTData d) {
    return d != null
        && pc_start_index >= d.pc_start_index
        && pc_end_index <= d.pc_end_index
        && (source_positions.isWithin(d.source_positions) || hasNoPosition() || d.hasNoPosition());
  }

  /**
   * Returns {@code true} if this data has no source position.
   *
   * @return {@code true} if this data has no source position.
   */
  private boolean hasNoPosition() {
    return source_positions.isUndefined();
  }

  /**
   * Returns the source positions.
   *
   * @return The returned array consists of four positions: the start line number, the start column
   *     number, the end line number, the end column number
   */
  public final Range getSourceInfo() {
    return source_positions;
  }

  @Override
  public String toString() {
    return "(Range [pc]: "
        + pc_start_index
        + '-'
        + pc_end_index
        + ") => "
        + source_positions.toString();
  }
}
