/**
 * Title: FeatFormulaErr2 (Formula Evaluation Shared Feature) common record part
 *
 * <p>This record part specifies Formula Evaluation & Error Ignoring data for a sheet, stored as
 * part of a Shared Feature. It can be found in records such as {@link FeatRecord}. For the full
 * meanings of the flags, see pages 669 and 670 of the Excel binary file format documentation.
 */
public final class FeatFormulaErr2 implements SharedFeature {
  static BitField checkCalculationErrors = BitFieldFactory.getInstance(0x01);
  static BitField checkEmptyCellRef = BitFieldFactory.getInstance(0x02);
  static BitField checkNumbersAsText = BitFieldFactory.getInstance(0x04);
  static BitField checkInconsistentRanges = BitFieldFactory.getInstance(0x08);
  static BitField checkInconsistentFormulas = BitFieldFactory.getInstance(0x10);
  static BitField checkDateTimeFormats = BitFieldFactory.getInstance(0x20);
  static BitField checkUnprotectedFormulas = BitFieldFactory.getInstance(0x40);
  static BitField performDataValidation = BitFieldFactory.getInstance(0x80);

  /** What errors we should ignore */
  private int errorCheck;

  public FeatFormulaErr2() {}

  public FeatFormulaErr2(RecordInputStream in) {
    errorCheck = in.readInt();
  }

  public String toString() {
    StringBuffer buffer = new StringBuffer();
    buffer.append(" [FEATURE FORMULA ERRORS]\n");
    buffer.append("  checkCalculationErrors    = ");
    buffer.append("  checkEmptyCellRef         = ");
    buffer.append("  checkNumbersAsText        = ");
    buffer.append("  checkInconsistentRanges   = ");
    buffer.append("  checkInconsistentFormulas = ");
    buffer.append("  checkDateTimeFormats      = ");
    buffer.append("  checkUnprotectedFormulas  = ");
    buffer.append("  performDataValidation     = ");
    buffer.append(" [/FEATURE FORMULA ERRORS]\n");
    return buffer.toString();
  }

  public void serialize(LittleEndianOutput out) {
    out.writeInt(errorCheck);
  }

  public int getDataSize() {
    return 4;
  }

  public int _getRawErrorCheckValue() {
    return errorCheck;
  }

  public boolean getCheckCalculationErrors() {
    return checkCalculationErrors.isSet(errorCheck);
  }

  public void setCheckCalculationErrors(boolean checkCalculationErrors) {
    FeatFormulaErr2.checkCalculationErrors.setBoolean(errorCheck, checkCalculationErrors);
  }

  public boolean getCheckEmptyCellRef() {
    return checkEmptyCellRef.isSet(errorCheck);
  }

  public void setCheckEmptyCellRef(boolean checkEmptyCellRef) {
    FeatFormulaErr2.checkEmptyCellRef.setBoolean(errorCheck, checkEmptyCellRef);
  }

  public boolean getCheckNumbersAsText() {
    return checkNumbersAsText.isSet(errorCheck);
  }

  public void setCheckNumbersAsText(boolean checkNumbersAsText) {
    FeatFormulaErr2.checkNumbersAsText.setBoolean(errorCheck, checkNumbersAsText);
  }

  public boolean getCheckInconsistentRanges() {
    return checkInconsistentRanges.isSet(errorCheck);
  }

  public void setCheckInconsistentRanges(boolean checkInconsistentRanges) {
    FeatFormulaErr2.checkInconsistentRanges.setBoolean(errorCheck, checkInconsistentRanges);
  }

  public boolean getCheckInconsistentFormulas() {
    return checkInconsistentFormulas.isSet(errorCheck);
  }

  public void setCheckInconsistentFormulas(boolean checkInconsistentFormulas) {
    FeatFormulaErr2.checkInconsistentFormulas.setBoolean(errorCheck, checkInconsistentFormulas);
  }

  public boolean getCheckDateTimeFormats() {
    return checkDateTimeFormats.isSet(errorCheck);
  }

  public void setCheckDateTimeFormats(boolean checkDateTimeFormats) {
    FeatFormulaErr2.checkDateTimeFormats.setBoolean(errorCheck, checkDateTimeFormats);
  }

  public boolean getCheckUnprotectedFormulas() {
    return checkUnprotectedFormulas.isSet(errorCheck);
  }

  public void setCheckUnprotectedFormulas(boolean checkUnprotectedFormulas) {
    FeatFormulaErr2.checkUnprotectedFormulas.setBoolean(errorCheck, checkUnprotectedFormulas);
  }

  public boolean getPerformDataValidation() {
    return performDataValidation.isSet(errorCheck);
  }

  public void setPerformDataValidation(boolean performDataValidation) {
    FeatFormulaErr2.performDataValidation.setBoolean(errorCheck, performDataValidation);
  }
}
/**
 * Title: Unicode String
 *
 * <p>Description: Unicode String - just standard fields that are in several records. It is
 * considered more desirable then repeating it in all of them.
 *
 * <p>This is often called a XLUnicodeRichExtendedString in MS documentation.
 *
 * <p>REFERENCE: PG 264 Microsoft Excel 97 Developer's Kit (ISBN: 1-57231-498-2)
 *
 * <p>REFERENCE: PG 951 Excel Binary File Format (.xls) Structure Specification v20091214
 */
public class UnicodeString
    implements Comparable<
        UnicodeString> { // TODO - make this final when the compatibility version is removed
  private short field_1_charCount;
  private byte field_2_optionflags;
  private String field_3_string;
  private List<FormatRun> field_4_format_runs;
  private ExtRst field_5_ext_rst;
  private static final BitField highByte = BitFieldFactory.getInstance(0x1);
  // 0x2 is reserved
  private static final BitField extBit = BitFieldFactory.getInstance(0x4);
  private static final BitField richText = BitFieldFactory.getInstance(0x8);

  public static class FormatRun implements Comparable<FormatRun> {
    final short _character;
    short _fontIndex;

    public FormatRun(short character, short fontIndex) {
      this._character = character;
      this._fontIndex = fontIndex;
    }

    public FormatRun(LittleEndianInput in) {
      this(in.readShort(), in.readShort());
    }

    public short getCharacterPos() {
      return _character;
    }

    public short getFontIndex() {
      return _fontIndex;
    }

    public boolean equals(Object o) {
      if (!(o instanceof FormatRun)) {
        return false;
      }
      FormatRun other = (FormatRun) o;

      return _character == other._character && _fontIndex == other._fontIndex;
    }

    public int compareTo(FormatRun r) {
      if (_character == r._character && _fontIndex == r._fontIndex) {
        return 0;
      }
      if (_character == r._character) {
        return _fontIndex - r._fontIndex;
      }
      return _character - r._character;
    }

    public String toString() {
      return "character=" + _character + ",fontIndex=" + _fontIndex;
    }

    public void serialize(LittleEndianOutput out) {
      out.writeShort(_character);
      out.writeShort(_fontIndex);
    }
  }

  // See page 681
  public static class ExtRst implements Comparable<ExtRst> {
    private short reserved;

    // This is a Phs (see page 881)
    private short formattingFontIndex;
    private short formattingOptions;

    // This is a RPHSSub (see page 894)
    private int numberOfRuns;
    private String phoneticText;

    // This is an array of PhRuns (see page 881)
    private PhRun[] phRuns;
    // Sometimes there's some cruft at the end
    private byte[] extraData;

    private void populateEmpty() {
      reserved = 1;
      phoneticText = "";
      phRuns = new PhRun[0];
      extraData = new byte[0];
    }

    protected ExtRst() {
      populateEmpty();
    }

    protected ExtRst(LittleEndianInput in, int expectedLength) {
      reserved = in.readShort();

      // Old style detection (Reserved = 0xFF)
      if (reserved == -1) {
        populateEmpty();
        return;
      }

      // Spot corrupt records
      if (reserved != 1) {
        System.err.println(
            "Warning - ExtRst was has wrong magic marker, expecting 1 but found "
                + reserved
                + " - ignoring");
        // Grab all the remaining data, and ignore it
        for (int i = 0; i < expectedLength - 2; i++) {
          in.readByte();
        }
        // And make us be empty
        populateEmpty();
        return;
      }

      // Carry on reading in as normal
      short stringDataSize = in.readShort();

      formattingFontIndex = in.readShort();
      formattingOptions = in.readShort();

      // RPHSSub
      numberOfRuns = in.readUShort();
      short length1 = in.readShort();
      // No really. Someone clearly forgot to read
      //  the docs on their datastructure...
      short length2 = in.readShort();
      // And sometimes they write out garbage :(
      if (length1 == 0 && length2 > 0) {
        length2 = 0;
      }
      if (length1 != length2) {
        throw new IllegalStateException(
            "The two length fields of the Phonetic Text don't agree! "
                + length1
                + " vs "
                + length2);
      }
      phoneticText = StringUtil.readUnicodeLE(in, length1);

      int runData = stringDataSize - 4 - 6 - (2 * phoneticText.length());
      int numRuns = (runData / 6);
      phRuns = new PhRun[numRuns];
      for (int i = 0; i < phRuns.length; i++) {
        phRuns[i] = new PhRun(in);
      }

      int extraDataLength = runData - (numRuns * 6);
      if (extraDataLength < 0) {
        System.err.println("Warning - ExtRst overran by " + (0 - extraDataLength) + " bytes");
        extraDataLength = 0;
      }
      extraData = new byte[extraDataLength];
      for (int i = 0; i < extraData.length; i++) {
        extraData[i] = in.readByte();
      }
    }
    /** Returns our size, excluding our 4 byte header */
    protected int getDataSize() {
      return 4 + 6 + (2 * phoneticText.length()) + (6 * phRuns.length) + extraData.length;
    }

    protected void serialize(ContinuableRecordOutput out) {
      int dataSize = getDataSize();

      out.writeContinueIfRequired(8);
      out.writeShort(reserved);
      out.writeShort(dataSize);
      out.writeShort(formattingFontIndex);
      out.writeShort(formattingOptions);

      out.writeContinueIfRequired(6);
      out.writeShort(numberOfRuns);
      out.writeShort(phoneticText.length());
      out.writeShort(phoneticText.length());

      out.writeContinueIfRequired(phoneticText.length() * 2);
      StringUtil.putUnicodeLE(phoneticText, out);

      for (int i = 0; i < phRuns.length; i++) {
        phRuns[i].serialize(out);
      }

      out.write(extraData);
    }

    public boolean equals(Object obj) {
      if (!(obj instanceof ExtRst)) {
        return false;
      }
      ExtRst other = (ExtRst) obj;
      return (compareTo(other) == 0);
    }

    public int compareTo(ExtRst o) {
      int result;

      result = reserved - o.reserved;
      if (result != 0) return result;
      result = formattingFontIndex - o.formattingFontIndex;
      if (result != 0) return result;
      result = formattingOptions - o.formattingOptions;
      if (result != 0) return result;
      result = numberOfRuns - o.numberOfRuns;
      if (result != 0) return result;

      result = phoneticText.compareTo(o.phoneticText);
      if (result != 0) return result;

      result = phRuns.length - o.phRuns.length;
      if (result != 0) return result;
      for (int i = 0; i < phRuns.length; i++) {
        result =
            phRuns[i].phoneticTextFirstCharacterOffset
                - o.phRuns[i].phoneticTextFirstCharacterOffset;
        if (result != 0) return result;
        result = phRuns[i].realTextFirstCharacterOffset - o.phRuns[i].realTextFirstCharacterOffset;
        if (result != 0) return result;
        result = phRuns[i].realTextFirstCharacterOffset - o.phRuns[i].realTextLength;
        if (result != 0) return result;
      }

      result = extraData.length - o.extraData.length;
      if (result != 0) return result;

      // If we get here, it's the same
      return 0;
    }

    protected ExtRst clone() {
      ExtRst ext = new ExtRst();
      ext.reserved = reserved;
      ext.formattingFontIndex = formattingFontIndex;
      ext.formattingOptions = formattingOptions;
      ext.numberOfRuns = numberOfRuns;
      ext.phoneticText = phoneticText;
      ext.phRuns = new PhRun[phRuns.length];
      for (int i = 0; i < ext.phRuns.length; i++) {
        ext.phRuns[i] =
            new PhRun(
                phRuns[i].phoneticTextFirstCharacterOffset,
                phRuns[i].realTextFirstCharacterOffset,
                phRuns[i].realTextLength);
      }
      return ext;
    }

    public short getFormattingFontIndex() {
      return formattingFontIndex;
    }

    public short getFormattingOptions() {
      return formattingOptions;
    }

    public int getNumberOfRuns() {
      return numberOfRuns;
    }

    public String getPhoneticText() {
      return phoneticText;
    }

    public PhRun[] getPhRuns() {
      return phRuns;
    }
  }

  public static class PhRun {
    private int phoneticTextFirstCharacterOffset;
    private int realTextFirstCharacterOffset;
    private int realTextLength;

    public PhRun(
        int phoneticTextFirstCharacterOffset,
        int realTextFirstCharacterOffset,
        int realTextLength) {
      this.phoneticTextFirstCharacterOffset = phoneticTextFirstCharacterOffset;
      this.realTextFirstCharacterOffset = realTextFirstCharacterOffset;
      this.realTextLength = realTextLength;
    }

    private PhRun(LittleEndianInput in) {
      phoneticTextFirstCharacterOffset = in.readUShort();
      realTextFirstCharacterOffset = in.readUShort();
      realTextLength = in.readUShort();
    }

    private void serialize(ContinuableRecordOutput out) {
      out.writeContinueIfRequired(6);
      out.writeShort(phoneticTextFirstCharacterOffset);
      out.writeShort(realTextFirstCharacterOffset);
      out.writeShort(realTextLength);
    }
  }

  private UnicodeString() {
    // Used for clone method.
  }

  public UnicodeString(String str) {
    setString(str);
  }

  public int hashCode() {
    int stringHash = 0;
    if (field_3_string != null) stringHash = field_3_string.hashCode();
    return field_1_charCount + stringHash;
  }

  /**
   * Our handling of equals is inconsistent with compareTo. The trouble is because we don't truely
   * understand rich text fields yet it's difficult to make a sound comparison.
   *
   * @param o The object to compare.
   * @return true if the object is actually equal.
   */
  public boolean equals(Object o) {
    if (!(o instanceof UnicodeString)) {
      return false;
    }
    UnicodeString other = (UnicodeString) o;

    // OK lets do this in stages to return a quickly, first check the actual string
    boolean eq =
        ((field_1_charCount == other.field_1_charCount)
            && (field_2_optionflags == other.field_2_optionflags)
            && field_3_string.equals(other.field_3_string));
    if (!eq) return false;

    // OK string appears to be equal but now lets compare formatting runs
    if ((field_4_format_runs == null) && (other.field_4_format_runs == null))
      // Strings are equal, and there are not formatting runs.
      return true;
    if (((field_4_format_runs == null) && (other.field_4_format_runs != null))
        || (field_4_format_runs != null) && (other.field_4_format_runs == null))
      // Strings are equal, but one or the other has formatting runs
      return false;

    // Strings are equal, so now compare formatting runs.
    int size = field_4_format_runs.size();
    if (size != other.field_4_format_runs.size()) return false;

    for (int i = 0; i < size; i++) {
      FormatRun run1 = field_4_format_runs.get(i);
      FormatRun run2 = other.field_4_format_runs.get(i);

      if (!run1.equals(run2)) return false;
    }

    // Well the format runs are equal as well!, better check the ExtRst data
    if (field_5_ext_rst == null && other.field_5_ext_rst == null) {
      // Good
    } else if (field_5_ext_rst != null && other.field_5_ext_rst != null) {
      int extCmp = field_5_ext_rst.compareTo(other.field_5_ext_rst);
      if (extCmp == 0) {
        // Good
      } else {
        return false;
      }
    } else {
      return false;
    }

    // Phew!! After all of that we have finally worked out that the strings
    // are identical.
    return true;
  }

  /**
   * construct a unicode string record and fill its fields, ID is ignored
   *
   * @param in the RecordInputstream to read the record from
   */
  public UnicodeString(RecordInputStream in) {
    field_1_charCount = in.readShort();
    field_2_optionflags = in.readByte();

    int runCount = 0;
    int extensionLength = 0;
    // Read the number of rich runs if rich text.
    if (isRichText()) {
      runCount = in.readShort();
    }
    // Read the size of extended data if present.
    if (isExtendedText()) {
      extensionLength = in.readInt();
    }

    boolean isCompressed = ((field_2_optionflags & 1) == 0);
    if (isCompressed) {
      field_3_string = in.readCompressedUnicode(getCharCount());
    } else {
      field_3_string = in.readUnicodeLEString(getCharCount());
    }

    if (isRichText() && (runCount > 0)) {
      field_4_format_runs = new ArrayList<FormatRun>(runCount);
      for (int i = 0; i < runCount; i++) {
        field_4_format_runs.add(new FormatRun(in));
      }
    }

    if (isExtendedText() && (extensionLength > 0)) {
      field_5_ext_rst = new ExtRst(new ContinuableRecordInput(in), extensionLength);
      if (field_5_ext_rst.getDataSize() + 4 != extensionLength) {
        System.err.println(
            "ExtRst was supposed to be "
                + extensionLength
                + " bytes long, but seems to actually be "
                + (field_5_ext_rst.getDataSize() + 4));
      }
    }
  }

  /**
   * get the number of characters in the string, as an un-wrapped int
   *
   * @return number of characters
   */
  public int getCharCount() {
    if (field_1_charCount < 0) {
      return field_1_charCount + 65536;
    }
    return field_1_charCount;
  }

  /**
   * get the number of characters in the string, wrapped as needed to fit within a short
   *
   * @return number of characters
   */
  public short getCharCountShort() {
    return field_1_charCount;
  }

  /**
   * set the number of characters in the string
   *
   * @param cc - number of characters
   */
  public void setCharCount(short cc) {
    field_1_charCount = cc;
  }

  /**
   * get the option flags which among other things return if this is a 16-bit or 8 bit string
   *
   * @return optionflags bitmask
   */
  public byte getOptionFlags() {
    return field_2_optionflags;
  }

  /**
   * set the option flags which among other things return if this is a 16-bit or 8 bit string
   *
   * @param of optionflags bitmask
   */
  public void setOptionFlags(byte of) {
    field_2_optionflags = of;
  }

  /** @return the actual string this contains as a java String object */
  public String getString() {
    return field_3_string;
  }

  /**
   * set the actual string this contains
   *
   * @param string the text
   */
  public void setString(String string) {
    field_3_string = string;
    setCharCount((short) field_3_string.length());
    // scan for characters greater than 255 ... if any are
    // present, we have to use 16-bit encoding. Otherwise, we
    // can use 8-bit encoding
    boolean useUTF16 = false;
    int strlen = string.length();

    for (int j = 0; j < strlen; j++) {
      if (string.charAt(j) > 255) {
        useUTF16 = true;
        break;
      }
    }
    if (useUTF16)
      // Set the uncompressed bit
      field_2_optionflags = highByte.setByte(field_2_optionflags);
    else field_2_optionflags = highByte.clearByte(field_2_optionflags);
  }

  public int getFormatRunCount() {
    if (field_4_format_runs == null) return 0;
    return field_4_format_runs.size();
  }

  public FormatRun getFormatRun(int index) {
    if (field_4_format_runs == null) {
      return null;
    }
    if (index < 0 || index >= field_4_format_runs.size()) {
      return null;
    }
    return field_4_format_runs.get(index);
  }

  private int findFormatRunAt(int characterPos) {
    int size = field_4_format_runs.size();
    for (int i = 0; i < size; i++) {
      FormatRun r = field_4_format_runs.get(i);
      if (r._character == characterPos) return i;
      else if (r._character > characterPos) return -1;
    }
    return -1;
  }

  /**
   * Adds a font run to the formatted string.
   *
   * <p>If a font run exists at the current charcter location, then it is replaced with the font run
   * to be added.
   */
  public void addFormatRun(FormatRun r) {
    if (field_4_format_runs == null) {
      field_4_format_runs = new ArrayList<FormatRun>();
    }

    int index = findFormatRunAt(r._character);
    if (index != -1) field_4_format_runs.remove(index);

    field_4_format_runs.add(r);
    // Need to sort the font runs to ensure that the font runs appear in
    // character order
    Collections.sort(field_4_format_runs);

    // Make sure that we now say that we are a rich string
    field_2_optionflags = richText.setByte(field_2_optionflags);
  }

  public Iterator<FormatRun> formatIterator() {
    if (field_4_format_runs != null) {
      return field_4_format_runs.iterator();
    }
    return null;
  }

  public void removeFormatRun(FormatRun r) {
    field_4_format_runs.remove(r);
    if (field_4_format_runs.size() == 0) {
      field_4_format_runs = null;
      field_2_optionflags = richText.clearByte(field_2_optionflags);
    }
  }

  public void clearFormatting() {
    field_4_format_runs = null;
    field_2_optionflags = richText.clearByte(field_2_optionflags);
  }

  public ExtRst getExtendedRst() {
    return this.field_5_ext_rst;
  }

  void setExtendedRst(ExtRst ext_rst) {
    if (ext_rst != null) {
      field_2_optionflags = extBit.setByte(field_2_optionflags);
    } else {
      field_2_optionflags = extBit.clearByte(field_2_optionflags);
    }
    this.field_5_ext_rst = ext_rst;
  }

  /**
   * Swaps all use in the string of one font index for use of a different font index. Normally only
   * called when fonts have been removed / re-ordered
   */
  public void swapFontUse(short oldFontIndex, short newFontIndex) {
    for (FormatRun run : field_4_format_runs) {
      if (run._fontIndex == oldFontIndex) {
        run._fontIndex = newFontIndex;
      }
    }
  }

  /**
   * unlike the real records we return the same as "getString()" rather than debug info
   *
   * @see #getDebugInfo()
   * @return String value of the record
   */
  public String toString() {
    return getString();
  }

  /**
   * return a character representation of the fields of this record
   *
   * @return String of output for biffviewer etc.
   */
  public String getDebugInfo() {
    StringBuffer buffer = new StringBuffer();

    buffer.append("[UNICODESTRING]\n");
    buffer
        .append("    .charcount       = ")
        .append(Integer.toHexString(getCharCount()))
        .append("\n");
    buffer
        .append("    .optionflags     = ")
        .append(Integer.toHexString(getOptionFlags()))
        .append("\n");
    buffer.append("    .string          = ").append(getString()).append("\n");
    if (field_4_format_runs != null) {
      for (int i = 0; i < field_4_format_runs.size(); i++) {
        FormatRun r = field_4_format_runs.get(i);
        buffer.append("      .format_run" + i + "          = ").append(r.toString()).append("\n");
      }
    }
    if (field_5_ext_rst != null) {
      buffer.append("    .field_5_ext_rst          = ").append("\n");
      buffer.append(field_5_ext_rst.toString()).append("\n");
    }
    buffer.append("[/UNICODESTRING]\n");
    return buffer.toString();
  }

  /**
   * Serialises out the String. There are special rules about where we can and can't split onto
   * Continue records.
   */
  public void serialize(ContinuableRecordOutput out) {
    int numberOfRichTextRuns = 0;
    int extendedDataSize = 0;
    if (isRichText() && field_4_format_runs != null) {
      numberOfRichTextRuns = field_4_format_runs.size();
    }
    if (isExtendedText() && field_5_ext_rst != null) {
      extendedDataSize = 4 + field_5_ext_rst.getDataSize();
    }

    // Serialise the bulk of the String
    // The writeString handles tricky continue stuff for us
    out.writeString(field_3_string, numberOfRichTextRuns, extendedDataSize);

    if (numberOfRichTextRuns > 0) {

      // This will ensure that a run does not split a continue
      for (int i = 0; i < numberOfRichTextRuns; i++) {
        if (out.getAvailableSpace() < 4) {
          out.writeContinue();
        }
        FormatRun r = field_4_format_runs.get(i);
        r.serialize(out);
      }
    }

    if (extendedDataSize > 0) {
      field_5_ext_rst.serialize(out);
    }
  }

  public int compareTo(UnicodeString str) {

    int result = getString().compareTo(str.getString());

    // As per the equals method lets do this in stages
    if (result != 0) return result;

    // OK string appears to be equal but now lets compare formatting runs
    if ((field_4_format_runs == null) && (str.field_4_format_runs == null))
      // Strings are equal, and there are no formatting runs.
      return 0;

    if ((field_4_format_runs == null) && (str.field_4_format_runs != null))
      // Strings are equal, but one or the other has formatting runs
      return 1;
    if ((field_4_format_runs != null) && (str.field_4_format_runs == null))
      // Strings are equal, but one or the other has formatting runs
      return -1;

    // Strings are equal, so now compare formatting runs.
    int size = field_4_format_runs.size();
    if (size != str.field_4_format_runs.size()) return size - str.field_4_format_runs.size();

    for (int i = 0; i < size; i++) {
      FormatRun run1 = field_4_format_runs.get(i);
      FormatRun run2 = str.field_4_format_runs.get(i);

      result = run1.compareTo(run2);
      if (result != 0) return result;
    }

    // Well the format runs are equal as well!, better check the ExtRst data
    if ((field_5_ext_rst == null) && (str.field_5_ext_rst == null)) return 0;
    if ((field_5_ext_rst == null) && (str.field_5_ext_rst != null)) return 1;
    if ((field_5_ext_rst != null) && (str.field_5_ext_rst == null)) return -1;

    result = field_5_ext_rst.compareTo(str.field_5_ext_rst);
    if (result != 0) return result;

    // Phew!! After all of that we have finally worked out that the strings
    // are identical.
    return 0;
  }

  private boolean isRichText() {
    return richText.isSet(getOptionFlags());
  }

  private boolean isExtendedText() {
    return extBit.isSet(getOptionFlags());
  }

  public Object clone() {
    UnicodeString str = new UnicodeString();
    str.field_1_charCount = field_1_charCount;
    str.field_2_optionflags = field_2_optionflags;
    str.field_3_string = field_3_string;
    if (field_4_format_runs != null) {
      str.field_4_format_runs = new ArrayList<FormatRun>();
      for (FormatRun r : field_4_format_runs) {
        str.field_4_format_runs.add(new FormatRun(r._character, r._fontIndex));
      }
    }
    if (field_5_ext_rst != null) {
      str.field_5_ext_rst = field_5_ext_rst.clone();
    }

    return str;
  }
}
/**
 * The axis options record provides unit information and other various tidbits about the axis.
 *
 * <p>
 *
 * @author Andrew C. Oliver(acoliver at apache.org)
 */
public final class AxisOptionsRecord extends StandardRecord {
  public static final short sid = 0x1062;

  private static final BitField defaultMinimum = BitFieldFactory.getInstance(0x01);
  private static final BitField defaultMaximum = BitFieldFactory.getInstance(0x02);
  private static final BitField defaultMajor = BitFieldFactory.getInstance(0x04);
  private static final BitField defaultMinorUnit = BitFieldFactory.getInstance(0x08);
  private static final BitField isDate = BitFieldFactory.getInstance(0x10);
  private static final BitField defaultBase = BitFieldFactory.getInstance(0x20);
  private static final BitField defaultCross = BitFieldFactory.getInstance(0x40);
  private static final BitField defaultDateSettings = BitFieldFactory.getInstance(0x80);

  private short field_1_minimumCategory;
  private short field_2_maximumCategory;
  private short field_3_majorUnitValue;
  private short field_4_majorUnit;
  private short field_5_minorUnitValue;
  private short field_6_minorUnit;
  private short field_7_baseUnit;
  private short field_8_crossingPoint;
  private short field_9_options;

  public AxisOptionsRecord() {}

  public AxisOptionsRecord(RecordInputStream in) {
    field_1_minimumCategory = in.readShort();
    field_2_maximumCategory = in.readShort();
    field_3_majorUnitValue = in.readShort();
    field_4_majorUnit = in.readShort();
    field_5_minorUnitValue = in.readShort();
    field_6_minorUnit = in.readShort();
    field_7_baseUnit = in.readShort();
    field_8_crossingPoint = in.readShort();
    field_9_options = in.readShort();
  }

  public String toString() {
    StringBuffer buffer = new StringBuffer();

    buffer.append("[AXCEXT]\n");
    buffer
        .append("    .minimumCategory      = ")
        .append("0x")
        .append(HexDump.toHex(getMinimumCategory()))
        .append(" (")
        .append(getMinimumCategory())
        .append(" )");
    buffer.append(System.getProperty("line.separator"));
    buffer
        .append("    .maximumCategory      = ")
        .append("0x")
        .append(HexDump.toHex(getMaximumCategory()))
        .append(" (")
        .append(getMaximumCategory())
        .append(" )");
    buffer.append(System.getProperty("line.separator"));
    buffer
        .append("    .majorUnitValue       = ")
        .append("0x")
        .append(HexDump.toHex(getMajorUnitValue()))
        .append(" (")
        .append(getMajorUnitValue())
        .append(" )");
    buffer.append(System.getProperty("line.separator"));
    buffer
        .append("    .majorUnit            = ")
        .append("0x")
        .append(HexDump.toHex(getMajorUnit()))
        .append(" (")
        .append(getMajorUnit())
        .append(" )");
    buffer.append(System.getProperty("line.separator"));
    buffer
        .append("    .minorUnitValue       = ")
        .append("0x")
        .append(HexDump.toHex(getMinorUnitValue()))
        .append(" (")
        .append(getMinorUnitValue())
        .append(" )");
    buffer.append(System.getProperty("line.separator"));
    buffer
        .append("    .minorUnit            = ")
        .append("0x")
        .append(HexDump.toHex(getMinorUnit()))
        .append(" (")
        .append(getMinorUnit())
        .append(" )");
    buffer.append(System.getProperty("line.separator"));
    buffer
        .append("    .baseUnit             = ")
        .append("0x")
        .append(HexDump.toHex(getBaseUnit()))
        .append(" (")
        .append(getBaseUnit())
        .append(" )");
    buffer.append(System.getProperty("line.separator"));
    buffer
        .append("    .crossingPoint        = ")
        .append("0x")
        .append(HexDump.toHex(getCrossingPoint()))
        .append(" (")
        .append(getCrossingPoint())
        .append(" )");
    buffer.append(System.getProperty("line.separator"));
    buffer
        .append("    .options              = ")
        .append("0x")
        .append(HexDump.toHex(getOptions()))
        .append(" (")
        .append(getOptions())
        .append(" )");
    buffer.append(System.getProperty("line.separator"));
    buffer.append("         .defaultMinimum           = ").append(isDefaultMinimum()).append('\n');
    buffer.append("         .defaultMaximum           = ").append(isDefaultMaximum()).append('\n');
    buffer.append("         .defaultMajor             = ").append(isDefaultMajor()).append('\n');
    buffer
        .append("         .defaultMinorUnit         = ")
        .append(isDefaultMinorUnit())
        .append('\n');
    buffer.append("         .isDate                   = ").append(isIsDate()).append('\n');
    buffer.append("         .defaultBase              = ").append(isDefaultBase()).append('\n');
    buffer.append("         .defaultCross             = ").append(isDefaultCross()).append('\n');
    buffer
        .append("         .defaultDateSettings      = ")
        .append(isDefaultDateSettings())
        .append('\n');

    buffer.append("[/AXCEXT]\n");
    return buffer.toString();
  }

  public void serialize(LittleEndianOutput out) {
    out.writeShort(field_1_minimumCategory);
    out.writeShort(field_2_maximumCategory);
    out.writeShort(field_3_majorUnitValue);
    out.writeShort(field_4_majorUnit);
    out.writeShort(field_5_minorUnitValue);
    out.writeShort(field_6_minorUnit);
    out.writeShort(field_7_baseUnit);
    out.writeShort(field_8_crossingPoint);
    out.writeShort(field_9_options);
  }

  protected int getDataSize() {
    return 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2;
  }

  public short getSid() {
    return sid;
  }

  public Object clone() {
    AxisOptionsRecord rec = new AxisOptionsRecord();

    rec.field_1_minimumCategory = field_1_minimumCategory;
    rec.field_2_maximumCategory = field_2_maximumCategory;
    rec.field_3_majorUnitValue = field_3_majorUnitValue;
    rec.field_4_majorUnit = field_4_majorUnit;
    rec.field_5_minorUnitValue = field_5_minorUnitValue;
    rec.field_6_minorUnit = field_6_minorUnit;
    rec.field_7_baseUnit = field_7_baseUnit;
    rec.field_8_crossingPoint = field_8_crossingPoint;
    rec.field_9_options = field_9_options;
    return rec;
  }

  /** Get the minimum category field for the AxisOptions record. */
  public short getMinimumCategory() {
    return field_1_minimumCategory;
  }

  /** Set the minimum category field for the AxisOptions record. */
  public void setMinimumCategory(short field_1_minimumCategory) {
    this.field_1_minimumCategory = field_1_minimumCategory;
  }

  /** Get the maximum category field for the AxisOptions record. */
  public short getMaximumCategory() {
    return field_2_maximumCategory;
  }

  /** Set the maximum category field for the AxisOptions record. */
  public void setMaximumCategory(short field_2_maximumCategory) {
    this.field_2_maximumCategory = field_2_maximumCategory;
  }

  /** Get the major unit value field for the AxisOptions record. */
  public short getMajorUnitValue() {
    return field_3_majorUnitValue;
  }

  /** Set the major unit value field for the AxisOptions record. */
  public void setMajorUnitValue(short field_3_majorUnitValue) {
    this.field_3_majorUnitValue = field_3_majorUnitValue;
  }

  /** Get the major unit field for the AxisOptions record. */
  public short getMajorUnit() {
    return field_4_majorUnit;
  }

  /** Set the major unit field for the AxisOptions record. */
  public void setMajorUnit(short field_4_majorUnit) {
    this.field_4_majorUnit = field_4_majorUnit;
  }

  /** Get the minor unit value field for the AxisOptions record. */
  public short getMinorUnitValue() {
    return field_5_minorUnitValue;
  }

  /** Set the minor unit value field for the AxisOptions record. */
  public void setMinorUnitValue(short field_5_minorUnitValue) {
    this.field_5_minorUnitValue = field_5_minorUnitValue;
  }

  /** Get the minor unit field for the AxisOptions record. */
  public short getMinorUnit() {
    return field_6_minorUnit;
  }

  /** Set the minor unit field for the AxisOptions record. */
  public void setMinorUnit(short field_6_minorUnit) {
    this.field_6_minorUnit = field_6_minorUnit;
  }

  /** Get the base unit field for the AxisOptions record. */
  public short getBaseUnit() {
    return field_7_baseUnit;
  }

  /** Set the base unit field for the AxisOptions record. */
  public void setBaseUnit(short field_7_baseUnit) {
    this.field_7_baseUnit = field_7_baseUnit;
  }

  /** Get the crossing point field for the AxisOptions record. */
  public short getCrossingPoint() {
    return field_8_crossingPoint;
  }

  /** Set the crossing point field for the AxisOptions record. */
  public void setCrossingPoint(short field_8_crossingPoint) {
    this.field_8_crossingPoint = field_8_crossingPoint;
  }

  /** Get the options field for the AxisOptions record. */
  public short getOptions() {
    return field_9_options;
  }

  /** Set the options field for the AxisOptions record. */
  public void setOptions(short field_9_options) {
    this.field_9_options = field_9_options;
  }

  /** Sets the default minimum field value. use the default minimum category */
  public void setDefaultMinimum(boolean value) {
    field_9_options = defaultMinimum.setShortBoolean(field_9_options, value);
  }

  /**
   * use the default minimum category
   *
   * @return the default minimum field value.
   */
  public boolean isDefaultMinimum() {
    return defaultMinimum.isSet(field_9_options);
  }

  /** Sets the default maximum field value. use the default maximum category */
  public void setDefaultMaximum(boolean value) {
    field_9_options = defaultMaximum.setShortBoolean(field_9_options, value);
  }

  /**
   * use the default maximum category
   *
   * @return the default maximum field value.
   */
  public boolean isDefaultMaximum() {
    return defaultMaximum.isSet(field_9_options);
  }

  /** Sets the default major field value. use the default major unit */
  public void setDefaultMajor(boolean value) {
    field_9_options = defaultMajor.setShortBoolean(field_9_options, value);
  }

  /**
   * use the default major unit
   *
   * @return the default major field value.
   */
  public boolean isDefaultMajor() {
    return defaultMajor.isSet(field_9_options);
  }

  /** Sets the default minor unit field value. use the default minor unit */
  public void setDefaultMinorUnit(boolean value) {
    field_9_options = defaultMinorUnit.setShortBoolean(field_9_options, value);
  }

  /**
   * use the default minor unit
   *
   * @return the default minor unit field value.
   */
  public boolean isDefaultMinorUnit() {
    return defaultMinorUnit.isSet(field_9_options);
  }

  /** Sets the isDate field value. this is a date axis */
  public void setIsDate(boolean value) {
    field_9_options = isDate.setShortBoolean(field_9_options, value);
  }

  /**
   * this is a date axis
   *
   * @return the isDate field value.
   */
  public boolean isIsDate() {
    return isDate.isSet(field_9_options);
  }

  /** Sets the default base field value. use the default base unit */
  public void setDefaultBase(boolean value) {
    field_9_options = defaultBase.setShortBoolean(field_9_options, value);
  }

  /**
   * use the default base unit
   *
   * @return the default base field value.
   */
  public boolean isDefaultBase() {
    return defaultBase.isSet(field_9_options);
  }

  /** Sets the default cross field value. use the default crossing point */
  public void setDefaultCross(boolean value) {
    field_9_options = defaultCross.setShortBoolean(field_9_options, value);
  }

  /**
   * use the default crossing point
   *
   * @return the default cross field value.
   */
  public boolean isDefaultCross() {
    return defaultCross.isSet(field_9_options);
  }

  /** Sets the default date settings field value. use default date setttings for this axis */
  public void setDefaultDateSettings(boolean value) {
    field_9_options = defaultDateSettings.setShortBoolean(field_9_options, value);
  }

  /**
   * use default date setttings for this axis
   *
   * @return the default date settings field value.
   */
  public boolean isDefaultDateSettings() {
    return defaultDateSettings.isSet(field_9_options);
  }
}
/**
 * Describes a chart sheet properties record. SHTPROPS (0x1044)
 *
 * <p>(As with all chart related records, documentation is lacking. See {@link ChartRecord} for more
 * details)
 *
 * @author Glen Stampoultzis (glens at apache.org)
 */
public final class SheetPropertiesRecord extends StandardRecord {
  public static final short sid = 0x1044;

  private static final BitField chartTypeManuallyFormatted = BitFieldFactory.getInstance(0x01);
  private static final BitField plotVisibleOnly = BitFieldFactory.getInstance(0x02);
  private static final BitField doNotSizeWithWindow = BitFieldFactory.getInstance(0x04);
  private static final BitField defaultPlotDimensions = BitFieldFactory.getInstance(0x08);
  private static final BitField autoPlotArea = BitFieldFactory.getInstance(0x10);

  private int field_1_flags;
  private int field_2_empty;
  public static final byte EMPTY_NOT_PLOTTED = 0;
  public static final byte EMPTY_ZERO = 1;
  public static final byte EMPTY_INTERPOLATED = 2;

  public SheetPropertiesRecord() {
    // fields uninitialised
  }

  public SheetPropertiesRecord(RecordInputStream in) {
    field_1_flags = in.readUShort();
    field_2_empty = in.readUShort();
  }

  public String toString() {
    StringBuffer buffer = new StringBuffer();

    buffer.append("[SHTPROPS]\n");
    buffer
        .append("    .flags                = ")
        .append(HexDump.shortToHex(field_1_flags))
        .append('\n');
    buffer
        .append("         .chartTypeManuallyFormatted= ")
        .append(isChartTypeManuallyFormatted())
        .append('\n');
    buffer
        .append("         .plotVisibleOnly           = ")
        .append(isPlotVisibleOnly())
        .append('\n');
    buffer
        .append("         .doNotSizeWithWindow       = ")
        .append(isDoNotSizeWithWindow())
        .append('\n');
    buffer
        .append("         .defaultPlotDimensions     = ")
        .append(isDefaultPlotDimensions())
        .append('\n');
    buffer.append("         .autoPlotArea              = ").append(isAutoPlotArea()).append('\n');
    buffer
        .append("    .empty                = ")
        .append(HexDump.shortToHex(field_2_empty))
        .append('\n');

    buffer.append("[/SHTPROPS]\n");
    return buffer.toString();
  }

  public void serialize(LittleEndianOutput out) {
    out.writeShort(field_1_flags);
    out.writeShort(field_2_empty);
  }

  protected int getDataSize() {
    return 2 + 2;
  }

  public short getSid() {
    return sid;
  }

  public Object clone() {
    SheetPropertiesRecord rec = new SheetPropertiesRecord();

    rec.field_1_flags = field_1_flags;
    rec.field_2_empty = field_2_empty;
    return rec;
  }

  /** Get the flags field for the SheetProperties record. */
  public int getFlags() {
    return field_1_flags;
  }

  /**
   * Get the empty field for the SheetProperties record.
   *
   * @return One of EMPTY_NOT_PLOTTED EMPTY_ZERO EMPTY_INTERPOLATED
   */
  public int getEmpty() {
    return field_2_empty;
  }

  /**
   * Set the empty field for the SheetProperties record.
   *
   * @param empty One of EMPTY_NOT_PLOTTED EMPTY_ZERO EMPTY_INTERPOLATED
   */
  public void setEmpty(byte empty) {
    this.field_2_empty = empty;
  }

  /**
   * Sets the chart type manually formatted field value. Has the chart type been manually formatted?
   */
  public void setChartTypeManuallyFormatted(boolean value) {
    field_1_flags = chartTypeManuallyFormatted.setBoolean(field_1_flags, value);
  }

  /**
   * Has the chart type been manually formatted?
   *
   * @return the chart type manually formatted field value.
   */
  public boolean isChartTypeManuallyFormatted() {
    return chartTypeManuallyFormatted.isSet(field_1_flags);
  }

  /** Sets the plot visible only field value. Only show visible cells on the chart. */
  public void setPlotVisibleOnly(boolean value) {
    field_1_flags = plotVisibleOnly.setBoolean(field_1_flags, value);
  }

  /**
   * Only show visible cells on the chart.
   *
   * @return the plot visible only field value.
   */
  public boolean isPlotVisibleOnly() {
    return plotVisibleOnly.isSet(field_1_flags);
  }

  /**
   * Sets the do not size with window field value. Do not size the chart when the window changes
   * size
   */
  public void setDoNotSizeWithWindow(boolean value) {
    field_1_flags = doNotSizeWithWindow.setBoolean(field_1_flags, value);
  }

  /**
   * Do not size the chart when the window changes size
   *
   * @return the do not size with window field value.
   */
  public boolean isDoNotSizeWithWindow() {
    return doNotSizeWithWindow.isSet(field_1_flags);
  }

  /**
   * Sets the default plot dimensions field value. Indicates that the default area dimensions should
   * be used.
   */
  public void setDefaultPlotDimensions(boolean value) {
    field_1_flags = defaultPlotDimensions.setBoolean(field_1_flags, value);
  }

  /**
   * Indicates that the default area dimensions should be used.
   *
   * @return the default plot dimensions field value.
   */
  public boolean isDefaultPlotDimensions() {
    return defaultPlotDimensions.isSet(field_1_flags);
  }

  /** Sets the auto plot area field value. ?? */
  public void setAutoPlotArea(boolean value) {
    field_1_flags = autoPlotArea.setBoolean(field_1_flags, value);
  }

  /**
   * ??
   *
   * @return the auto plot area field value.
   */
  public boolean isAutoPlotArea() {
    return autoPlotArea.isSet(field_1_flags);
  }
}