var EventEmitter = Npm.require('events').EventEmitter;

function time() {
    return Math.floor(Date.now() / 1000);
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}

var Envelope = function(command, dataset) {
    EventEmitter.call(this);
    this.command = command;
    this.dataset = dataset;
}
util.inherits(Envelope, EventEmitter);

CSocket = function(socket, options) {
    EventEmitter.call(this);
    this.socket = socket;
    this.negotiatedContexts = {};
    this.receiving = null;
    this.receiveLength = null;
    this.minRecv = null;    
    this.lastReceived = null;
    this.presentationContexts = [];
    this.associated = false;
    this.pendingPDVs = null;
    this.connected = false;
    this.started = null;    
    this.intervalId = null;
    this.lastCommand = null;
    this.lastSent = null;    
    this.messages = {};
    this.messageIdCounter = 0;
    this.callingAe = null;
    this.calledAe = null;
    this.id = getRandomInt(1000, 9999);
    this.options = options;

    var o = this;
    this.socket.on("connect", function(){
        o.ready();
    });
    this.socket.on("data", function(data) {
        o.received(data);
    });
    this.socket.on("error", function(he) {
        console.log("Error: ", he);
    });
    this.socket.on("close", function() {
        if (o.intervalId) {
            clearInterval(o.intervalId);
        }
        o.connected = false;
        console.log("Connection closed");
        o.emit('close');        
    })

    this.on("released", function() {
        this.released();
    });
    this.on('aborted', function() {
        this.released();
    })
    this.on('message', function(pdvs) {
        this.receivedMessage(pdvs);
    });    
};
util.inherits(CSocket, EventEmitter);

CSocket.prototype.setCallingAE = function(ae) {
    this.callingAe = ae;
};

CSocket.prototype.setCalledAe = function(ae) {
    this.calledAe = ae;
};

CSocket.prototype.associate = function() {
    var associateRQ = new AssociateRQ();
    associateRQ.setCalledAETitle(this.calledAe);
    associateRQ.setCallingAETitle(this.callingAe);
    associateRQ.setApplicationContextItem(new ApplicationContextItem());

    var contextItems = []
    this.presentationContexts.forEach(function(context) {
      var contextItem = new PresentationContextItem(),
          syntaxes = [];
      context.transferSyntaxes.forEach(function(transferSyntax) {
          var transfer = new TransferSyntaxItem();
          transfer.setTransferSyntaxName(transferSyntax);
          syntaxes.push(transfer);
      });
      contextItem.setTransferSyntaxesItems(syntaxes);
      contextItem.setPresentationContextID(context.id);

      var abstractItem = new AbstractSyntaxItem();
      abstractItem.setAbstractSyntaxName(context.abstractSyntax);
      contextItem.setAbstractSyntaxItem(abstractItem);
      contextItems.push(contextItem);
    });
    associateRQ.setPresentationContextItems(contextItems);

    var maxLengthItem = new MaximumLengthItem(),
      classUIDItem = new ImplementationClassUIDItem(),
      versionItem = new ImplementationVersionNameItem();

    classUIDItem.setImplementationClassUID(C.IMPLEM_UID);
    versionItem.setImplementationVersionName(C.IMPLEM_VERSION);
    var packageSize = this.options.maxPackageSize ? this.options.maxPackageSize : C.DEFAULT_MAX_PACKAGE_SIZE;
    maxLengthItem.setMaximumLengthReceived(packageSize);

    var userInfo = new UserInformationItem();
    userInfo.setUserDataItems([maxLengthItem, classUIDItem, versionItem]);

    associateRQ.setUserInformationItem(userInfo);

    this.send(associateRQ);
};

CSocket.prototype.getContext = function(id) {
    for (var k in this.presentationContexts) {
        var ctx = this.presentationContexts[k];
        if (id == ctx.id) return ctx;
    }
    return null;
}

CSocket.prototype.getSyntax = function(contextId) {
    if (!this.negotiatedContexts[contextId]) return null;

    return this.negotiatedContexts[contextId].transferSyntax;
}

CSocket.prototype.getContextByUID = function(uid) {
    for (var k in this.negotiatedContexts) {
        var ctx = this.negotiatedContexts[k];
        if (ctx.abstractSyntax == uid) {
            return ctx;
        }
    }
    return null;
}

CSocket.prototype.getContextId = function(contextId) {
    if (!this.negotiatedContexts[contextId]) return null;

    return this.negotiatedContexts[contextId].id;
}

CSocket.prototype.setPresentationContexts = function(uids) {
    var contexts = [],
        id = 0;
    uids.forEach(function(uid) {
        contexts.push({
            id: ++id,
            abstractSyntax: uid,
            transferSyntaxes: [C.IMPLICIT_LITTLE_ENDIAN, C.EXPLICIT_LITTLE_ENDIAN, C.EXPLICIT_BIG_ENDIAN]
        });
    });
    this.presentationContexts = contexts;
};

CSocket.prototype.newMessageId = function() {
    return (++this.messageIdCounter) % 65536;
}

CSocket.prototype.resetReceive = function() {
    this.receiving = this.receiveLength = null;
};

CSocket.prototype.send = function(pdu, afterCbk) {
    //console.log('SEND PDU-TYPE: ', pdu.type);
    var toSend = pdu.buffer();
    //console.log('send buffer', toSend.toString('hex'));
    this.socket.write(toSend, afterCbk ? afterCbk : function() {
        //console.log('Data written');
    });
};

CSocket.prototype.release = function() {
    var releaseRQ = new ReleaseRQ();
    this.send(releaseRQ);
};

CSocket.prototype.released = function() {
    this.socket.end();
};

CSocket.prototype.ready = function() {
    console.log("Connection established");
    this.connected = true;
    this.started = time();

    var o = this;
    if (this.options.idle) {
        this.intervalId = setInterval(function() {
            o.checkIdle();
        }, 3000);
    }
};

CSocket.prototype.checkIdle = function() {
    var current = time(),
        idl = this.options.idle;

    if (!this.lastReceived && (current - this.started >= idl)) {
        this.idleClose();
    } else if (this.lastReceived && (current - this.lastReceived >= idl)) {
        this.idleClose();
    } else {
        //console.log('keep idling')
    }
};

CSocket.prototype.idleClose = function() {
    console.log('Exceed idle time, closing connection');
    this.release();
};

CSocket.prototype.received = function(data) {
    var i = 0;
    do {
        data = this.process(data);
    } while (data !== null);
    this.lastReceived = time();
};

CSocket.prototype.process = function(data) {
    //console.log("Data received");
    if (this.receiving === null) {
        if (this.minRecv) {
            data = Buffer.concat([this.minRecv, data], this.minRecv.length + data.length);
            this.minRecv = null;
        }

        if (data.length < 6) {
            this.minRecv = data;
            return null;
        }

        var stream = new ReadStream(data);
        var type = stream.read(C.TYPE_UINT8);
        stream.increment(1);
        var len = stream.read(C.TYPE_UINT32),
            cmp = data.length - 6;
        if (len > cmp) {
            this.receiving = data;
            this.receiveLength = len;
        } else {
            var process = data,
                remaining = null;
            if (len < cmp) {
                process = data.slice(0, len + 6);
                remaining = data.slice(len + 6, cmp + 6);
            }
            this.resetReceive();
            this.interpret(new ReadStream(process), this);
            if (remaining) {
                return remaining;
            }
        }
    } else {
        var newData = Buffer.concat([this.receiving, data], this.receiving.length + data.length),
            pduLength = newData.length - 6;

        if (pduLength < this.receiveLength) {
            this.receiving = newData;
        } else {
            var remaining = null;
            if (pduLength > this.receiveLength) {
                remaining = newData.slice(this.receiveLength + 6, pduLength + 6);
                newData = newData.slice(0, this.receiveLength + 6);
            }
            this.resetReceive();
            this.interpret(new ReadStream(newData));
            if (remaining) {
                return remaining;
            }
        }
    }
    return null;
};

CSocket.prototype.interpret = function(stream) {
    var pdatas = [],
        size = stream.size(),
        o = this;
    while (stream.offset < size) {
        var pdu = PDU.createByStream(stream);
        //console.log("Received PDU-TYPE " + PDU.typeToString(pdu.type));
        if (pdu.is(C.ITEM_TYPE_PDU_ASSOCIATE_AC)) {
            pdu.presentationContextItems.forEach(function(ctx) {
                var requested = o.getContext(ctx.presentationContextID);
                if (!requested) {
                    throw "Accepted presentation context not found";
                }
                o.negotiatedContexts[ctx.presentationContextID] = {
                    id: ctx.presentationContextID,
                    transferSyntax: ctx.transferSyntaxesItems[0].transferSyntaxName,
                    abstractSyntax: requested.abstractSyntax
                };
            });

            //console.log('Accepted');
            this.associated = true;
            this.emit('associated', pdu);
        } else if (pdu.is(C.ITEM_TYPE_PDU_ASSOCIATE_RQ)) {
            var accepd = new AssociateAC();

            pdu.presentationContextItems.forEach(function(ctx) {

            });
        } else if (pdu.is(C.ITEM_TYPE_PDU_RELEASE_RP)) {
            //console.log('Released');
            this.associated = false;
            this.emit('released');
        } else if (pdu.is(C.ITEM_TYPE_PDU_AABORT)) {
            //console.log('Aborted');
            this.emit('aborted');
        } else if (pdu.is(C.ITEM_TYPE_PDU_PDATA)) {
            pdatas.push(pdu);
        }
    }

    if (pdatas) {
        var pdvs = this.pendingPDVs ? this.pendingPDVs : [];
        pdatas.forEach(function(pdata) {
            pdvs = pdvs.concat(pdata.presentationDataValueItems);
        });
        this.pendingPDVs = null;
        var i = 0,
            count = pdvs.length;
        while (i < count) {
            if (!pdvs[i].isLast) {
                var j = i + 1;
                while (j < count) {
                    pdvs[i].messageStream.concat(pdvs[j].messageStream);
                    if (pdvs[j++].isLast) {
                        pdvs[i].isLast = true;
                        break;
                    }
                }
                if (pdvs[i].isLast) {
                    this.emit('message', pdvs[i]);
                } else {
                    this.pendingPDVs = [pdvs[i]];
                }
                i = j;
            } else {
                this.emit('message', pdvs[i++]);
            }
        }
    }
};

CSocket.prototype.receivedMessage = function(pdv) {
    var syntax = this.getSyntax(pdv.contextId),
        msg = DicomMessage.read(pdv.messageStream, pdv.type, syntax, this.options.vr);

    if (msg.isCommand()) {
        this.lastCommand = msg;

        if (msg.isResponse()) {
            if (msg.is(C.COMMAND_C_GET_RSP) || msg.is(C.COMMAND_C_MOVE_RSP)) {
                //console.log('remaining', msg.getNumOfRemainingSubOperations(), msg.getNumOfCompletedSubOperations());
            }
            if (msg.failure()) {
                //console.log("message failed with status ", msg.getStatus().toString(16));
            }
            if (msg.isFinal()) {
                var replyId = msg.respondedTo();
                if (this.messages[replyId].listener) {
                    this.messages[replyId].listener.emit('end', msg);

                    if (!msg.haveData())
                        delete this.messages[replyId];
                }

                if (msg.is(C.COMMAND_C_GET_RSP)) {
                    if (!msg.getNumOfRemainingSubOperations()) {
                        if (this.lastGets && this.lastGets.length > 0) this.lastGets.shift();
                    }
                }
            }
        } else {
            /*if (msg.is(0x01)) {
        console.log('ae title ', msg.getValue(0x00001031))
      }*/
        }

    } else {
        if (!this.lastCommand) {
            throw "Only dataset?";
        } else if (!this.lastCommand.haveData()) {
            throw "Last command didn't indicate presence of data";
        }

        if (this.lastCommand.isResponse()) {
            var replyId = this.lastCommand.respondedTo();
            if (this.messages[replyId].listener) {
                var flag = this.lastCommand.failure() ? true : false;

                this.messages[replyId].listener.emit("result", msg, flag);

                if (this.lastCommand.failure()) {
                    delete this.messages[replyId];
                }
            }
        } else {
            if (this.lastCommand.is(C.COMMAND_C_STORE_RQ)) {
                var moveMessageId = this.lastCommand.getMoveMessageId(),
                    useId = moveMessageId;
                if (!moveMessageId) {
                    //!! Going to deprecate now
                    //kinda hacky but we know this c-store is came from a c-get
                    if (this.lastGets.length > 0) {
                        useId = this.lastGets[0];
                    } else {
                        throw "Where does this c-store came from?";
                    }
                } else console.log('move ', moveMessageId);
                //this.storeResponse(useId, msg);
            }
        }
    }
};

CSocket.prototype.wrapToPData = function(message, context) {
    var useContext = message.contextUID ? message.contextUID : context;
    var ctx = this.getContextByUID(useContext);

    var pdata = new PDataTF(),
        pdv = new PresentationDataValueItem(ctx.id);
    pdv.setMessage(message);
    pdata.setPresentationDataValueItems([pdv]);   
    return pdata;
}

CSocket.prototype.sendMessage = function(context, command, dataset) {
    var nContext = this.getContextByUID(context),
        syntax = nContext.transferSyntax,
        cid = nContext.id,
        messageId = this.newMessageId(),
        msgData = {};

    msgData.listener = new Envelope(command);

    var o = this;
    msgData.listener.on('cancel', function(){
        var cancelMessage = null;
        if (this.command.is(C.COMMAND_C_FIND_RQ) || this.command.is(C.COMMAND_C_MOVE_RQ)) {
            cancelMessage = new CCancelRQ();

            cancelMessage.setReplyMessageId(this.command.messageId);
            cancelMessage.setSyntax(C.IMPLICIT_LITTLE_ENDIAN);

            o.send(o.wrapToPData(cancelMessage, this.command.contextUID));           
        }
    });

    command.setSyntax(C.IMPLICIT_LITTLE_ENDIAN);
    command.setContextId(context);
    command.setMessageId(messageId);
    if (dataset)
        command.setDataSetPresent(C.DATA_SET_PRESENT);

    this.lastSent = command;
    if (command.is(C.COMMAND_C_GET_RQ)) {
        this.lastGets.push(messageId);
    }
    var pdata = this.wrapToPData(command);

    msgData.command = command;
    this.messages[messageId] = msgData;

    this.send(pdata);
    if (dataset) {
        dataset.setSyntax(syntax);
        var dsData = new PDataTF(),
            dPdv = new PresentationDataValueItem(cid);

        dPdv.setMessage(dataset);
        dsData.setPresentationDataValueItems([dPdv]);
        this.send(dsData);
    }
    return msgData.listener;
};

CSocket.prototype.verify = function() {
    this.setPresentationContexts([C.SOP_VERIFICATION]);
    this.startAssociationRequest(function() {
        //associated, we can release now
        this.release();
    });
};

CSocket.prototype.wrapMessage = function(data) {
    if (data) {
        var datasetMessage = new DataSetMessage();
        datasetMessage.setElements(data);
        return datasetMessage;
    } else return data;
};

CSocket.prototype.find = function(params, options) {
    return this.sendMessage(options.context, new CFindRQ(), this.wrapMessage(params));
};

CSocket.prototype.move = function(destination, params, options) {
    var moveMessage = new CMoveRQ();
    moveMessage.setDestination(destination);

    return this.sendMessage(options.context, moveMessage, this.wrapMessage(params));
};

CSocket.prototype.moveInstances = function(destination, params, options) {
    var sendParams = Object.assign({
        0x00080052: C.QUERY_RETRIEVE_LEVEL_IMAGE,
    }, params);
    options = Object.assign({
        context : C.SOP_STUDY_ROOT_MOVE
    }, options);     

    return this.move(destination, sendParams, options);
};

CSocket.prototype.findPatients = function(params, options) {
    var sendParams = Object.assign({
        0x00080052: C.QUERY_RETRIEVE_LEVEL_PATIENT,
        0x00100010: "",
        0x00100020: "",
        0x00100030: "",
        0x00100040: "",
    }, params);
    options = Object.assign({
        context : C.SOP_PATIENT_ROOT_FIND
    }, options);

    return this.find(sendParams, options);
};

CSocket.prototype.findStudies = function(params, options) {
    var sendParams = Object.assign({
        0x00080052: C.QUERY_RETRIEVE_LEVEL_STUDY,
        0x00080020: "",
        0x00100010: "",
        0x00080061: "",
        0x0020000D: ""
    }, params);
    options = Object.assign({
        context : C.SOP_STUDY_ROOT_FIND
    }, options);   

    return this.find(sendParams, options);
};

CSocket.prototype.findSeries = function(params, options) {
    var sendParams = Object.assign({
        0x00080052: C.QUERY_RETRIEVE_LEVEL_SERIES,
        0x00080020: "",
        0x0020000E: "",
        0x0008103E: "",
        0x0020000D: ""
    }, params);
    options = Object.assign({
        context : C.SOP_STUDY_ROOT_FIND
    }, options);     

    return this.find(sendParams, options);
};

CSocket.prototype.findInstances = function(params, options) {
    var sendParams = Object.assign({
        0x00080052: C.QUERY_RETRIEVE_LEVEL_IMAGE,
        0x00080020: "",
        0x0020000E: "",
        0x0008103E: "",
        0x0020000D: ""
    }, params);
    options = Object.assign({
        context : C.SOP_STUDY_ROOT_FIND
    }, options);     

    return this.find(sendParams, options);
};