/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You 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 org.apache.geode.pdx.internal;

import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

import org.apache.geode.DataSerializer;
import org.apache.geode.internal.DSCODE;
import org.apache.geode.internal.Sendable;
import org.apache.geode.internal.tcp.ByteBufferInputStream;

/**
 * A class that references the String offset in PdxInstance Used as Index keys for PdxInstances and
 * query evaluation for PdxInstances
 *
 * @since GemFire 7.0
 */
public class PdxString implements Comparable<PdxString>, Sendable {
  private final byte[] bytes;
  private final int offset;
  private final byte header;
  // private int hash; // optimization: cache the hashcode

  public PdxString(byte[] bytes, int offset) {
    this.bytes = bytes;
    this.header = bytes[offset];
    this.offset = calcOffset(header, offset);
  }

  public PdxString(String s) {
    ByteArrayOutputStream bos = new ByteArrayOutputStream(s.length());
    try {
      DataSerializer.writeString(s, new DataOutputStream(bos));
    } catch (IOException e) {
      throw new IllegalStateException(e);
    }
    this.bytes = bos.toByteArray();
    this.header = bytes[0];
    this.offset = calcOffset(header, 0);
  }

  private int calcOffset(int header, int offset) {
    offset++; // increment offset for the header byte
    // length is stored as short for small strings
    if (header == DSCODE.STRING_BYTES || header == DSCODE.STRING) {
      offset += 2; // position the offset to the start of the String
                   // (skipping header and length bytes)
    }
    // length is stored as int for huge strings
    else if (header == DSCODE.HUGE_STRING_BYTES || header == DSCODE.HUGE_STRING) {
      offset += 4; // position the offset to the start of the String
                   // (skipping header and length bytes)
    }
    return offset;
  }

  private int getLength() {
    int length = 0;
    int lenOffset = this.offset;
    if (header == DSCODE.STRING_BYTES || header == DSCODE.STRING) {
      lenOffset -= 2;
      byte a = bytes[lenOffset];
      byte b = bytes[lenOffset + 1];
      length = ((a & 0xff) << 8) | (b & 0xff);
    }
    // length is stored as int for huge strings
    else if (header == DSCODE.HUGE_STRING_BYTES || header == DSCODE.HUGE_STRING) {
      lenOffset -= 4;
      byte a = bytes[lenOffset];
      byte b = bytes[lenOffset + 1];
      byte c = bytes[lenOffset + 2];
      byte d = bytes[lenOffset + 3];
      length = (((a & 0xff) << 24) | ((b & 0xff) << 16) | ((c & 0xff) << 8) | (d & 0xff));
    }
    return length;
  }

  public int compareTo(PdxString o) {
    // not handling strings with different headers
    if (this.header != o.header) {
      int diff = this.toString().compareTo(o.toString());
      return diff;
    }
    int len1 = this.getLength();
    int len2 = o.getLength();
    int n = Math.min(len1, len2);

    int i = this.offset;
    int j = o.offset;

    if (i == j) {
      int k = i;
      int lim = n + i;
      while (k < lim) {
        byte c1 = bytes[k];
        byte c2 = o.bytes[k];
        if (c1 != c2) {
          return c1 - c2;
        }
        k++;
      }
    } else {
      while (n-- != 0) {
        byte c1 = bytes[i++];
        byte c2 = o.bytes[j++];
        if (c1 != c2) {
          return c1 - c2;
        }
      }
    }
    return len1 - len2;
  }

  public int hashCode() {
    int h = 0;
    int len = this.getLength();
    if (len > 0) {
      int off = this.offset;
      for (int i = 0; i < len; i++) {
        h = 31 * h + bytes[off++];
      }
    }
    return h;
  }

  public boolean equals(Object anObject) {
    if (this == anObject) {
      return true;
    }
    if (anObject instanceof PdxString) {
      PdxString o = (PdxString) anObject;
      if (this.header != o.header) { // header needs to be same for Pdxstrings to be equal
        return false;
      }
      int n = this.getLength();
      if (n == o.getLength()) {
        int i = this.offset;
        int j = o.offset;
        while (n-- != 0) {
          if (bytes[i++] != o.bytes[j++])
            return false;
        }
        return true;
      }
    }
    return false;
  }


  public String toString() {
    String s = null;
    int headerOffset = this.offset;
    try {
      --headerOffset; // for header byte
      if (header == DSCODE.STRING_BYTES || header == DSCODE.STRING) {
        headerOffset -= 2; // position the offset to the start of the String (skipping
        // header and length bytes)
      }
      // length is stored as int for huge strings
      else if (header == DSCODE.HUGE_STRING_BYTES || header == DSCODE.HUGE_STRING) {
        headerOffset -= 4;
      }
      ByteBuffer stringByteBuffer =
          ByteBuffer.wrap(bytes, headerOffset, bytes.length - headerOffset); // Wrapping more bytes
                                                                             // than the actual
                                                                             // String bytes in
      // array. Counting on the readString() to read only String
      // bytes
      s = DataSerializer.readString(new ByteBufferInputStream(stringByteBuffer));
    } catch (IOException e) {
      throw new IllegalStateException(e);
    }
    return s;
  }

  @Override
  public void sendTo(DataOutput out) throws IOException {
    int offset = this.offset;
    int len = getLength();
    --offset; // for header byte
    len++;
    if (header == DSCODE.STRING_BYTES || header == DSCODE.STRING) {
      len += 2;
      offset -= 2;
    } else if (header == DSCODE.HUGE_STRING_BYTES || header == DSCODE.HUGE_STRING) {
      len += 4;
      offset -= 4;
    }
    out.write(bytes, offset, len);
  }

}
