// https://www.rfc-editor.org/rfc/rfc6455#section-5.2
//
// Frame Format
// 0                   1                   2                   3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-------+-+-------------+-------------------------------+
// |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
// |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
// |N|V|V|V|       |S|             |   (if payload len==126/127)   |
// | |1|2|3|       |K|             |                               |
// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
// |     Extended payload length continued, if payload len == 127  |
// + - - - - - - - - - - - - - - - +-------------------------------+
// |                               |Masking-key, if MASK set to 1  |
// +-------------------------------+-------------------------------+
// | Masking-key (continued)       |          Payload Data         |
// +-------------------------------- - - - - - - - - - - - - - - - +
// :                     Payload Data continued ...                :
// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
// |                     Payload Data continued ...                |
// +---------------------------------------------------------------+

package ws

import (
	"encoding/binary"
	"io"
	"math/rand"
	"net"

	"github.com/nadoo/glider/pool"
)

const (
	hdrSize = 2 + 8 // Fixed header + length

	// byte 0
	finalBit     byte = 1 << 7
	opCodeBinary byte = 2

	// byte 1
	maskBit byte = 1 << 7
)

type frameWriter struct {
	io.Writer
	header  [hdrSize]byte
	server  bool
	maskKey [4]byte
}

// FrameWriter returns a frame writer.
func FrameWriter(w io.Writer, server bool) io.Writer {
	n := rand.Uint32()
	return &frameWriter{
		Writer:  w,
		server:  server,
		maskKey: [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)},
	}
}

func (w *frameWriter) Write(b []byte) (int, error) {
	hdr := w.header
	hdr[0], hdr[1] = opCodeBinary|finalBit, 0
	if !w.server {
		hdr[1] = maskBit
	}

	nPayload, nHeader := len(b), 2
	switch {
	case nPayload <= 125:
		hdr[1] |= byte(nPayload)
	case nPayload < 65536:
		hdr[1] |= 126
		nHeader += 2
		binary.BigEndian.PutUint16(hdr[2:nHeader], uint16(nPayload))
	default:
		hdr[1] |= 127
		nHeader += 8
		binary.BigEndian.PutUint64(hdr[2:nHeader], uint64(nPayload))
	}

	header := hdr[:nHeader]
	if w.server {
		n, err := (&net.Buffers{header, b}).WriteTo(w.Writer)
		if int(n) > nHeader {
			return int(n) - nHeader, err
		}
		return 0, err
	}

	payload := pool.GetBuffer(nPayload)
	defer pool.PutBuffer(payload)

	// payload with mask
	for i := 0; i < nPayload; i++ {
		payload[i] = b[i] ^ w.maskKey[i%4]
	}

	n, err := (&net.Buffers{header, w.maskKey[:], payload}).WriteTo(w.Writer)
	if int(n) > nHeader+4 {
		return int(n) - nHeader - 4, err
	}
	return 0, err
}

type frameReader struct {
	io.Reader
	buf        [8]byte
	left       int64
	server     bool
	maskKey    [4]byte
	maskOffset int
}

// FrameReader returns a chunked reader.
func FrameReader(r io.Reader, server bool) io.Reader {
	return &frameReader{Reader: r, server: server}
}

func (r *frameReader) Read(b []byte) (int, error) {
	if r.left == 0 {
		// get msg header
		_, err := io.ReadFull(r.Reader, r.buf[:2])
		if err != nil {
			return 0, err
		}

		r.left = int64(r.buf[1] & 0x7f)
		switch r.left {
		case 126:
			_, err := io.ReadFull(r.Reader, r.buf[:2])
			if err != nil {
				return 0, err
			}
			r.left = int64(binary.BigEndian.Uint16(r.buf[:2]))
		case 127:
			_, err := io.ReadFull(r.Reader, r.buf[:8])
			if err != nil {
				return 0, err
			}
			r.left = int64(binary.BigEndian.Uint64(r.buf[:8]))
		}

		if r.server {
			_, err := io.ReadFull(r.Reader, r.maskKey[:])
			if err != nil {
				return 0, err
			}
			r.maskOffset = 0
		}
	}

	readLen := int64(len(b))
	if readLen > r.left {
		readLen = r.left
	}

	m, err := io.ReadFull(r.Reader, b[:readLen])
	if err != nil {
		return m, err
	}

	if r.server {
		for i := range b[:m] {
			b[i] = b[i] ^ r.maskKey[(i+r.maskOffset)%4]
		}
		r.maskOffset = (m + r.maskOffset) % 4
	}

	r.left -= int64(m)
	return m, err
}
