PDU = function() {
  this.fields = [];
  this.lengthBytes = 4;  
}

PDU.prototype.length = function(fields) {
  var len = 0;
  fields.forEach(function(f) {
    len += !f.getFields ? f.length() : f.length(f.getFields());
  });
  return len;
}

PDU.prototype.is = function(type) {
  return this.type == type;
}

PDU.prototype.getFields = function(fields) {
  var len = this.lengthField(fields);
  fields.unshift(len);
  if (this.type !== null) {
    fields.unshift(new ReservedField());
    fields.unshift(new HexField(this.type));
  }
    
  return fields;
}

PDU.prototype.lengthField = function(fields) {
  if (this.lengthBytes == 4) {
    return new UInt32Field(this.length(fields));
  } else if (this.lengthBytes == 2) {
    return new UInt16Field(this.length(fields));
  } else {
    throw "Invalid length bytes";
  }
}  

PDU.prototype.read = function(stream) {
  stream.read(C.TYPE_HEX, 1);
  var length = stream.read(C.TYPE_UINT32);
  this.readBytes(stream, length);
}

PDU.prototype.load = function(stream) {
  return PDU.createByStream(stream);
}

PDU.prototype.loadPDV = function(stream, length) {
  if (stream.end()) return false;
  var bytesRead = 0, pdvs = [];
  while (bytesRead < length) {
    var plength = stream.read(C.TYPE_UINT32), 
        pdv = new PresentationDataValueItem();
    pdv.readBytes(stream, plength);
    bytesRead += plength + 4;

    pdvs.push(pdv);
  }

  return pdvs;
}

PDU.prototype.loadDicomMessage = function(stream, isCommand, isLast) {
  var message = DicomMessage.read(stream, isCommand, isLast);
  return message;
}

PDU.prototype.stream = function() {
  var stream = new WriteStream(), 
      fields = this.getFields();

  // writing to buffer
  fields.forEach(function(field){
    field.write(stream);
  });

  return stream;
}

PDU.prototype.buffer = function() {
  return this.stream().buffer();
}

var interpretCommand = function(stream, isLast) {
  parseDicomMessage(stream);
}

mergePDVs = function(pdvs) {
  var merges = [], count = pdvs.length, i = 0;
  while (i < count) {console.log(pdvs[i].isLast, pdvs[i].type);
    if (!pdvs[i].isLast) {
      var j = i;
      while (!pdvs[j++].isLast && j < count) {
        pdvs[i].messageStream.concat(pdvs[j].messageStream);
      }
      merges.push(pdvs[i]);
      i = j;
    } else {
      merges.push(pdvs[i++]);
    }
  }
  return merges;
}

PDU.typeToString = function(type) {
  var pdu = null, typeNum = parseInt(type, 16);
  //console.log("RECEIVED PDU-TYPE ", typeNum);
  switch (typeNum) {
    case 0x01 : pdu = 'ASSOCIATE-RQ'; break;
    case 0x02 : pdu = 'ASSOCIATE-AC'; break;
    case 0x04 : pdu = 'P-DATA-TF'; break;
    case 0x06 : pdu = 'RELEASE-RP'; break;
    case 0x07 : pdu = 'ASSOCIATE-ABORT'; break;
    case 0x10 : pdu = 'APPLICATION-CONTEXT-ITEM'; break;
    case 0x20 : pdu = 'PRESENTATION-CONTEXT-ITEM'; break;
    case 0x21 : pdu = 'PRESENTATION-CONTEXT-ITEM-AC'; break;
    case 0x30 : pdu = 'ABSTRACT-SYNTAX-ITEM'; break;
    case 0x40 : pdu = 'TRANSFER-SYNTAX-ITEM'; break;
    case 0x50 : pdu = 'USER-INFORMATION-ITEM'; break;
    case 0x51 : pdu = 'MAXIMUM-LENGTH-ITEM'; break;
    case 0x52 : pdu = 'IMPLEMENTATION-CLASS-UID-ITEM'; break;
    case 0x55 : pdu = 'IMPLEMENTATION-VERSION-NAME-ITEM'; break;
    default : break;
  }

  return pdu;
}

PDU.createByStream = function(stream) {
  if (stream.end()) return null;

  var pduType = stream.read(C.TYPE_HEX, 1), typeNum = parseInt(pduType, 16), pdu = null;
  //console.log("RECEIVED PDU-TYPE ", pduType);
  switch (typeNum) {
    case 0x01 : pdu = new AssociateRQ(); break;
    case 0x02 : pdu = new AssociateAC(); break;
    case 0x04 : pdu = new PDataTF(); break;
    case 0x06 : pdu = new ReleaseRP(); break;
    case 0x07 : pdu = new AssociateAbort(); break;
    case 0x10 : pdu = new ApplicationContextItem(); break;
    case 0x20 : pdu = new PresentationContextItem(); break;
    case 0x21 : pdu = new PresentationContextItemAC(); break;
    case 0x30 : pdu = new AbstractSyntaxItem(); break;
    case 0x40 : pdu = new TransferSyntaxItem(); break;
    case 0x50 : pdu = new UserInformationItem(); break;
    case 0x51 : pdu = new MaximumLengthItem(); break;
    case 0x52 : pdu = new ImplementationClassUIDItem(); break;
    case 0x55 : pdu = new ImplementationVersionNameItem(); break;
    default : throw "Unrecoginized pdu type " + pduType; break;
  }
  if (pdu)
    pdu.read(stream);

  return pdu;
}

var nextItemIs = function(stream, pduType) {
  if (stream.end()) return false;

  var nextType = stream.read(C.TYPE_HEX, 1);
  stream.increment(-1);
  return pduType == nextType;
}

AssociateRQ = function() {
  PDU.call(this);
  this.type = C.ITEM_TYPE_PDU_ASSOCIATE_RQ;
  this.protocolVersion = 1;  
}

util.inherits(AssociateRQ, PDU);

AssociateRQ.prototype.setProtocolVersion = function(version) {
  this.protocolVersion = version;
}

AssociateRQ.prototype.setCalledAETitle = function(title) {
  this.calledAETitle = title;
}

AssociateRQ.prototype.setCallingAETitle = function(title) {
  this.callingAETitle = title;
}

AssociateRQ.prototype.setApplicationContextItem = function(item) {
  this.applicationContextItem = item;
}

AssociateRQ.prototype.setPresentationContextItems = function(items) {
  this.presentationContextItems = items;
}

AssociateRQ.prototype.setUserInformationItem = function(item) {
  this.userInformationItem = item;
}

AssociateRQ.prototype.allAccepted = function() {
  for (var i in this.presentationContextItems) {
    var item = this.presentationContextItems[i];
    if (!item.accepted()) return false;
  }
  return true;
}

AssociateRQ.prototype.getFields = function() {
  var f = [
    new UInt16Field(this.protocolVersion), new ReservedField(2),
    new FilledField(this.calledAETitle, 16), new FilledField(this.callingAETitle, 16),
    new ReservedField(32), this.applicationContextItem
  ];
  this.presentationContextItems.forEach(function(context){
    f.push(context);
  });
  f.push(this.userInformationItem);
  return AssociateRQ.super_.prototype.getFields.call(this, f);
}

AssociateRQ.prototype.readBytes = function(stream, length) {
  this.type = C.ITEM_TYPE_PDU_ASSOCIATE_RQ;
  var version = stream.read(C.TYPE_UINT16);
  this.setProtocolVersion(version);
  stream.increment(2);
  var calledAE = stream.read(C.TYPE_ASCII, 16);
  this.setCalledAETitle(calledAE);
  var callingAE = stream.read(C.TYPE_ASCII, 16);
  this.setCallingAETitle(callingAE);
  stream.increment(32);
  
  var appContext = this.load(stream);
  this.setApplicationContextItem(appContext);

  var presContexts = [];
  do {
    presContexts.push(this.load(stream));
  } while (nextItemIs(stream, C.ITEM_TYPE_PRESENTATION_CONTEXT));
  this.setPresentationContextItems(presContexts);

  var userItem = this.load(stream);
  this.setUserInformationItem(userItem);
}

AssociateRQ.prototype.buffer = function() {
  return AssociateRQ.super_.prototype.buffer.call(this);
}

AssociateAC = function() {
  AssociateRQ.call(this);
};

util.inherits(AssociateAC, AssociateRQ);

AssociateAC.prototype.readBytes = function(stream, length) {
  this.type = C.ITEM_TYPE_PDU_ASSOCIATE_AC;
  var version = stream.read(C.TYPE_UINT16);
  this.setProtocolVersion(version);
  stream.increment(66);
  
  var appContext = this.load(stream);
  this.setApplicationContextItem(appContext);

  var presContexts = [];
  do {
    presContexts.push(this.load(stream));
  } while (nextItemIs(stream, C.ITEM_TYPE_PRESENTATION_CONTEXT_AC));
  this.setPresentationContextItems(presContexts);

  var userItem = this.load(stream);
  this.setUserInformationItem(userItem);
}

AssociateAbort = function() {
  this.type = C.ITEM_TYPE_PDU_AABORT;
  this.source = 1;
  this.reason = 0;
  PDU.call(this);  
}

util.inherits(AssociateAbort, PDU);

AssociateAbort.prototype.setSource = function(src) {
  this.source = src;
}

AssociateAbort.prototype.setReason = function(reason) {
  this.reason = reason;
}

AssociateAbort.prototype.readBytes = function(stream, length) {
  stream.increment(2);

  var source = stream.read(C.TYPE_UINT8);
  this.setSource(source);

  var reason = stream.read(C.TYPE_UINT8);
  this.setReason(reason);
}

AssociateAbort.prototype.getFields = function() {
  return AssociateAbort.super_.prototype.getFields.call(this, [
    new ReservedField(), new ReservedField(), 
    new UInt8Field(this.source), new UInt8Field(this.reason)
  ]);
}

ReleaseRQ = function() {
  this.type = C.ITEM_TYPE_PDU_RELEASE_RQ;
  PDU.call(this);
};

util.inherits(ReleaseRQ, PDU);

ReleaseRQ.prototype.getFields = function() {
  return ReleaseRQ.super_.prototype.getFields.call(this, [new ReservedField(4)]);
};

ReleaseRP = function() {
  this.type = C.ITEM_TYPE_PDU_RELEASE_RP;
  PDU.call(this);
}

util.inherits(ReleaseRP, PDU);

ReleaseRP.prototype.readBytes = function(stream, length) {
  stream.increment(4);
};

ReleaseRP.prototype.getFields = function() {
  return ReleaseRP.super_.prototype.getFields.call(this, [new ReservedField(4)]);
}

PDataTF = function() {
  this.type = C.ITEM_TYPE_PDU_PDATA;
  this.presentationDataValueItems = [];
  PDU.call(this);
}
util.inherits(PDataTF, PDU);

PDataTF.prototype.setPresentationDataValueItems = function(items) {
  this.presentationDataValueItems = items ? items : [];
}

PDataTF.prototype.getFields = function() {
  var fields = this.presentationDataValueItems;

  return PDataTF.super_.prototype.getFields.call(this, fields);
}

PDataTF.prototype.readBytes = function(stream, length) {
  var pdvs = this.loadPDV(stream, length);
  //let merges = mergePDVs(pdvs);

  this.setPresentationDataValueItems(pdvs);
}

Item = function() {
  PDU.call(this);
  this.lengthBytes = 2;
};
util.inherits(Item, PDU);

Item.prototype.read = function(stream) {
  stream.read(C.TYPE_HEX, 1);
  var length = stream.read(C.TYPE_UINT16);
  this.readBytes(stream, length);
}  

Item.prototype.write = function(stream) {
  stream.concat(this.stream());
}

Item.prototype.getFields = function(fields) {
  return Item.super_.prototype.getFields.call(this, fields);
}

PresentationDataValueItem = function(context) {
  this.type = null;
  this.isLast = true;
  this.dataFragment = null;
  this.contextId = context;
  this.messageStream = null;
  Item.call(this);

  this.lengthBytes = 4;
};
util.inherits(PresentationDataValueItem, Item);

PresentationDataValueItem.prototype.setContextId = function(id) {
  this.contextId = id;
}

PresentationDataValueItem.prototype.setFlag = function(flag) {
  this.flag = flag;
}

PresentationDataValueItem.prototype.setPresentationDataValue = function(pdv) {
  this.pdv = pdv;
}

PresentationDataValueItem.prototype.setMessage = function(msg) {
  this.dataFragment = msg;
}

PresentationDataValueItem.prototype.getMessage = function() {
  return this.dataFragment;
}

PresentationDataValueItem.prototype.readBytes = function(stream, length) {
  this.contextId = stream.read(C.TYPE_UINT8);
  var messageHeader = stream.read(C.TYPE_UINT8);
  this.isLast = messageHeader >> 1;
  this.type = messageHeader & 1 ? C.DATA_TYPE_COMMAND : C.DATA_TYPE_DATA;

  //load dicom messages
  this.messageStream = stream.more(length - 2);
}

PresentationDataValueItem.prototype.getFields = function() {
  var fields = [new UInt8Field(this.contextId)];
  //define header
  var messageHeader = (1 & this.dataFragment.type) | ((this.isLast ? 1 : 0) << 1);
  fields.push(new UInt8Field(messageHeader));

  fields.push(this.dataFragment);
/*var stream = new WriteStream();this.dataFragment.write(stream);
//var f = this.dataFragment.getFields();
//var wr = new WriteStream();f[0].setSyntax(this.dataFragment.syntax);f[0].write(wr);
var rst = stream.toReadBuffer();
rst.setEndian(C.LITTLE_ENDIAN);
  var group = rst.read(C.TYPE_UINT16), 
      element = rst.read(C.TYPE_UINT16),
      tag = tagFromNumbers(group, element);

      console.log(tag.toString(), rst.read(C.TYPE_UINT32));*/
  return PresentationDataValueItem.super_.prototype.getFields.call(this, fields);
}  

ApplicationContextItem = function() {
  this.type = C.ITEM_TYPE_APPLICATION_CONTEXT;
  this.applicationContextName = C.APPLICATION_CONTEXT_NAME;
  Item.call(this);  
}
util.inherits(ApplicationContextItem, Item);

ApplicationContextItem.prototype.setApplicationContextName = function(name) {
  this.applicationContextName = name;
}

ApplicationContextItem.prototype.getFields = function() {
  return ApplicationContextItem.super_.prototype.getFields.call(this, [new StringField(this.applicationContextName)]);
}

ApplicationContextItem.prototype.readBytes = function(stream, length) {
  var appContext = stream.read(C.TYPE_ASCII, length);
  this.setApplicationContextName(appContext);
}

ApplicationContextItem.prototype.buffer = function() {
  return ApplicationContextItem.super_.prototype.buffer.call(this);
}

PresentationContextItem = function() {
  this.type = C.ITEM_TYPE_PRESENTATION_CONTEXT;
  Item.call(this);
};
util.inherits(PresentationContextItem, Item);

PresentationContextItem.prototype.setPresentationContextID = function(id) {
  this.presentationContextID = id;
}

PresentationContextItem.prototype.setAbstractSyntaxItem = function(item) {
  this.abstractSyntaxItem = item;
}

PresentationContextItem.prototype.setTransferSyntaxesItems = function(items) {
  this.transferSyntaxesItems = items;
}

PresentationContextItem.prototype.setResultReason = function(reason) {
  this.resultReason = reason;
}

PresentationContextItem.prototype.accepted = function() {
  return this.resultReason == 0;
}

PresentationContextItem.prototype.readBytes = function(stream, length) {
  var contextId = stream.read(C.TYPE_UINT8);
  this.setPresentationContextID(contextId);
  stream.increment(1);
  stream.increment(1);
  stream.increment(1);

  var abstractItem = this.load(stream);
  this.setAbstractSyntaxItem(abstractItem);

  var transContexts = [];
  do {
    transContexts.push(this.load(stream));
  } while (nextItemIs(stream, C.ITEM_TYPE_TRANSFER_CONTEXT));
  this.setTransferSyntaxesItems(transContexts);
}

PresentationContextItem.prototype.getFields = function() {
  var f = [
    new UInt8Field(this.presentationContextID), 
    new ReservedField(), new ReservedField(), new ReservedField(), this.abstractSyntaxItem
  ];
  this.transferSyntaxesItems.forEach(function(syntaxItem){
    f.push(syntaxItem);
  });
  return PresentationContextItem.super_.prototype.getFields.call(this, f);  
}

PresentationContextItem.prototype.buffer = function() {
  return PresentationContextItem.super_.prototype.buffer.call(this);
}

PresentationContextItemAC = function() {
  this.type = C.ITEM_TYPE_PRESENTATION_CONTEXT_AC;
  Item.call(this);
};
util.inherits(PresentationContextItemAC, PresentationContextItem);

PresentationContextItemAC.prototype.readBytes = function(stream, length) {
  var contextId = stream.read(C.TYPE_UINT8);
  this.setPresentationContextID(contextId);
  stream.increment(1);
  var resultReason = stream.read(C.TYPE_UINT8);
  this.setResultReason(resultReason);
  stream.increment(1);

  var transItem = this.load(stream);
  this.setTransferSyntaxesItems([transItem]);
}

AbstractSyntaxItem = function() {
  this.type = C.ITEM_TYPE_ABSTRACT_CONTEXT;
  Item.call(this);  
}
util.inherits(AbstractSyntaxItem, Item);

AbstractSyntaxItem.prototype.setAbstractSyntaxName = function(name) {
  this.abstractSyntaxName = name;
}

AbstractSyntaxItem.prototype.getFields = function() {
  return AbstractSyntaxItem.super_.prototype.getFields.call(this, [new StringField(this.abstractSyntaxName)]);
}  

AbstractSyntaxItem.prototype.buffer = function() {
  return AbstractSyntaxItem.super_.prototype.buffer.call(this);
}

AbstractSyntaxItem.prototype.readBytes = function(stream, length) {
  var name = stream.read(C.TYPE_ASCII, length);
  this.setAbstractSyntaxName(name);
}

TransferSyntaxItem = function() {
  this.type = C.ITEM_TYPE_TRANSFER_CONTEXT;
  Item.call(this);
};
util.inherits(TransferSyntaxItem, Item);

TransferSyntaxItem.prototype.setTransferSyntaxName = function(name) {
  this.transferSyntaxName = name;
}

TransferSyntaxItem.prototype.readBytes = function(stream, length) {
  var transfer = stream.read(C.TYPE_ASCII, length);
  this.setTransferSyntaxName(transfer);
}

TransferSyntaxItem.prototype.getFields = function() {
  return TransferSyntaxItem.super_.prototype.getFields.call(this, [new StringField(this.transferSyntaxName)]);
}

TransferSyntaxItem.prototype.buffer = function() {
  return TransferSyntaxItem.super_.prototype.buffer.call(this);
}

UserInformationItem = function() {
  this.type = C.ITEM_TYPE_USER_INFORMATION;
  Item.call(this);
};
util.inherits(UserInformationItem, Item);

UserInformationItem.prototype.setUserDataItems = function(items) {
  this.userDataItems = items;
}

UserInformationItem.prototype.readBytes = function(stream, length) {
  var items = [], pdu = this.load(stream);

  do {
    items.push(pdu);
  } while (pdu = this.load(stream));
  this.setUserDataItems(items);
}

UserInformationItem.prototype.getFields = function() {
  var f = [];
  this.userDataItems.forEach(function(userData){
    f.push(userData);
  });
  return UserInformationItem.super_.prototype.getFields.call(this, f);
}

UserInformationItem.prototype.buffer = function() {
  return UserInformationItem.super_.prototype.buffer.call(this);
}

ImplementationClassUIDItem = function() {
  this.type = C.ITEM_TYPE_IMPLEMENTATION_UID;
  Item.call(this);  
}
util.inherits(ImplementationClassUIDItem, Item);

ImplementationClassUIDItem.prototype.setImplementationClassUID = function(id) {
  this.implementationClassUID = id;
}

ImplementationClassUIDItem.prototype.readBytes = function(stream, length) {
  var uid = stream.read(C.TYPE_ASCII, length);
  this.setImplementationClassUID(uid);
}

ImplementationClassUIDItem.prototype.getFields = function() {
  return ImplementationClassUIDItem.super_.prototype.getFields.call(this, [new StringField(this.implementationClassUID)]);
}

ImplementationClassUIDItem.prototype.buffer = function() {
  return ImplementationClassUIDItem.super_.prototype.buffer.call(this);
}

ImplementationVersionNameItem = function() {
  this.type = C.ITEM_TYPE_IMPLEMENTATION_VERSION;
  Item.call(this);  
}
util.inherits(ImplementationVersionNameItem, Item);

ImplementationVersionNameItem.prototype.setImplementationVersionName = function(name) {
  this.implementationVersionName = name;
}

ImplementationVersionNameItem.prototype.readBytes = function(stream, length) {
  var name = stream.read(C.TYPE_ASCII, length);
  this.setImplementationVersionName(name);
}

ImplementationVersionNameItem.prototype.getFields = function() {
  return ImplementationVersionNameItem.super_.prototype.getFields.call(this, [new StringField(this.implementationVersionName)]);
}

ImplementationVersionNameItem.prototype.buffer = function() {
  return ImplementationVersionNameItem.super_.prototype.buffer.call(this);
}

MaximumLengthItem = function() {
  this.type = C.ITEM_TYPE_MAXIMUM_LENGTH;
  this.maximumLengthReceived = 32768;
  Item.call(this);  
}
util.inherits(MaximumLengthItem, Item);

MaximumLengthItem.prototype.setMaximumLengthReceived = function(length) {
  this.maximumLengthReceived = length;
}

MaximumLengthItem.prototype.readBytes = function(stream, length) {
  var l = stream.read(C.TYPE_UINT32);
  this.setMaximumLengthReceived(l);
}

MaximumLengthItem.prototype.getFields = function() {
  return MaximumLengthItem.super_.prototype.getFields.call(this, [new UInt32Field(this.maximumLengthReceived)]);
}

MaximumLengthItem.prototype.buffer = function() {
  return MaximumLengthItem.super_.prototype.buffer.call(this);
}

