# A simple and flexible NBT parser.

import struct

# Allows for easy sequential reading of binary data
class DataReader:
  def __init__(self, data):
    self.data = data
    self.idx = 0

  def pop(self, key):
    key = "<{}".format(key)
    size = struct.calcsize(key)
    popped = struct.unpack(key, self.data[self.idx:self.idx + size])[0]
    self.idx += size
    return popped

  # Specific to the NBT string format, two bytes for size followed by that many bytes of string.
  def popString(self):
    size = self.pop("h")
    popped = struct.unpack("<{}s".format(size), self.data[self.idx:self.idx + size])[0]
    self.idx += size
    return popped.decode("utf-8")

  # Useful when dealing with an unknown number of compound tags back to back.
  def finished(self):
    return self.idx >= len(self.data)

  def __repr__(self):
    return str(self.data[self.idx:])

# Allows for easy sequential writing of binary data.
class DataWriter:
  def __init__(self):
    self.data = b""

  def put(self, key, *data):
    key = "<{}".format(key)
    self.data += struct.pack(key, *data)

  def putString(self, string):
    string = string.encode("utf-8")
    self.put("h", len(string))
    self.data += struct.pack("<{}s".format(len(string)), string)

  def __repr__(self):
    return str(self.data)

# Generic base tag, calls self.decode with binary data to fill in payload.
class TAG:
  ID = None
  def __init__(self, name, data):
    self.name = name
    if isinstance(data, DataReader):
      self.payload = self.decode(data)
    else:
      self.payload = data

  def decode(self, dataReader):
    raise NotImplementedError("Decode method not overridden by subclass.")

  def encode(self, dataWriter):
    raise NotImplementedError("Encode method not overridden by subclass.")

  def __getitem__(self, name):
    for item in self.payload:
      if item.name == name:
        return item
    raise KeyError("{} not found in {}".format(name, self.payload))

  def __eq__(self, other):
    return self.name == other.name and self.payload == other.payload and self.ID == other.ID

  def __repr__(self):
    return "{}-{}:{}".format(self.__class__.__name__, self.name, self.payload)

def TAG_Generator(ID, fmt):
  def decode(self, dataReader):
    return dataReader.pop(fmt)
  def encode(self, dataWriter):
    return dataWriter.put(fmt, self.payload)
  return type("TAG_{}".format(ID), (TAG,), {"ID": ID, "decode": decode, "encode": encode})

tags = [] # Need to pre define tags for the later classes.

TAG_Byte = TAG_Generator(1, "b")
TAG_Short = TAG_Generator(2, "h")
TAG_Int = TAG_Generator(3, "i")
TAG_Long = TAG_Generator(4, "q")
TAG_Float = TAG_Generator(5, "f")
TAG_Double = TAG_Generator(6, "d")

# Similar to TAG_List, except the type of tag is not specified, as we know it is a byte.
class TAG_Byte_Array(TAG):
  ID = 7
  def decode(self, dataReader):
    size = dataReader.pop("i")
    payload = []
    for i in range(size):
      payload.append(TAG_Byte(i, dataReader))
    return payload

  def encode(self, dataWriter):
    dataWriter.put("i", len(self.payload)) # Size
    for item in self.payload:
      item.encode(dataWriter)

class TAG_String(TAG):
  ID = 8
  def decode(self, dataReader):
    return dataReader.popString()
  def encode(self, dataWriter):
    dataWriter.putString(self.payload)

# Basically a TAG_Compound, but the items don't have names, and instead are named integer indexes.
#  This allows for a generic __getitem__ function in the TAG class.
class TAG_List(TAG):
  ID = 9
  def decode(self, dataReader):
    self.itemID = dataReader.pop("b")
    size = dataReader.pop("i")
    payload = []
    for i in range(size):
      payload.append(tags[self.itemID](i, dataReader))
    return payload

  def encode(self, dataWriter):
    if self.payload == []: # We don't know the dataWriter type.
      dataWriter.put("b", 0) # Default to TAG_End
    else:
      dataWriter.put("b", tags.index(type(self.payload[0])))
    dataWriter.put("i", len(self.payload))
    for item in self.payload:
      item.encode(dataWriter)

  def add(self, tag):
    self.payload.append(tag)

# Stores some number of complete tags, followed by a TAG_End
class TAG_Compound(TAG):
  ID = 10
  def decode(self, dataReader):
    payload = []
    tagID = dataReader.pop("b")
    while tagID != 0:
      if tags[tagID] is not None:
        name = dataReader.popString()
        payload.append(tags[tagID](name, dataReader))
      else:
        raise NotImplementedError("Tag {} not implemented.".format(tagID))
      tagID = dataReader.pop("b")
    return payload

  def encode(self, dataWriter):
    for item in self.payload:
      dataWriter.put("b", item.ID)
      dataWriter.putString(item.name)
      item.encode(dataWriter)
    dataWriter.put("b", 0)

  def add(self, tag):
    self.payload.append(tag)

  def pop(self, name):
    for i in range(len(self.payload)):
      if self.payload[i].name == name:
        return self.payload.pop(i)
    return None

# Similar to TAG_List, except the type of tag is not specified, as we know it is an int.
class TAG_Int_Array(TAG):
  ID = 7
  def decode(self, dataReader):
    size = dataReader.pop("i")
    payload = []
    for i in range(size):
      payload.append(TAG_Int(i, dataReader))
    return payload

  def encode(self, dataWriter):
    dataWriter.put("i", len(self.payload)) # Size
    for item in self.payload:
      item.encode(dataWriter)

# Similar to TAG_List, except the type of tag is not specified, as we know it is a long.
class TAG_Long_Array(TAG):
  ID = 7
  def decode(self, dataReader):
    size = dataReader.pop("i")
    payload = []
    for i in range(size):
      payload.append(TAG_Long(i, dataReader))
    return payload

  def encode(self, dataWriter):
    dataWriter.put("i", len(self.payload)) # Size
    for item in self.payload:
      item.encode(dataWriter)

tags = [ None,
         TAG_Byte,
         TAG_Short,
         TAG_Int,
         TAG_Long,
         TAG_Float,
         TAG_Double,
         TAG_Byte_Array,
         TAG_String,
         TAG_List,
         TAG_Compound,
         TAG_Int_Array,
         TAG_Long_Array ]

def decode(dataReader):
  tagID = dataReader.pop("b")
  if tags[tagID] is not None:
    name = dataReader.popString()
    return tags[tagID](name, dataReader)
  raise NotImplementedError("Tag {} not implemented.".format(tagID))

def encode(toEncode, dataWriter=None):
  dataWriter = dataWriter or DataWriter()
  dataWriter.put("b", toEncode.ID)
  dataWriter.putString(toEncode.name)
  toEncode.encode(dataWriter)
  return dataWriter.data
