content: package io.flic.fliclib.javaclient;
  
  import java.io.*;
  import java.nio.charset.StandardCharsets;
  
  import io.flic.fliclib.javaclient.enums.*;
  
  /**
   * Flic Protocol Packets
   */
  
  abstract class CommandPacket {
      protected int opcode;
  
      public final byte[] construct() {
          ByteArrayOutputStream stream = new ByteArrayOutputStream();
          try {
              write(stream);
          } catch (IOException e) {
          }
          byte[] res = new byte[3 + stream.size()];
          res[0] = (byte)(1 + stream.size());
          res[1] = (byte)((1 + stream.size()) >> 8);
          res[2] = (byte)opcode;
          System.arraycopy(stream.toByteArray(), 0, res, 3, stream.size());
          return res;
      }
  
      abstract protected void write(OutputStream stream) throws IOException;
  }
  
  class CmdGetInfo extends CommandPacket {
      @Override
      protected void write(OutputStream stream) {
          opcode = 0;
      }
  }
  
  class CmdCreateScanner extends CommandPacket {
      public int scanId;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 1;
          StreamUtils.writeInt32(stream, scanId);
      }
  }
  
  class CmdRemoveScanner extends CommandPacket {
      public int scanId;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 2;
          StreamUtils.writeInt32(stream, scanId);
      }
  }
  
  class CmdCreateConnectionChannel extends CommandPacket {
      public int connId;
      public Bdaddr bdaddr;
      public LatencyMode latencyMode;
      public short autoDisconnectTime;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 3;
          StreamUtils.writeInt32(stream, connId);
          StreamUtils.writeBdaddr(stream, bdaddr);
          StreamUtils.writeEnum(stream, latencyMode);
          StreamUtils.writeInt16(stream, autoDisconnectTime);
      }
  }
  
  class CmdRemoveConnectionChannel extends CommandPacket {
      public int connId;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 4;
          StreamUtils.writeInt32(stream, connId);
      }
  }
  
  class CmdForceDisconnect extends CommandPacket {
      public Bdaddr bdaddr;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 5;
          StreamUtils.writeBdaddr(stream, bdaddr);
      }
  }
  
  class CmdChangeModeParameters extends CommandPacket {
      public int connId;
      public LatencyMode latencyMode;
      public short autoDisconnectTime;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 6;
          StreamUtils.writeInt32(stream, connId);
          StreamUtils.writeEnum(stream, latencyMode);
          StreamUtils.writeInt16(stream, autoDisconnectTime);
      }
  }
  
  class CmdPing extends CommandPacket {
      public int pingId;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 7;
          StreamUtils.writeInt32(stream, pingId);
      }
  }
  
  class CmdGetButtonInfo extends CommandPacket {
      public Bdaddr bdaddr;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 8;
          StreamUtils.writeBdaddr(stream, bdaddr);
      }
  }
  
  class CmdCreateScanWizard extends CommandPacket {
      public int scanWizardId;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 9;
          StreamUtils.writeInt32(stream, scanWizardId);
      }
  }
  
  class CmdCancelScanWizard extends CommandPacket {
      public int scanWizardId;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 10;
          StreamUtils.writeInt32(stream, scanWizardId);
      }
  }
  
  class CmdDeleteButton extends CommandPacket {
      public Bdaddr bdaddr;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 11;
          StreamUtils.writeBdaddr(stream, bdaddr);
      }
  }
  
  class CmdCreateBatteryStatusListener extends CommandPacket {
      public int listenerId;
      public Bdaddr bdaddr;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 12;
          StreamUtils.writeInt32(stream, listenerId);
          StreamUtils.writeBdaddr(stream, bdaddr);
      }
  }
  
  class CmdRemoveBatteryStatusListener extends CommandPacket {
      public int listenerId;
  
      @Override
      protected void write(OutputStream stream) throws IOException {
          opcode = 13;
          StreamUtils.writeInt32(stream, listenerId);
      }
  }
  
  abstract class EventPacket {
      public static final int EVT_ADVERTISEMENT_PACKET_OPCODE = 0;
      public static final int EVT_CREATE_CONNECTION_CHANNEL_RESPONSE_OPCODE = 1;
      public static final int EVT_CONNECTION_STATUS_CHANGED_OPCODE = 2;
      public static final int EVT_CONNECTION_CHANNEL_REMOVED_OPCODE = 3;
      public static final int EVT_BUTTON_UP_OR_DOWN_OPCODE = 4;
      public static final int EVT_BUTTON_CLICK_OR_HOLD_OPCODE = 5;
      public static final int EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OPCODE = 6;
      public static final int EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OR_HOLD_OPCODE = 7;
      public static final int EVT_NEW_VERIFIED_BUTTON_OPCODE = 8;
      public static final int EVT_GET_INFO_RESPONSE_OPCODE = 9;
      public static final int EVT_NO_SPACE_FOR_NEW_CONNECTION_OPCODE = 10;
      public static final int EVT_GOT_SPACE_FOR_NEW_CONNECTION_OPCODE = 11;
      public static final int EVT_BLUETOOTH_CONTROLLER_STATE_CHANGE_OPCODE = 12;
      public static final int EVT_PING_RESPONSE_OPCODE = 13;
      public static final int EVT_GET_BUTTON_INFO_RESPONSE_OPCODE = 14;
      public static final int EVT_SCAN_WIZARD_FOUND_PRIVATE_BUTTON_OPCODE = 15;
      public static final int EVT_SCAN_WIZARD_FOUND_PUBLIC_BUTTON_OPCODE = 16;
      public static final int EVT_SCAN_WIZARD_BUTTON_CONNECTED_OPCODE = 17;
      public static final int EVT_SCAN_WIZARD_COMPLETED_OPCODE = 18;
      public static final int EVT_BUTTON_DELETED_OPCODE = 19;
      public static final int EVT_BATTERY_STATUS_OPCODE = 20;
  
      public void parse(byte[] arr) {
          InputStream stream = new ByteArrayInputStream(arr);
          try {
              stream.skip(1);
              parseInternal(stream);
          } catch(IOException e) {
          }
      }
  
      abstract protected void parseInternal(InputStream stream) throws IOException;
  }
  
  class EvtAdvertisementPacket extends EventPacket {
      public int scanId;
      public Bdaddr addr;
      public String name;
      public int rssi;
      public boolean isPrivate;
      public boolean alreadyVerified;
      public boolean alreadyConnectedToThisDevice;
      public boolean alreadyConnectedToOtherDevice;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          scanId = StreamUtils.getInt32(stream);
          addr = StreamUtils.getBdaddr(stream);
          name = StreamUtils.getString(stream, 16);
          rssi = StreamUtils.getInt8(stream);
          isPrivate = StreamUtils.getBoolean(stream);
          alreadyVerified = StreamUtils.getBoolean(stream);
          alreadyConnectedToThisDevice = StreamUtils.getBoolean(stream);
          alreadyConnectedToOtherDevice = StreamUtils.getBoolean(stream);
      }
  }
  
  class EvtCreateConnectionChannelResponse extends EventPacket {
      public int connId;
      public CreateConnectionChannelError connectionChannelError;
      public ConnectionStatus connectionStatus;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          connId = StreamUtils.getInt32(stream);
          connectionChannelError = CreateConnectionChannelError.values()[StreamUtils.getUInt8(stream)];
          connectionStatus = ConnectionStatus.values()[StreamUtils.getUInt8(stream)];
      }
  }
  
  class EvtConnectionStatusChanged extends EventPacket {
      public int connId;
      public ConnectionStatus connectionStatus;
      public DisconnectReason disconnectReason;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          connId = StreamUtils.getInt32(stream);
          connectionStatus = ConnectionStatus.values()[StreamUtils.getUInt8(stream)];
          disconnectReason = DisconnectReason.values()[StreamUtils.getUInt8(stream)];
      }
  }
  
  class EvtConnectionChannelRemoved extends EventPacket {
      public int connId;
      public RemovedReason removedReason;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          connId = StreamUtils.getInt32(stream);
          removedReason = RemovedReason.values()[StreamUtils.getUInt8(stream)];
      }
  }
  
  class EvtButtonEvent extends EventPacket {
      public int connId;
      public ClickType clickType;
      public boolean wasQueued;
      public int timeDiff;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          connId = StreamUtils.getInt32(stream);
          clickType = ClickType.values()[StreamUtils.getUInt8(stream)];
          wasQueued = StreamUtils.getBoolean(stream);
          timeDiff = StreamUtils.getInt32(stream);
      }
  }
  
  class EvtNewVerifiedButton extends EventPacket {
      public Bdaddr bdaddr;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          bdaddr = StreamUtils.getBdaddr(stream);
      }
  }
  
  class EvtGetInfoResponse extends EventPacket {
      public BluetoothControllerState bluetoothControllerState;
      public Bdaddr myBdAddr;
      public BdAddrType myBdAddrType;
      public int maxPendingConnections;
      public int maxConcurrentlyConnectedButtons;
      public int currentPendingConnections;
      public boolean currentlyNoSpaceForNewConnections;
      public Bdaddr[] bdAddrOfVerifiedButtons;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          bluetoothControllerState = BluetoothControllerState.values()[StreamUtils.getUInt8(stream)];
          myBdAddr = StreamUtils.getBdaddr(stream);
          myBdAddrType = BdAddrType.values()[StreamUtils.getUInt8(stream)];
          maxPendingConnections = StreamUtils.getUInt8(stream);
          maxConcurrentlyConnectedButtons = StreamUtils.getInt16(stream);
          currentPendingConnections = StreamUtils.getUInt8(stream);
          currentlyNoSpaceForNewConnections = StreamUtils.getBoolean(stream);
          int nbVerifiedButtons = StreamUtils.getUInt16(stream);
          bdAddrOfVerifiedButtons = new Bdaddr[nbVerifiedButtons];
          for (int i = 0; i < nbVerifiedButtons; i++) {
              bdAddrOfVerifiedButtons[i] = StreamUtils.getBdaddr(stream);
          }
      }
  }
  
  class EvtNoSpaceForNewConnection extends EventPacket {
      public int maxConcurrentlyConnectedButtons;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          maxConcurrentlyConnectedButtons = StreamUtils.getUInt8(stream);
      }
  }
  
  class EvtGotSpaceForNewConnection extends EventPacket {
      public int maxConcurrentlyConnectedButtons;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          maxConcurrentlyConnectedButtons = StreamUtils.getUInt8(stream);
      }
  }
  
  class EvtBluetoothControllerStateChange extends EventPacket {
      public BluetoothControllerState state;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          state = BluetoothControllerState.values()[StreamUtils.getUInt8(stream)];
      }
  }
  
  class EvtGetButtonInfoResponse extends EventPacket {
      public Bdaddr bdaddr;
      public String uuid;
      public String color;
      public String serialNumber;
      public int flicVersion;
      public int firmwareVersion;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          bdaddr = StreamUtils.getBdaddr(stream);
          byte[] uuidBytes = StreamUtils.getByteArr(stream, 16);
          StringBuilder sb = new StringBuilder(32);
          for (int i = 0; i < 16; i++) {
              sb.append(String.format("%02x", uuidBytes[i]));
          }
          uuid = sb.toString();
          if (uuid.equals("00000000000000000000000000000000")) {
              uuid = null;
          }
          color = StreamUtils.getString(stream, 16);
          if (color.isEmpty()) {
              color = null;
          }
          serialNumber = StreamUtils.getString(stream, 16);
          if (serialNumber.isEmpty()) {
              serialNumber = null;
          }
          flicVersion = StreamUtils.getUInt8(stream);
          firmwareVersion = StreamUtils.getInt32(stream);
      }
  }
  
  class EvtScanWizardFoundPrivateButton extends EventPacket {
      public int scanWizardId;
      
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          scanWizardId = StreamUtils.getInt32(stream);
      }
  }
  
  class EvtScanWizardFoundPublicButton extends EventPacket {
      public int scanWizardId;
      public Bdaddr addr;
      public String name;
      
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          scanWizardId = StreamUtils.getInt32(stream);
          addr = StreamUtils.getBdaddr(stream);
          int nameLen = StreamUtils.getUInt8(stream);
          byte[] bytes = new byte[nameLen];
          for (int i = 0; i < nameLen; i++) {
              bytes[i] = (byte)stream.read();
          }
          for (int i = nameLen; i < 16; i++) {
              stream.skip(1);
          }
          name = new String(bytes, StandardCharsets.UTF_8);
      }
  }
  
  class EvtScanWizardButtonConnected extends EventPacket {
      public int scanWizardId;
      
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          scanWizardId = StreamUtils.getInt32(stream);
      }
  }
  
  class EvtScanWizardCompleted extends EventPacket {
      public int scanWizardId;
      public ScanWizardResult result;
      
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          scanWizardId = StreamUtils.getInt32(stream);
          result = ScanWizardResult.values()[StreamUtils.getUInt8(stream)];
      }
  }
  
  class EvtButtonDeleted extends EventPacket {
      public Bdaddr bdaddr;
      public boolean deletedByThisClient;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          bdaddr = StreamUtils.getBdaddr(stream);
          deletedByThisClient = StreamUtils.getBoolean(stream);
      }
  }
  
  class EvtBatteryStatus extends EventPacket {
      public int listenerId;
      public int batteryPercentage;
      public long timestamp;
  
      @Override
      protected void parseInternal(InputStream stream) throws IOException {
          listenerId = StreamUtils.getInt32(stream);
          batteryPercentage = StreamUtils.getInt8(stream);
          timestamp = StreamUtils.getInt64(stream);
      }
  }
  