/*
Package ibmmq provides a wrapper to a subset of the IBM MQ
procedural interface (the MQI).

In this initial implementation not all the MQI verbs are
included, but it does have the core operations required to
put and get messages and work with topics.

The verbs are given mixed case names without MQ - Open instead
of MQOPEN etc.

If an MQI call returns MQCC_FAILED or MQCC_WARNING, a custom error
type is returned containing the MQCC/MQRC values as
a formatted string. Use mqreturn:= err(*ibmmq.MQReturn) to access
the particular MQRC or MQCC values.

The build directives for Windows assume the header and library files have
been copied to a temporary location, because the default paths are not
acceptable to Go (it does not like spaces or special characters like ~).
Note: This problem appears to have been fixed in Go 1.9, and once that
is fully available, the directives will be changed to a more reasonable
path in this file. For example
    cgo windows CFLAGS -I"c:/Program Files/IBM/MQ/tools/c/include" -m64

The build directives for Linux assume the default MQ installation path
in /opt/mqm. These would need to be changed in this file if you use a
non-default path.
*/
package ibmmq

/*
  Copyright (c) IBM Corporation 2016

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific

   Contributors:
     Mark Taylor - Initial Contribution
*/

/*
#cgo !windows CFLAGS: -I/opt/mqm/inc -D_REENTRANT
#cgo windows CFLAGS:  -I"C:/Program Files/IBM/MQ/Tools/c/include"
#cgo !windows LDFLAGS: -L/opt/mqm/lib64 -lmqm_r -Wl,-rpath=/opt/mqm/lib64 -Wl,-rpath=/usr/lib64
#cgo windows LDFLAGS: -L "C:/Program Files/IBM/MQ/bin64" -lmqm

#include <stdlib.h>
#include <string.h>
#include <cmqc.h>
#include <cmqxc.h>
*/
import "C"

import (
	"encoding/binary"
	"unsafe"
)

/*
   This file contains the C wrappers, calling out to structure-specific
   functions where necessary.

   Define some basic types to hold the
   references to MQ objects - hconn, hobj - and
   a simple way to pass the combination of MQCC/MQRC
   returned from MQI verbs

   The object name is copied into the structures only
   for convenience. It's not really needed, but
   it can sometimes be nice to print which queue an hObj
   refers to during debug.
*/

/*
MQQueueManager contains the connection to the queue manager
*/
type MQQueueManager struct {
	hConn C.MQHCONN
	Name  string
}

/*
MQObject contains a reference to an open object and the associated
queue manager
*/
type MQObject struct {
	hObj C.MQHOBJ
	qMgr *MQQueueManager
	Name string
}

/*
MQReturn holds the MQRC and MQCC values returned from an MQI verb. It
implements the Error() function so is returned as the specific error
from the verbs. See the sample programs for how to access the
MQRC/MQCC values in this returned error.
*/
type MQReturn struct {
	MQCC int32
	MQRC int32
	verb string
}

func (e *MQReturn) Error() string {
	return mqstrerror(e.verb, C.MQLONG(e.MQCC), C.MQLONG(e.MQRC))
}

/*
 * Copy a Go string into a fixed-size C char array such as MQCHAR12
 * Once the string has been copied, it can be immediately freed
 * Empty strings have first char set to 0 in MQI structures
 */
func setMQIString(a *C.char, v string, l int) {
	if len(v) > 0 {
		p := C.CString(v)
		C.strncpy(a, p, (C.size_t)(l))
		C.free(unsafe.Pointer(p))
	} else {
		*a = 0
	}
}

/*
Conn is the function to connect to a queue manager
*/
func Conn(goQMgrName string) (MQQueueManager, error) {
	return Connx(goQMgrName, nil)
}

/*
Connx is the extended function to connect to a queue manager.
*/
func Connx(goQMgrName string, gocno *MQCNO) (MQQueueManager, error) {
	var mqrc C.MQLONG
	var mqcc C.MQLONG
	var mqcno C.MQCNO

	if (C.MQENC_NATIVE % 2) == 0 { // May be needed for conversion later
		endian = binary.LittleEndian
	} else {
		endian = binary.BigEndian
	}

	qMgr := MQQueueManager{}
	mqQMgrName := unsafe.Pointer(C.CString(goQMgrName))
	defer C.free(mqQMgrName)

	// Set up a default CNO if not provided.
	if gocno == nil {
		// Because Go programs are always threaded, and we cannot
		// tell on which thread we might get dispatched, allow handles always to
		// be shareable.
		gocno = NewMQCNO()
		gocno.Options = MQCNO_HANDLE_SHARE_NO_BLOCK
	} else {
		if (gocno.Options & (MQCNO_HANDLE_SHARE_NO_BLOCK |
			MQCNO_HANDLE_SHARE_BLOCK)) == 0 {
			gocno.Options |= MQCNO_HANDLE_SHARE_NO_BLOCK
		}
	}
	copyCNOtoC(&mqcno, gocno)

	C.MQCONNX((*C.MQCHAR)(mqQMgrName), &mqcno, &qMgr.hConn, &mqcc, &mqrc)

	if gocno != nil {
		copyCNOfromC(&mqcno, gocno)
	}

	mqreturn := &MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQCONNX",
	}

	if mqcc != C.MQCC_OK {
		return qMgr, mqreturn
	}

	qMgr.Name = goQMgrName

	return qMgr, nil
}

/*
Disc is the function to disconnect from the queue manager
*/
func (x *MQQueueManager) Disc() error {
	var mqrc C.MQLONG
	var mqcc C.MQLONG

	C.MQDISC(&x.hConn, &mqcc, &mqrc)

	mqreturn := MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQDISC",
	}

	if mqcc != C.MQCC_OK {
		return &mqreturn
	}

	return nil
}

/*
Open an object such as a queue or topic
*/
func (x *MQQueueManager) Open(good *MQOD, goOpenOptions int32) (MQObject, error) {
	var mqrc C.MQLONG
	var mqcc C.MQLONG
	var mqod C.MQOD
	var mqOpenOptions C.MQLONG

	object := MQObject{
		Name: good.ObjectName,
		qMgr: x,
	}

	copyODtoC(&mqod, good)
	mqOpenOptions = C.MQLONG(goOpenOptions)

	C.MQOPEN(x.hConn,
		(C.PMQVOID)(unsafe.Pointer(&mqod)),
		mqOpenOptions,
		&object.hObj,
		&mqcc,
		&mqrc)

	copyODfromC(&mqod, good)

	mqreturn := MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQOPEN",
	}

	if mqcc != C.MQCC_OK {
		return object, &mqreturn
	}

	// ObjectName may have changed because it's a model queue
	object.Name = good.ObjectName

	return object, nil

}

/*
Close the object
*/
func (object *MQObject) Close(goCloseOptions int32) error {
	var mqrc C.MQLONG
	var mqcc C.MQLONG
	var mqCloseOptions C.MQLONG

	mqCloseOptions = C.MQLONG(goCloseOptions)

	C.MQCLOSE(object.qMgr.hConn, &object.hObj, mqCloseOptions, &mqcc, &mqrc)

	mqreturn := MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQCLOSE",
	}

	if mqcc != C.MQCC_OK {
		return &mqreturn
	}

	return nil

}

/*
Sub is the function to subscribe to a topic
*/
func (x *MQQueueManager) Sub(gosd *MQSD, qObject *MQObject) (MQObject, error) {
	var mqrc C.MQLONG
	var mqcc C.MQLONG
	var mqsd C.MQSD

	subObject := MQObject{
		Name: gosd.ObjectName,
		qMgr: x,
	}

	copySDtoC(&mqsd, gosd)

	C.MQSUB(x.hConn,
		(C.PMQVOID)(unsafe.Pointer(&mqsd)),
		&qObject.hObj,
		&subObject.hObj,
		&mqcc,
		&mqrc)

	copySDfromC(&mqsd, gosd)

	mqreturn := MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQSUB",
	}

	if mqcc != C.MQCC_OK {
		return subObject, &mqreturn
	}

	qObject.qMgr = x // Force the correct hConn for managed objects

	return subObject, nil

}

/*
Cmit is the function to commit an in-flight transaction
*/
func (x *MQQueueManager) Cmit() error {
	var mqrc C.MQLONG
	var mqcc C.MQLONG

	C.MQCMIT(x.hConn, &mqcc, &mqrc)

	mqreturn := MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQCMIT",
	}

	if mqcc != C.MQCC_OK {
		return &mqreturn
	}

	return nil

}

/*
Back is the function to backout an in-flight transaction
*/
func (x *MQQueueManager) Back() error {
	var mqrc C.MQLONG
	var mqcc C.MQLONG

	C.MQBACK(x.hConn, &mqcc, &mqrc)

	mqreturn := MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQBACK",
	}

	if mqcc != C.MQCC_OK {
		return &mqreturn
	}

	return nil

}

/*
Put a message to a queue or publish to a topic
*/
func (object MQObject) Put(gomd *MQMD,
	gopmo *MQPMO, buffer []byte) error {
	var mqrc C.MQLONG
	var mqcc C.MQLONG
	var mqmd C.MQMD
	var mqpmo C.MQPMO
	var ptr C.PMQVOID

	bufflen := len(buffer)

	copyMDtoC(&mqmd, gomd)
	copyPMOtoC(&mqpmo, gopmo)

	if bufflen > 0 {
		ptr = (C.PMQVOID)(unsafe.Pointer(&buffer[0]))
	} else {
		ptr = nil
	}

	C.MQPUT(object.qMgr.hConn, object.hObj, (C.PMQVOID)(unsafe.Pointer(&mqmd)),
		(C.PMQVOID)(unsafe.Pointer(&mqpmo)),
		(C.MQLONG)(bufflen),
		ptr,
		&mqcc, &mqrc)

	copyMDfromC(&mqmd, gomd)
	copyPMOfromC(&mqpmo, gopmo)

	mqreturn := MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQPUT",
	}

	if mqcc != C.MQCC_OK {
		return &mqreturn
	}

	return nil
}

/*
Put1 puts a single messsage to a queue or topic. Typically used for one-shot
replies where it can be cheaper than multiple Open/Put/Close
sequences
*/
func (x *MQQueueManager) Put1(good *MQOD, gomd *MQMD,
	gopmo *MQPMO, buffer []byte) error {
	var mqrc C.MQLONG
	var mqcc C.MQLONG
	var mqmd C.MQMD
	var mqpmo C.MQPMO
	var mqod C.MQOD
	var ptr C.PMQVOID

	copyODtoC(&mqod, good)
	copyMDtoC(&mqmd, gomd)
	copyPMOtoC(&mqpmo, gopmo)

	bufflen := len(buffer)

	if bufflen > 0 {
		ptr = (C.PMQVOID)(unsafe.Pointer(&buffer[0]))
	} else {
		ptr = nil
	}

	C.MQPUT1(x.hConn, (C.PMQVOID)(unsafe.Pointer(&mqod)),
		(C.PMQVOID)(unsafe.Pointer(&mqmd)),
		(C.PMQVOID)(unsafe.Pointer(&mqpmo)),
		(C.MQLONG)(bufflen),
		ptr,
		&mqcc, &mqrc)

	copyODfromC(&mqod, good)
	copyMDfromC(&mqmd, gomd)
	copyPMOfromC(&mqpmo, gopmo)

	mqreturn := MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQPUT1",
	}

	if mqcc != C.MQCC_OK {
		return &mqreturn
	}

	return nil

}

/*
Get a message from a queue
The length of the retrieved message is returned.
*/
func (object MQObject) Get(gomd *MQMD,
	gogmo *MQGMO, buffer []byte) (int, error) {

	var mqrc C.MQLONG
	var mqcc C.MQLONG
	var mqmd C.MQMD
	var mqgmo C.MQGMO
	var datalen C.MQLONG
	var ptr C.PMQVOID

	bufflen := len(buffer)

	copyMDtoC(&mqmd, gomd)
	copyGMOtoC(&mqgmo, gogmo)

	if bufflen > 0 {
		ptr = (C.PMQVOID)(unsafe.Pointer(&buffer[0]))
	} else {
		ptr = nil
	}

	C.MQGET(object.qMgr.hConn, object.hObj, (C.PMQVOID)(unsafe.Pointer(&mqmd)),
		(C.PMQVOID)(unsafe.Pointer(&mqgmo)),
		(C.MQLONG)(bufflen),
		ptr,
		&datalen,
		&mqcc, &mqrc)

	godatalen := int(datalen)
	copyMDfromC(&mqmd, gomd)
	copyGMOfromC(&mqgmo, gogmo)

	mqreturn := MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQGET",
	}

	if mqcc != C.MQCC_OK {
		return 0, &mqreturn
	}

	return godatalen, nil

}

/*
Inq is the function to inquire on an attribute of an object

Slices are returned containing the integer attributes, and all the
strings concatenated into a single buffer - the caller needs to know
how long each field in that buffer will be.

The caller passes in how many integer selectors are expected to be
returned, as well as the maximum length of the char buffer to be returned
*/
func (object MQObject) Inq(goSelectors []int32, intAttrCount int, charAttrLen int) ([]int32,
	[]byte, error) {
	var mqrc C.MQLONG
	var mqcc C.MQLONG
	var mqCharAttrs C.PMQCHAR
	var goCharAttrs []byte
	var goIntAttrs []int32
	var ptr C.PMQLONG

	if intAttrCount > 0 {
		goIntAttrs = make([]int32, intAttrCount)
		ptr = (C.PMQLONG)(unsafe.Pointer(&goIntAttrs[0]))
	} else {
		ptr = nil
	}
	if charAttrLen > 0 {
		mqCharAttrs = (C.PMQCHAR)(C.malloc(C.size_t(charAttrLen)))
		defer C.free(unsafe.Pointer(mqCharAttrs))
	} else {
		mqCharAttrs = nil
	}

	// Pass in the selectors directly
	C.MQINQ(object.qMgr.hConn, object.hObj,
		C.MQLONG(len(goSelectors)),
		C.PMQLONG(unsafe.Pointer(&goSelectors[0])),
		C.MQLONG(intAttrCount),
		ptr,
		C.MQLONG(charAttrLen),
		mqCharAttrs,
		&mqcc, &mqrc)

	mqreturn := MQReturn{MQCC: int32(mqcc),
		MQRC: int32(mqrc),
		verb: "MQINQ",
	}

	if mqcc != C.MQCC_OK {
		return nil, nil, &mqreturn
	}

	if charAttrLen > 0 {
		goCharAttrs = C.GoBytes(unsafe.Pointer(mqCharAttrs), C.int(charAttrLen))
	}

	return goIntAttrs, goCharAttrs, nil
}
