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

package fuse

import (
	"bytes"
	"fmt"
	"log"
	"strings"
	"time"
	"unsafe"
)

var sizeOfOutHeader = unsafe.Sizeof(OutHeader{})
var zeroOutBuf [outputHeaderSize]byte

type request struct {
	inflightIndex int

	cancel chan struct{}

	// written under Server.reqMu
	interrupted bool

	inputBuf []byte

	// These split up inputBuf.
	inHeader *InHeader      // generic header
	inData   unsafe.Pointer // per op data
	arg      []byte         // flat data.

	filenames []string // filename arguments

	// Output data.
	status   Status
	flatData []byte
	fdData   *readResultFd

	// In case of read, keep read result here so we can call
	// Done() on it.
	readResult ReadResult

	// Start timestamp for timing info.
	startTime time.Time

	// All information pertaining to opcode of this request.
	handler *operationHandler

	// Request storage. For large inputs and outputs, use data
	// obtained through bufferpool.
	bufferPoolInputBuf  []byte
	bufferPoolOutputBuf []byte

	// For small pieces of data, we use the following inlines
	// arrays:
	//
	// Output header and structured data.
	outBuf [outputHeaderSize]byte

	// Input, if small enough to fit here.
	smallInputBuf [128]byte
}

func (r *request) clear() {
	r.inputBuf = nil
	r.inHeader = nil
	r.inData = nil
	r.arg = nil
	r.filenames = nil
	r.status = OK
	r.flatData = nil
	r.fdData = nil
	r.startTime = time.Time{}
	r.handler = nil
	r.readResult = nil
}

func (r *request) InputDebug() string {
	val := ""
	if r.handler != nil && r.handler.DecodeIn != nil {
		val = fmt.Sprintf("%v ", Print(r.handler.DecodeIn(r.inData)))
	}

	names := ""
	if r.filenames != nil {
		names = fmt.Sprintf("%q", r.filenames)
	}

	if l := len(r.arg); l > 0 {
		data := ""
		if len(r.filenames) == 0 {
			dots := ""
			if l > 8 {
				l = 8
				dots = "..."
			}

			data = fmt.Sprintf("%q%s", r.arg[:l], dots)
		}

		names += fmt.Sprintf("%s %db", data, len(r.arg))
	}

	return fmt.Sprintf("rx %d: %s i%d %s%s",
		r.inHeader.Unique, operationName(r.inHeader.Opcode), r.inHeader.NodeId,
		val, names)
}

func (r *request) OutputDebug() string {
	var dataStr string
	if r.handler != nil && r.handler.DecodeOut != nil && r.handler.OutputSize > 0 {
		dataStr = Print(r.handler.DecodeOut(r.outData()))
	}

	max := 1024
	if len(dataStr) > max {
		dataStr = dataStr[:max] + fmt.Sprintf(" ...trimmed")
	}

	flatStr := ""
	if r.flatDataSize() > 0 {
		if r.handler != nil && r.handler.FileNameOut {
			s := strings.TrimRight(string(r.flatData), "\x00")
			flatStr = fmt.Sprintf(" %q", s)
		} else {
			spl := ""
			if r.fdData != nil {
				spl = " (fd data)"
			} else {
				l := len(r.flatData)
				s := ""
				if l > 8 {
					l = 8
					s = "..."
				}
				spl = fmt.Sprintf(" %q%s", r.flatData[:l], s)
			}
			flatStr = fmt.Sprintf(" %db data%s", r.flatDataSize(), spl)
		}
	}

	extraStr := dataStr + flatStr
	if extraStr != "" {
		extraStr = ", " + extraStr
	}
	return fmt.Sprintf("tx %d:     %v%s",
		r.inHeader.Unique, r.status, extraStr)
}

// setInput returns true if it takes ownership of the argument, false if not.
func (r *request) setInput(input []byte) bool {
	if len(input) < len(r.smallInputBuf) {
		copy(r.smallInputBuf[:], input)
		r.inputBuf = r.smallInputBuf[:len(input)]
		return false
	}
	r.inputBuf = input
	r.bufferPoolInputBuf = input[:cap(input)]

	return true
}

func (r *request) parseHeader() Status {
	if len(r.inputBuf) < int(unsafe.Sizeof(InHeader{})) {
		log.Printf("Short read for input header: %v", r.inputBuf)
		return EINVAL
	}

	r.inHeader = (*InHeader)(unsafe.Pointer(&r.inputBuf[0]))
	return OK
}

func (r *request) parse() {
	r.arg = r.inputBuf[:]
	r.handler = getHandler(r.inHeader.Opcode)
	if r.handler == nil {
		log.Printf("Unknown opcode %d", r.inHeader.Opcode)
		r.status = ENOSYS
		return
	}

	if len(r.arg) < int(r.handler.InputSize) {
		log.Printf("Short read for %v: %v", operationName(r.inHeader.Opcode), r.arg)
		r.status = EIO
		return
	}

	if r.handler.InputSize > 0 {
		r.inData = unsafe.Pointer(&r.arg[0])
		r.arg = r.arg[r.handler.InputSize:]
	} else {
		r.arg = r.arg[unsafe.Sizeof(InHeader{}):]
	}

	count := r.handler.FileNames
	if count > 0 {
		if count == 1 && r.inHeader.Opcode == _OP_SETXATTR {
			// SETXATTR is special: the only opcode with a file name AND a
			// binary argument.
			splits := bytes.SplitN(r.arg, []byte{0}, 2)
			r.filenames = []string{string(splits[0])}
		} else if count == 1 {
			r.filenames = []string{string(r.arg[:len(r.arg)-1])}
		} else {
			names := bytes.SplitN(r.arg[:len(r.arg)-1], []byte{0}, count)
			r.filenames = make([]string, len(names))
			for i, n := range names {
				r.filenames[i] = string(n)
			}
			if len(names) != count {
				log.Println("filename argument mismatch", names, count)
				r.status = EIO
			}
		}
	}

	copy(r.outBuf[:r.handler.OutputSize+sizeOfOutHeader],
		zeroOutBuf[:r.handler.OutputSize+sizeOfOutHeader])

}

func (r *request) outData() unsafe.Pointer {
	return unsafe.Pointer(&r.outBuf[sizeOfOutHeader])
}

// serializeHeader serializes the response header. The header points
// to an internal buffer of the receiver.
func (r *request) serializeHeader(flatDataSize int) (header []byte) {
	var dataLength uintptr

	if r.handler != nil {
		dataLength = r.handler.OutputSize
	}
	if r.status > OK {
		// only do this for positive status; negative status
		// is used for notification.
		dataLength = 0
	}

	// [GET|LIST]XATTR is two opcodes in one: get/list xattr size (return
	// structured GetXAttrOut, no flat data) and get/list xattr data
	// (return no structured data, but only flat data)
	if r.inHeader.Opcode == _OP_GETXATTR || r.inHeader.Opcode == _OP_LISTXATTR {
		if (*GetXAttrIn)(r.inData).Size != 0 {
			dataLength = 0
		}
	}

	header = r.outBuf[:sizeOfOutHeader+dataLength]
	o := (*OutHeader)(unsafe.Pointer(&header[0]))
	o.Unique = r.inHeader.Unique
	o.Status = int32(-r.status)
	o.Length = uint32(
		int(sizeOfOutHeader) + int(dataLength) + flatDataSize)
	return header
}

func (r *request) flatDataSize() int {
	if r.fdData != nil {
		return r.fdData.Size()
	}
	return len(r.flatData)
}
