// Copyright 2016, Joe Tsai. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE.md file.

//go:build cgo
// +build cgo

// Package brotli implements the Brotli compressed data format using C wrappers.
package brotli

/*
// This relies upon the shared library built from github.com/google/brotli.
//
// The steps to build and install the shared library is as follows:
//	curl -L https://github.com/google/brotli/archive/v1.0.2.tar.gz | tar -zxv
//	cd brotli-1.0.2
//	mkdir out
//	cd out
//	../configure-cmake
//	make
//	sudo make install

#cgo LDFLAGS: -lbrotlicommon
#cgo LDFLAGS: -lbrotlidec
#cgo LDFLAGS: -lbrotlienc

#include "brotli/decode.h"
#include "brotli/encode.h"

BrotliDecoderState* zbDecCreate() {
	return BrotliDecoderCreateInstance(NULL, NULL, NULL);
}

BrotliDecoderResult zbDecStream(
	BrotliDecoderState* state,
	size_t* avail_in, const uint8_t* next_in,
	size_t* avail_out, uint8_t* next_out
) {
	return BrotliDecoderDecompressStream(
		state, avail_in, &next_in, avail_out, &next_out, NULL
	);
}

void zbDecDestroy(BrotliDecoderState* state) {
	return BrotliDecoderDestroyInstance(state);
}

BrotliEncoderState* zbEncCreate(int level) {
	BrotliEncoderState* state = BrotliEncoderCreateInstance(NULL, NULL, NULL);
	if (state != NULL) {
		BrotliEncoderSetParameter(state, BROTLI_PARAM_QUALITY, level);
	}
	return state;
}

BROTLI_BOOL zbEncStream(
    BrotliEncoderState* state, BrotliEncoderOperation op,
    size_t* avail_in, const uint8_t* next_in,
    size_t* avail_out, uint8_t* next_out
) {
	return BrotliEncoderCompressStream(
		state, op, avail_in, &next_in, avail_out, &next_out, NULL
	);
}

void zbEncDestroy(BrotliEncoderState* state) {
	return BrotliEncoderDestroyInstance(state);
}
*/
import "C"

import (
	"errors"
	"io"
	"unsafe"
)

type reader struct {
	r     io.Reader
	err   error
	state *C.BrotliDecoderState
	buf   []byte
	arr   [1 << 14]byte
}

func NewReader(r io.Reader) io.ReadCloser {
	zr := &reader{r: r, state: C.zbDecCreate()}
	if zr.state == nil {
		panic("brotli: could not allocate decoder state")
	}
	return zr
}

func (zr *reader) Read(buf []byte) (int, error) {
	if zr.state == nil {
		return 0, io.ErrClosedPipe
	}

	var n int
	for zr.err == nil && (len(buf) > 0 && n == 0) {
		availIn, availOut, ptrIn, ptrOut := sizePtrs(zr.buf, buf)
		ret := C.zbDecStream(zr.state, &availIn, ptrIn, &availOut, ptrOut)
		n += len(buf) - int(availOut)
		buf = buf[len(buf)-int(availOut):]
		zr.buf = zr.buf[len(zr.buf)-int(availIn):]

		switch ret {
		case C.BROTLI_DECODER_RESULT_ERROR:
			zr.err = errors.New("brotli: corrupted input")
		case C.BROTLI_DECODER_RESULT_SUCCESS:
			return n, io.EOF
		case C.BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:
			n1 := copy(zr.arr[:], zr.buf)
			n2, err := zr.r.Read(zr.arr[n1:])
			if n2 > 0 {
				zr.buf = zr.arr[:n1+n2]
			} else if err != nil {
				if err == io.EOF {
					err = io.ErrUnexpectedEOF
				}
				zr.err = err
			}
		case C.BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:
			return n, nil
		default:
			zr.err = errors.New("brotli: unknown decoder error")
		}
	}
	return n, zr.err
}

func (zr *reader) Close() error {
	if zr.state != nil {
		defer func() {
			C.zbDecDestroy(zr.state)
			zr.state = nil
		}()
	}
	return zr.err
}

type writer struct {
	w     io.Writer
	err   error
	state *C.BrotliEncoderState
	buf   []byte
	arr   [1 << 14]byte
}

func NewWriter(w io.Writer, level int) io.WriteCloser {
	if level < C.BROTLI_MIN_QUALITY || level > C.BROTLI_MAX_QUALITY {
		panic("brotli: invalid compression level")
	}

	zw := &writer{w: w, state: C.zbEncCreate(C.int(level))}
	if zw.state == nil {
		panic("brotli: could not allocate encoder state")
	}
	return zw
}

func (zw *writer) Write(buf []byte) (int, error) {
	return zw.write(buf, C.BROTLI_OPERATION_PROCESS)
}

func (zw *writer) write(buf []byte, op C.BrotliEncoderOperation) (int, error) {
	if zw.state == nil {
		return 0, io.ErrClosedPipe
	}

	var n int
	flush := op != C.BROTLI_OPERATION_PROCESS
	for zw.err == nil && (len(buf) > 0 || flush) {
		availIn, availOut, ptrIn, ptrOut := sizePtrs(buf, zw.arr[:])
		ret := C.zbEncStream(zw.state, op, &availIn, ptrIn, &availOut, ptrOut)
		n += len(buf) - int(availIn)
		buf = buf[len(buf)-int(availIn):]
		zw.buf = zw.arr[:len(zw.arr)-int(availOut)]

		if len(zw.buf) > 0 {
			if _, err := zw.w.Write(zw.buf); err != nil {
				zw.err = err
			}
		}
		if ret == 0 && zw.err == nil {
			zw.err = errors.New("brotli: compression error")
		}
		if flush && C.BrotliEncoderHasMoreOutput(zw.state) == 0 {
			break
		}
	}
	return n, zw.err
}

func (zw *writer) Close() error {
	if zw.state != nil {
		defer func() {
			C.zbEncDestroy(zw.state)
			zw.state = nil
		}()
		zw.write(nil, C.BROTLI_OPERATION_FINISH)
	}
	return zw.err
}

func sizePtrs(in, out []byte) (sizeIn, sizeOut C.size_t, ptrIn, ptrOut *C.uint8_t) {
	sizeIn = C.size_t(len(in))
	sizeOut = C.size_t(len(out))
	if len(in) > 0 {
		ptrIn = (*C.uint8_t)(unsafe.Pointer(&in[0]))
	}
	if len(out) > 0 {
		ptrOut = (*C.uint8_t)(unsafe.Pointer(&out[0]))
	}
	return
}
