package zstd_0

/*
#include "zstd.h"
*/
import "C"
import (
	"bytes"
	"errors"
	"io/ioutil"
	"unsafe"
)

// ErrorCode is an error returned by the zstd library.
type ErrorCode int

func (e ErrorCode) Error() string {
	switch int(e) {
	case -1:
		return "Error (generic)"
	case -2:
		return "Unknown frame descriptor"
	case -3:
		return "Unsupported frame parameter"
	case -4:
		return "Frame parameter unsupported in 32-bits mode"
	case -5:
		return "Context should be init first"
	case -6:
		return "Allocation error: not enough memory"
	case -7:
		return "Operation not authorized at current processing stage"
	case -8:
		return "Destination buffer is too small"
	case -9:
		return "Src size incorrect"
	case -10:
		return "Corrupted block detected"
	case -11:
		return "tableLog requires too much memory"
	case -12:
		return "Unsupported max possible Symbol Value : too large"
	case -13:
		return "Specified maxSymbolValue is too small"
	case -14:
		return "Dictionary is corrupted"
	default:
		return ""
	}
}

const (
	BestSpeed       = 1
	BestCompression = 20
)

var (
	ErrGeneric                           ErrorCode = -1
	ErrPrefixUnknown                     ErrorCode = -2
	ErrFrameParameterUnsupported         ErrorCode = -3
	ErrFrameParameterUnsupportedBy32bits ErrorCode = -4
	ErrInitMissing                       ErrorCode = -5
	ErrMemoryAllocation                  ErrorCode = -6
	ErrStageWrong                        ErrorCode = -7
	ErrDstSizeTooSmall                   ErrorCode = -8
	ErrSrcSizeWrong                      ErrorCode = -9
	ErrCorruptionDetected                ErrorCode = -10
	ErrTableLogTooLarge                  ErrorCode = -11
	ErrMaxSymbolValueTooLarge            ErrorCode = -12
	ErrMaxSymbolValueTooSmall            ErrorCode = -13
	ErrDictionaryCorrupted               ErrorCode = -14
	ErrEmptySlice                                  = errors.New("Bytes slice is empty")

	DefaultCompression = 5
)

// CompressBound returns the worst case size needed for a destination buffer,
// which can be used to preallocate a destination buffer or select a previously
// allocated buffer from a pool.
func CompressBound(srcSize int) int {
	return 512 + srcSize + (srcSize >> 7) + 12
}

// cCompressBound is a cgo call to check the go implementation above against the c code.
func cCompressBound(srcSize int) int {
	return int(C.ZSTD_0_5_X_compressBound(C.size_t(srcSize)))
}

// getError returns an error for the return code, or nil if it's not an error
func getError(code int) error {
	if code < 0 && code >= -14 {
		return ErrorCode(code)
	}
	return nil
}

func cIsError(code int) bool {
	return int(C.ZSTD_0_5_X_isError(C.size_t(code))) != 0
}

// Compress src into dst.  If you have a buffer to use, you can pass it to
// prevent allocation.  If it is too small, or if nil is passed, a new buffer
// will be allocated and returned.
func Compress(dst, src []byte) ([]byte, error) {
	return CompressLevel(dst, src, DefaultCompression)
}

// CompressLevel is the same as Compress but you can pass a compression level
func CompressLevel(dst, src []byte, level int) ([]byte, error) {
	bound := CompressBound(len(src))
	if cap(dst) >= bound {
		dst = dst[0:bound] // Reuse dst buffer
	} else {
		dst = make([]byte, bound)
	}

	var cWritten C.size_t
	if len(src) > 0 {
		cWritten = C.ZSTD_0_5_X_compress(
			unsafe.Pointer(&dst[0]),
			C.size_t(len(dst)),
			unsafe.Pointer(&src[0]),
			C.size_t(len(src)),
			C.int(level))
	} else {
		// Special case for when src is empty.
		// If you refactor this, careful not to store unsafe.Pointer(nil) in a
		// variable or else you'll hit
		// https://github.com/golang/go/issues/28606
		cWritten = C.ZSTD_0_5_X_compress(
			unsafe.Pointer(&dst[0]),
			C.size_t(len(dst)),
			unsafe.Pointer(nil),
			C.size_t(len(src)),
			C.int(level))
	}

	written := int(cWritten)
	// Check if the return is an Error code
	if err := getError(written); err != nil {
		return nil, err
	}
	return dst[:written], nil
}

// Decompress src into dst.  If you have a buffer to use, you can pass it to
// prevent allocation.  If it is too small, or if nil is passed, a new buffer
// will be allocated and returned.  By default, len(src)  * 4 is allocated.
// If the buffer is too small, it will retry with a 2x sized dst buffer up to
// 3 times before falling back to the slower stream API (ie. if the compression
// ratio is 32).
func Decompress(dst, src []byte) ([]byte, error) {
	if len(src) == 0 {
		return []byte{}, ErrEmptySlice
	}
	decompress := func(dst, src []byte) ([]byte, error) {

		cWritten := C.ZSTD_0_5_X_decompress(
			unsafe.Pointer(&dst[0]),
			C.size_t(len(dst)),
			unsafe.Pointer(&src[0]),
			C.size_t(len(src)))

		written := int(cWritten)
		// Check error
		if err := getError(written); err != nil {
			return nil, err
		}
		return dst[:written], nil
	}

	if dst == nil {
		// x is the 95 percentile compression ratio of zstd on points.mlti payloads
		dst = make([]byte, len(src)*3)
	}
	for i := 0; i < 3; i++ { // 3 tries to allocate a bigger buffer
		result, err := decompress(dst, src)
		if err != ErrDstSizeTooSmall {
			return result, err
		}
		dst = make([]byte, len(dst)*2) // Grow buffer by 2
	}
	// We failed getting a dst buffer of correct size, use stream API
	r := NewReader(bytes.NewReader(src))
	defer r.Close()
	return ioutil.ReadAll(r)
}
