/FileExtraction/Java_unseen/Kegbot_kegbot-android/kegtab/src/main/java/org/kegbot/kegboard/KegboardMessage.java
/*
 * Copyright 2003-2020 The Kegbot Project contributors <info@kegbot.org>
 *
 * This file is part of the Kegtab package from the Kegbot project. For
 * more information on Kegtab or Kegbot, see <http://kegbot.org/>.
 *
 * Kegtab is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free
 * Software Foundation, version 2.
 *
 * Kegtab is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with Kegtab. If not, see <http://www.gnu.org/licenses/>.
 */
package org.kegbot.kegboard;

import com.google.common.collect.Maps;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Shorts;

import java.util.Arrays;
import java.util.Map;

/**
 * Base message type for messages to/from a Kegboard device.
 * @see <a href="http://kegbot.org/docs/kegboard-guide/">Kegboard Guide</a>
 */
public abstract class KegboardMessage {

  private static final int KBSP_HEADER_LENGTH = 12;

  private static final int KBSP_TRAILER_LENGTH = 4;

  private static final int KBSP_MIN_LENGTH = KBSP_HEADER_LENGTH
      + KBSP_TRAILER_LENGTH;

  private static final byte[] KBSP_HEADER_BYTES = "KBSP v1:".getBytes();

  private static final byte[] KBSP_TRAILER_BYTES = {'\r', '\n'};

  private static final int KBSP_MAX_LENGTH = 256;

  private static final int KBSP_PAYLOAD_MAX_LENGTH = KBSP_MAX_LENGTH - KBSP_MIN_LENGTH;

  protected final Map<Integer, byte[]> mTags = Maps.newLinkedHashMap();

  protected KegboardMessage() {
    assert (false);
  }

  protected KegboardMessage(byte[] wholeMessage) throws KegboardMessageException {
    if (wholeMessage.length < KBSP_MIN_LENGTH) {
      throw new KegboardMessageException("Raw message size too small: min=" + KBSP_MIN_LENGTH
          + ", actual=" + wholeMessage.length);
    } else if (wholeMessage.length > KBSP_MAX_LENGTH) {
      throw new KegboardMessageException("Raw message size too large: max=" + KBSP_MAX_LENGTH
          + ", actual=" + wholeMessage.length);
    }

    final int payloadLength = Shorts.fromBytes(wholeMessage[11], wholeMessage[10]) & 0x0ffff;
    final int payloadEnd = KBSP_HEADER_LENGTH + payloadLength;
    if (payloadLength > KBSP_PAYLOAD_MAX_LENGTH) {
      throw new KegboardMessageException("Illegal payload size: max=" + KBSP_PAYLOAD_MAX_LENGTH
          + ", actual=" + payloadLength);
    }

    final int totalMessageSize = KBSP_HEADER_LENGTH + payloadLength + KBSP_TRAILER_LENGTH;
    if (wholeMessage.length != totalMessageSize) {
      throw new KegboardMessageException("Input buffer size does not match computed size: "
          + "payloadLength=" + payloadLength + ", needed=" + totalMessageSize
          + ", actual=" + wholeMessage.length);
    }

    final byte[] payload;
    if (payloadLength > 0) {
      payload = Arrays.copyOfRange(wholeMessage, KBSP_HEADER_LENGTH, payloadEnd);
    } else {
      payload = new byte[0];
    }

    final byte[] crcBytes = Arrays.copyOfRange(wholeMessage, payloadEnd, payloadEnd + 2);

    final byte[] trailerBytes = Arrays.copyOfRange(wholeMessage, payloadEnd + 2, payloadEnd + 4);
    if (!Arrays.equals(trailerBytes, KBSP_TRAILER_BYTES)) {
      throw new KegboardMessageException("Illegal trailer value.");
    }

    final int expectedCrc = KegboardCrc.crc16Ccitt(wholeMessage, wholeMessage.length - 4) & 0x0ffff;
    final int computedCrc = Shorts.fromBytes(crcBytes[1], crcBytes[0]) & 0x0ffff;

    if (expectedCrc != computedCrc) {
      throw new KegboardMessageException("Bad CRC: "
          + "expected=" + String.format("0x%04x ", Integer.valueOf(expectedCrc))
          + "computed=" + String.format("0x%04x ", Integer.valueOf(computedCrc)));
    }

    short messageType = Shorts.fromBytes(wholeMessage[9], wholeMessage[8]);
    if (messageType != getMessageType()) {
      throw new KegboardMessageException("Message type mismatch: expected=" + getMessageType()
          + " got=" + messageType);
    }

    // System.out.println(HexDump.dumpHexString(wholeMessage));

    for (int i = 0; i <= (payload.length - 2); ) {
      final int tagNum = payload[i] & 0x00ff;
      final int length = payload[i + 1] & 0x00ff;

      i += 2;

      if ((i + length) <= payload.length) {
        mTags.put(Integer.valueOf(tagNum),
            Arrays.copyOfRange(payload, i, i + length));
      }

      i += length;
    }
  }

  private static int getCrc(byte[] payload) {
    byte[] message = Bytes.concat(KBSP_HEADER_BYTES, payload);
    return KegboardCrc.crc16Ccitt(message, message.length);
  }

  public byte[] toBytes() {
    byte[] payload = new byte[0];

    for (Map.Entry<Integer, byte[]> entry : mTags.entrySet()) {
      final byte[] tag = new byte[]{(byte) (entry.getKey().intValue() & 0x0ff)};
      final byte[] value = entry.getValue();
      final int len = value.length & 0x0ff;
      final byte[] length = new byte[]{(byte) len};

      payload = Bytes.concat(payload, tag, length, value);
    }

    byte[] messageType = new byte[]{(byte) (getMessageType() & 0x0ff), 00};
    byte[] messageLength = new byte[]{(byte) (payload.length & 0x0ff), 00};

    byte[] message = Bytes.concat(KBSP_HEADER_BYTES, messageType, messageLength, payload);

    byte[] crc = Shorts.toByteArray((short) (getCrc(message) & 0x0ffff));
    byte[] result = Bytes.concat(message, crc, KBSP_TRAILER_BYTES);
    return result;
  }

  @Override
  public String toString() {
    Class<? extends KegboardMessage> clazz = this.getClass();

    StringBuilder builder = new StringBuilder();
    builder.append("<");
    builder.append(clazz.getSimpleName());
    builder.append(": ");

    if (clazz == KegboardMessage.class) {
      builder.append("type=");
      builder.append(String.format("0x%04x ", Integer.valueOf(getMessageType())));
    }
    builder.append(getStringExtra());
    builder.append(">");

    return builder.toString();
  }

  protected String getStringExtra() {
    return "";
  }

  public byte[] readTag(int tagNum) {
    return mTags.get(Integer.valueOf(tagNum));
  }

  public int readTagAsShort(int tagNum) {
    final byte[] tagData = readTag(tagNum);
    if (tagData != null && tagData.length == 2) {
      int result = (tagData[1] & 0xff) << 8;
      result |= tagData[0] & 0xff;
      return result;
    }
    return 0;
  }

  public Long readTagAsLong(int tagNum) {
    final byte[] tagData = readTag(tagNum);
    if (tagData != null && tagData.length == 4) {
      long result = (tagData[3] & 0xff) << 24;
      result |= (tagData[2] & 0xff) << 16;
      result |= (tagData[1] & 0xff) << 8;
      result |= tagData[0] & 0xff;
      return Long.valueOf(result);
    }
    return null;
  }

  public String readTagAsString(int tagNum) {
    final byte[] tagData = readTag(tagNum);
    if (tagData == null) {
      return null;
    }
    return new String(tagData).replace("\0", "");
  }

  private static int extractType(final byte[] bytes) {
    return Shorts.fromBytes(bytes[9], bytes[8]);
  }

  public static KegboardMessage fromBytes(final byte[] bytes) throws KegboardMessageException {
    if (bytes.length < KBSP_MIN_LENGTH) {
      throw new KegboardMessageException("Too small: " + bytes.length);
    } else if (bytes.length > KBSP_MAX_LENGTH) {
      throw new KegboardMessageException("Too large: " + bytes.length);
    }

    final int messageType = extractType(bytes);

    switch (messageType) {
      case KegboardHelloMessage.MESSAGE_TYPE:
        return new KegboardHelloMessage(bytes);
      case KegboardMeterStatusMessage.MESSAGE_TYPE:
        return new KegboardMeterStatusMessage(bytes);
      case KegboardTemperatureReadingMessage.MESSAGE_TYPE:
        return new KegboardTemperatureReadingMessage(bytes);
      case KegboardOutputStatusMessage.MESSAGE_TYPE:
        return new KegboardOutputStatusMessage(bytes);
      case KegboardAuthTokenMessage.MESSAGE_TYPE:
        return new KegboardAuthTokenMessage(bytes);
      default:
        throw new KegboardMessageException("Unknown message type");
    }
  }

  public abstract short getMessageType();

}
