DicomMessage = function(syntax) {
    this.syntax = syntax ? syntax : null;
    this.type = C.DATA_TYPE_COMMAND;
    this.messageId = C.DEFAULT_MESSAGE_ID;
    this.elementPairs = {};
};

DicomMessage.prototype.isCommand = function() {
    return this.type == C.DATA_TYPE_COMMAND;
};

DicomMessage.prototype.setSyntax = function(syntax) {
    this.syntax = syntax;

    for (var tag in this.elementPairs) {
        this.elementPairs[tag].setSyntax(this.syntax);
    }  
};

DicomMessage.prototype.setMessageId = function(id) {
    this.messageId = id;
};

DicomMessage.prototype.setReplyMessageId = function(id) {
    this.replyMessageId = id;
};

DicomMessage.prototype.command = function(cmds) {
    cmds.unshift(this.newElement(0x00000800, this.dataSetPresent ? C.DATA_SET_PRESENT : C.DATE_SET_ABSENCE));
    cmds.unshift(this.newElement(0x00000700, this.priority));
    cmds.unshift(this.newElement(0x00000110, this.messageId));
    cmds.unshift(this.newElement(0x00000100, this.commandType));
    cmds.unshift(this.newElement(0x00000002, this.contextUID));

    var length = 0;
    cmds.forEach(function(cmd) {
        length += cmd.length(cmd.getFields());
    });

    cmds.unshift(this.newElement(0x00000000, length));
    return cmds;
};

DicomMessage.prototype.response = function(cmds) {
    cmds.unshift(this.newElement(0x00000800, this.dataSetPresent ? C.DATA_SET_PRESENT : C.DATE_SET_ABSENCE));
    cmds.unshift(this.newElement(0x00000120, this.replyMessageId));
    cmds.unshift(this.newElement(0x00000100, this.commandType));
    if (this.contextUID)
      cmds.unshift(this.newElement(0x00000002, this.contextUID));

    var length = 0;
    cmds.forEach(function(cmd) {
        length += cmd.length(cmd.getFields());
    });

    cmds.unshift(this.newElement(0x00000000, length));
    return cmds;
};  

DicomMessage.prototype.setElements = function(pairs) {
    var p = {};
    for (var tag in pairs) {
        p[tag] = this.newElement(tag, pairs[tag]);
    }

    this.elementPairs = p;
};  

DicomMessage.prototype.newElement = function(tag, value) {
    return elementByType(tag, value, this.syntax);
};

DicomMessage.prototype.setElement = function(key, value) {
    this.elementPairs[key] = elementByType(key, value);
};

DicomMessage.prototype.setElementPairs = function(pairs) {
    this.elementPairs = pairs;
};

DicomMessage.prototype.setContextId = function(context) {
    this.contextUID = context;
};

DicomMessage.prototype.setPriority = function(pri) {
    this.priority = pri;
};

DicomMessage.prototype.setType = function(type) {
    this.type = type;
};

DicomMessage.prototype.setDataSetPresent = function(present) {
    this.dataSetPresent = present == 0x0101 ? false : true;
};

DicomMessage.prototype.haveData = function() {
    return this.dataSetPresent;
};  

DicomMessage.prototype.tags = function() {
    return Object.keys(this.elementPairs);
};

DicomMessage.prototype.key = function(tag) {
    return elementKeywordByTag(tag);
};

DicomMessage.prototype.getValue = function(tag) {
    return this.elementPairs[tag] ? this.elementPairs[tag].getValue() : null;
};

DicomMessage.prototype.affectedSOPClassUID = function() {
    return this.getValue(0x00000002);
};  

DicomMessage.prototype.getMessageId = function() {
    return this.getValue(0x00000110);
};

DicomMessage.prototype.getFields = function() {
    var eles = [];
    for (var tag in this.elementPairs) {
        eles.push(this.elementPairs[tag]);
    }

    return eles;
};  

DicomMessage.prototype.length = function(elems) {
    var len = 0;
    elems.forEach(function(elem) {
        len += elem.length(elem.getFields());
    });
    return len;
};

DicomMessage.prototype.isResponse = function() {
    return false;
};

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

DicomMessage.prototype.write = function(stream) {
    var fields = this.getFields(),
   o = this;
    fields.forEach(function(field) {
        field.setSyntax(o.syntax);
        field.write(stream);
    });
};

DicomMessage.prototype.printElements = function(pairs, indent) {
    var typeName = '';
    for (var tag in pairs) {
        var value = pairs[tag].getValue();
        typeName += (' '.repeat(indent)) + this.key(tag) + ' : ';
        if (value instanceof Array) {
            var o = this;
            value.forEach(function(p) {
                if (typeof p == 'object') {
                    typeName += '[\n' + o.printElements(p, indent + 2) + (' '.repeat(indent)) + ']';
                } else {
                    typeName += '[' + p + ']';
                }
            });
            if (typeName[typeName.length - 1] != '\n') {
                typeName += '\n';
            }
        } else {
            typeName += value + '\n';
        }
    }

    return typeName;
};

DicomMessage.prototype.typeString = function() {
    var typeName = '';
    if (!this.isCommand()) {
        typeName = 'DateSet Message';
    } else {
        switch (this.commandType) {
        case C.COMMAND_C_GET_RSP : typeName = 'C-GET-RSP'; break;
        case C.COMMAND_C_MOVE_RSP : typeName = 'C-MOVE-RSP'; break;
        case C.COMMAND_C_GET_RQ : typeName = 'C-GET-RQ'; break;
        case C.COMMAND_C_STORE_RQ : typeName = 'C-STORE-RQ'; break;
        case C.COMMAND_C_FIND_RSP : typeName = 'C-FIND-RSP'; break;
        case C.COMMAND_C_MOVE_RQ : typeName = 'C-MOVE-RQ'; break;
        case C.COMMAND_C_FIND_RQ : typeName = 'C-FIND-RQ'; break;
        case C.COMMAND_C_STORE_RSP : typeName = 'C-STORE-RSP'; break;
    }      
    }   

    return typeName;
};

DicomMessage.prototype.toString = function() {
    var typeName = this.typeString();
    typeName += ' [\n';
    typeName += this.printElements(this.elementPairs, 0);
    typeName += ']';
    return typeName;
};

DicomMessage.prototype.walkObject = function(pairs) {
    var obj = {},
   o = this;
    for (var tag in pairs) {
        var v = pairs[tag].getValue(),
     u = v;
        if (v instanceof Array) {
            u = [];
            v.forEach(function(a) {
                if (typeof a == 'object') {
                    u.push(o.walkObject(a));
                } else u.push(a);
            });
        }

        obj[tag] = u;
    }

    return obj;
};

DicomMessage.prototype.toObject = function() {
    return this.walkObject(this.elementPairs);
};

DicomMessage.readToPairs = function(stream, syntax, options) {
    var pairs = {};
    while (!stream.end()) {
        var elem = new DataElement();
        if (options) {
            elem.setOptions(options);
        }

        elem.setSyntax(syntax);
        elem.readBytes(stream);
        pairs[elem.tag.value] = elem;
    }

    return pairs;  
};

var fileValid = function(stream) {
    return stream.readString(4, C.TYPE_ASCII) == 'DICM';
};

var readMetaStream = function(stream, useSyntax, length, callback) {
    var message = new FileMetaMessage();
    message.setElementPairs(DicomMessage.readToPairs(stream, useSyntax));
    if (callback) {
        callback(null, message, length);
    }

    return message;  
};

DicomMessage.readMetaHeader = function(bufferOrFile, callback) {
    var useSyntax = C.EXPLICIT_LITTLE_ENDIAN;
    if (bufferOrFile instanceof Buffer) {
        var stream = new ReadStream(bufferOrFile);
        stream.reset();
        stream.increment(128);  
        if (!fileValid(stream)) {
            return quitWithError('Invalid a dicom file ', callback);  
        }      

        var el = readAElement(stream, useSyntax),
            metaLength = el.value,
            metaStream = stream.more(metaLength);

        return readMetaStream(metaStream, useSyntax, metaLength, callback);
    } else if (typeof bufferOrFile == 'string') {
        fs.open(bufferOrFile, 'r', function(err, fd) {
            if (err) {
                //fs.closeSync(fd);
                return quitWithError('Cannot open file', callback);
            }

            var buffer = new Buffer(16);
            fs.read(fd, buffer, 0, 16, 128, function(err, bytesRead) {
                if (err || bytesRead != 16) {
                    fs.closeSync(fd);
                    return quitWithError('Cannot read file', callback);
                } 

                var stream = new ReadStream(buffer);
                if (!fileValid(stream)) {
                    fs.closeSync(fd);
                    return quitWithError('Not a dicom file ' + bufferOrFile, callback);
                }

                var el = readAElement(stream, useSyntax), 
                    metaLength = el.value,
                    metaBuffer = new Buffer(metaLength);

                fs.read(fd, metaBuffer, 0, metaLength, 144, function(err, bytesRead) {
                    fs.closeSync(fd);
                    if (err || bytesRead != metaLength) {
                      return quitWithError('Invalid a dicom file ' + bufferOrFile, callback);            
                    }

                    var metaStream = new ReadStream(metaBuffer);
                    return readMetaStream(metaStream, useSyntax, metaLength, callback);
                });
            });
        });
    } 

    return null;
};

DicomMessage.read = function(stream, type, syntax, options) {
    var elements = [],
   pairs = {},
   useSyntax = type == C.DATA_TYPE_COMMAND ? C.IMPLICIT_LITTLE_ENDIAN : syntax;
    stream.reset();
    while (!stream.end()) {
        var elem = new DataElement();
        if (options) {
            elem.setOptions(options);
        }

        elem.setSyntax(useSyntax);
        elem.readBytes(stream);//return;
        pairs[elem.tag.value] = elem;
    }

    var message = null;
    if (type == C.DATA_TYPE_COMMAND) {
        var cmdType = pairs[0x00000100].value;

        switch (cmdType) {
        case 0x8020 : message = new CFindRSP(useSyntax); break;
        case 0x8021 : message = new CMoveRSP(useSyntax); break;
        case 0x8010 : message = new CGetRSP(useSyntax); break;
        case 0x0001 : message = new CStoreRQ(useSyntax); break;
        case 0x0020 : message = new CFindRQ(useSyntax); break;
        case 0x8001 : message = new CStoreRSP(useSyntax); break;
        default : throw 'Unrecognized command type ' + cmdType.toString(16); break;
    }

        message.setElementPairs(pairs);
        message.setDataSetPresent(message.getValue(0x00000800));
        message.setContextId(message.getValue(0x00000002));
        if (!message.isResponse()) {
            message.setMessageId(message.getValue(0x00000110));
        } else {
            message.setReplyMessageId(message.getValue(0x00000120));
        }
    } else if (type == C.DATA_TYPE_DATA) {
        message = new DataSetMessage(useSyntax);
        message.setElementPairs(pairs);
    } else {
        throw 'Unrecognized message type';
    }

    return message;
};

DataSetMessage = function(syntax) {
    DicomMessage.call(this, syntax);
    this.type = C.DATA_TYPE_DATA;
};

util.inherits(DataSetMessage, DicomMessage);

DataSetMessage.prototype.is = function(type) {
    return false;
};

FileMetaMessage = function(syntax) {
    DicomMessage.call(this, syntax);
    this.type = null;
};

util.inherits(FileMetaMessage, DicomMessage);

CommandMessage = function(syntax) {
    DicomMessage.call(this, syntax);
    this.type = C.DATA_TYPE_COMMAND;
    this.priority = C.PRIORITY_MEDIUM;
    this.dataSetPresent = true;     
};

util.inherits(CommandMessage, DicomMessage);

CommandMessage.prototype.getFields = function() {
    return this.command(CommandMessage.super_.prototype.getFields.call(this));
};  

CommandResponse = function(syntax) {
    DicomMessage.call(this, syntax);
    this.type = C.DATA_TYPE_COMMAND;
    this.dataSetPresent = true;
};

util.inherits(CommandResponse, DicomMessage);

CommandResponse.prototype.isResponse = function() {
    return true;
};

CommandResponse.prototype.respondedTo = function() {
    return this.getValue(0x00000120);
};

CommandResponse.prototype.isFinal = function() {
    return this.success() || this.failure() || this.cancel();
};

CommandResponse.prototype.warning = function() {
    var status = this.getStatus();
    return (status == 0x0001) || (status >> 12 == 0xb);
};

CommandResponse.prototype.success = function() {
    return this.getStatus() == 0x0000;
};

CommandResponse.prototype.failure = function() {
    var status = this.getStatus();
    return (status >> 12 == 0xa) || (status >> 12 == 0xc) || (status >> 8 == 0x1);
};

CommandResponse.prototype.cancel = function() {
    return this.getStatus() == C.STATUS_CANCEL;
};

CommandResponse.prototype.pending = function() {
    var status = this.getStatus();
    return (status == 0xff00) || (status == 0xff01);
};

CommandResponse.prototype.getStatus = function() {
    return this.getValue(0x00000900);
};

CommandResponse.prototype.setStatus = function(status) {
    this.setElement(0x00000900, status);
};

// following four methods only available to C-GET-RSP and C-MOVE-RSP
CommandResponse.prototype.getNumOfRemainingSubOperations = function() {
    return this.getValue(0x00001020);
};

CommandResponse.prototype.getNumOfCompletedSubOperations = function() {
    return this.getValue(0x00001021);
};

CommandResponse.prototype.getNumOfFailedSubOperations = function() {
    return this.getValue(0x00001022);
};

CommandResponse.prototype.getNumOfWarningSubOperations = function() {
    return this.getValue(0x00001023);
};
//end

CommandResponse.prototype.getFields = function() {
    return this.response(CommandResponse.super_.prototype.getFields.call(this));
};

CFindRSP = function(syntax) {
    CommandResponse.call(this, syntax);
    this.commandType = 0x8020;
};

util.inherits(CFindRSP, CommandResponse);

CGetRSP = function(syntax) {
    CommandResponse.call(this, syntax);
    this.commandType = 0x8010;
};

util.inherits(CGetRSP, CommandResponse);

CMoveRSP = function(syntax) {
    CommandResponse.call(this, syntax);
    this.commandType = 0x8021;
};

util.inherits(CMoveRSP, CommandResponse);

CFindRQ = function(syntax) {
    CommandMessage.call(this, syntax);
    this.commandType = 0x20;
    this.contextUID = C.SOP_STUDY_ROOT_FIND;
};

util.inherits(CFindRQ, CommandMessage);

CCancelRQ = function(syntax) {
    CommandResponse.call(this, syntax);
    this.commandType = 0x0fff;
    this.contextUID = null;
    this.dataSetPresent = false;
};

util.inherits(CCancelRQ, CommandResponse);

CCancelMoveRQ = function(syntax) {
    CommandResponse.call(this, syntax);
    this.commandType = 0x0fff;
    this.contextUID = null;
    this.dataSetPresent = false;
};

util.inherits(CCancelMoveRQ, CommandResponse);

CMoveRQ = function(syntax, destination) {
    CommandMessage.call(this, syntax);
    this.commandType = 0x21;
    this.contextUID = C.SOP_STUDY_ROOT_MOVE;
    this.setDestination(destination || '');  
};

util.inherits(CMoveRQ, CommandMessage);

CMoveRQ.prototype.setStore = function(cstr) {
    this.store = cstr;
};  

CMoveRQ.prototype.setDestination = function(dest) {
    this.setElement(0x00000600, dest);    
};

CGetRQ = function(syntax) {
    CommandMessage.call(this, syntax);
    this.commandType = 0x10;
    this.contextUID = C.SOP_STUDY_ROOT_GET;
    this.store = null;
};

util.inherits(CGetRQ, CommandMessage);

CGetRQ.prototype.setStore = function(cstr) {
    this.store = cstr;
};

CStoreRQ = function(syntax) {
    CommandMessage.call(this, syntax);
    this.commandType = 0x01;
    this.contextUID = C.SOP_STUDY_ROOT_GET;  
};

util.inherits(CStoreRQ, CommandMessage);

CStoreRQ.prototype.getOriginAETitle = function() {
    return this.getValue(0x00001030);
};

CStoreRQ.prototype.getMoveMessageId = function() {
    return this.getValue(0x00001031);
};

CStoreRQ.prototype.getAffectedSOPInstanceUID = function() {
    return this.getValue(0x00001000);
};

CStoreRQ.prototype.setAffectedSOPInstanceUID = function(uid) {
    this.setElement(0x00001000, uid);
};

CStoreRQ.prototype.setAffectedSOPClassUID = function(uid) {
    this.setElement(0x00000002, uid);
};

CStoreRSP = function(syntax) {
    CommandResponse.call(this, syntax);
    this.commandType = 0x8001;
    this.contextUID = C.SOP_STUDY_ROOT_GET;
    this.dataSetPresent = false;
};

util.inherits(CStoreRSP, CommandResponse);

CStoreRSP.prototype.setAffectedSOPInstanceUID = function(uid) {
    this.setElement(0x00001000, uid);
};  

CStoreRSP.prototype.getAffectedSOPInstanceUID = function(uid) {
    return this.getValue(0x00001000);
};
