/*
 * 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.uima.cas.impl;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

import org.apache.uima.UIMARuntimeException;

/**
 * Common de/serialization
 */
public class CommonSerDes {

  int version1;
  int version2;

  boolean isDelta;
  boolean isCompressed;

  // @formatter:off
  /*********************************************
   * HEADERS
   * Serialization versioning
   *   There are 1 or 2 words used for versioning.
   *     Compressed formats and plain formats with bit xx on in first word use 2nd word
   * 
   * First word:
   * 
   *     - bit in 0x01 position: on for binary non-delta (redundant)   
   *     - bit in 0x02 position: on means delta, off - not delta
   *     - bit in 0x04 position: on means compressed, off means plain binary
   *     - bit in 0x08 position: on means type system + indexes def included
   *     - bit in 0x10 position: on means type system (only) included
   *     - bits  0xF0 reserved
   * 
   *     - byte in 0xFF 00 position: 
   *               a sequential version number, incrementing (starting w/ 0)
   * 
   *                  0 = original (UIMA v2)
   *                  1 = fixes to original found during V3 development
   *                  2 = V3
   * 
   *     - byte in 0xFF 00 00  position: special flags with some shared meaning
   *       -- bit 0x01 00 00: V3 formats
   * 
   *   Second word:
   *     - bit in 0x01 position: on means form6, off = form 4 
   *********************************************/
  // @formatter:on
  public static class Header {
    boolean isDelta;
    boolean isCompressed;
    boolean form4;
    boolean form6;
    boolean typeSystemIncluded; // for form 6, TS only
    boolean typeSystemIndexDefIncluded;
    byte seqVersionNbr = 2; // safety, might be changed to write v2 style
    boolean isV3;
    boolean swap;
    int v; // for error messages

    Reading reading;

    /* ********** BUILDERS ************/
    public Header delta() {
      isDelta = true;
      return this;
    }

    public Header delta(boolean v2) {
      isDelta = v2;
      return this;
    }

    public Header form4() {
      isCompressed = form4 = true;
      form6 = false;
      return this;
    }

    public Header form6() {
      isCompressed = form6 = true;
      form4 = false;
      return this;
    }

    public Header typeSystemIncluded(boolean f) {
      typeSystemIncluded = f;
      return this;
    }

    public Header typeSystemIndexDefIncluded(boolean f) {
      typeSystemIndexDefIncluded = f;
      return this;
    }

    public Header seqVer(int v2) {
      assert (v2 >= 0 && v2 < 256);
      seqVersionNbr = (byte) v2;
      return this;
    }

    public Header v3() {
      isV3 = true;
      return this;
    }

    public void write(DataOutputStream dos) throws IOException {
      if (isV3) {
        assert seqVersionNbr >= 2;
      } else {
        assert seqVersionNbr < 2;
      }
      v = (!isCompressed && !isDelta) ? 1 : 0;
      if (isDelta)
        v |= 0x02;
      if (isCompressed)
        v |= 0x04;
      if (typeSystemIndexDefIncluded)
        v |= 0x08;
      if (typeSystemIncluded)
        v |= 0x10;
      v |= (seqVersionNbr << 8);
      if (isV3)
        v |= 0x010000;

      byte[] uima = new byte[4];
      uima[0] = 85; // U
      uima[1] = 73; // I
      uima[2] = 77; // M
      uima[3] = 65; // A

      ByteBuffer buf = ByteBuffer.wrap(uima);
      int key = buf.asIntBuffer().get();

      dos.writeInt(key);
      dos.writeInt(v);

      if (isCompressed) {
        dos.writeInt(form6 ? 1 : 0);
      }

    }

    /* ******** Header Properties **********/
    public boolean isDelta() {
      return isDelta;
    }

    public boolean isCompressed() {
      return isCompressed;
    }

    public boolean isForm4() {
      return form4;
    }

    public boolean isForm6() {
      return form6;
    }

    public boolean isTypeSystemIndexDefIncluded() {
      return typeSystemIndexDefIncluded;
    }

    public boolean isTypeSystemIncluded() {
      return typeSystemIncluded;
    }

    public byte getSeqVersionNbr() {
      return seqVersionNbr;
    }

    public boolean isV3() {
      return isV3;
    }

  }

  public static Header createHeader() {
    return new Header();
  }

  public static boolean isBinaryHeader(DataInputStream dis) {
    dis.mark(4);
    byte[] bytebuf = new byte[4];
    try {
      bytebuf[0] = dis.readByte(); // U
      bytebuf[1] = dis.readByte(); // I
      bytebuf[2] = dis.readByte(); // M
      bytebuf[3] = dis.readByte(); // A
      String s = new String(bytebuf, StandardCharsets.UTF_8);
      return s.equals("UIMA") || s.equals("AMIU");
    } catch (IOException e) {
      return false;
    } finally {
      try {
        dis.reset();
      } catch (IOException e) {
        throw new UIMARuntimeException(e);
      }
    }
  }

  public static Header readHeader(DataInputStream dis) throws IOException {

    Header h = new Header();
    // key
    // determine if byte swap if needed based on key
    byte[] bytebuf = new byte[4];
    bytebuf[0] = dis.readByte(); // U
    bytebuf[1] = dis.readByte(); // I
    bytebuf[2] = dis.readByte(); // M
    bytebuf[3] = dis.readByte(); // A

    h.swap = (bytebuf[0] != 85);
    Reading r = new Reading(dis, h.swap);
    h.reading = r;

    int v = h.v = r.readInt(); // h.v for error message use

    h.isDelta = (v & 2) != 0;
    h.isCompressed = (v & 4) != 0;
    h.typeSystemIndexDefIncluded = (v & 8) != 0;
    h.typeSystemIncluded = (v & 16) != 0;
    h.seqVersionNbr = (byte) ((v & 0xFF00) >> 8);
    h.isV3 = (v & 0x010000) != 0;

    if (h.isCompressed) {
      v = r.readInt();
      h.form4 = v == 0;
      h.form6 = v == 1;
    }

    return h;
  }

  public static DataOutputStream maybeWrapToDataOutputStream(OutputStream os) {
    if (os instanceof DataOutputStream dataOutputStream) {
      return dataOutputStream;
    }
    return new DataOutputStream(os);
  }

  public static DataInputStream maybeWrapToDataInputStream(InputStream os) {
    if (os instanceof DataInputStream dataInputStream) {
      return dataInputStream;
    }
    return new DataInputStream(os);
  }

  /**
   * byte swapping reads of integer forms
   */

  public static class Reading {
    final DataInputStream dis;
    final boolean swap;

    Reading(DataInputStream dis, boolean swap) {
      this.dis = dis;
      this.swap = swap;
    }

    long readLong() throws IOException {
      long v = dis.readLong();
      return swap ? Long.reverseBytes(v) : v;
    }

    int readInt() throws IOException {
      int v = dis.readInt();
      return swap ? Integer.reverseBytes(v) : v;
    }

    short readShort() throws IOException {
      short v = dis.readShort();
      return swap ? Short.reverseBytes(v) : v;
    }

  }

}
