package wsflate

import (
	"fmt"
	"strconv"

	"github.com/gobwas/httphead"
)

const (
	ExtensionName = "permessage-deflate"

	serverNoContextTakeover = "server_no_context_takeover"
	clientNoContextTakeover = "client_no_context_takeover"
	serverMaxWindowBits     = "server_max_window_bits"
	clientMaxWindowBits     = "client_max_window_bits"
)

var (
	ExtensionNameBytes = []byte(ExtensionName)

	serverNoContextTakeoverBytes = []byte(serverNoContextTakeover)
	clientNoContextTakeoverBytes = []byte(clientNoContextTakeover)
	serverMaxWindowBitsBytes     = []byte(serverMaxWindowBits)
	clientMaxWindowBitsBytes     = []byte(clientMaxWindowBits)
)

var windowBits [8][]byte

func init() {
	for i := range windowBits {
		windowBits[i] = []byte(strconv.Itoa(i + 8))
	}
}

// Parameters contains compression extension options.
type Parameters struct {
	ServerNoContextTakeover bool
	ClientNoContextTakeover bool
	ServerMaxWindowBits     WindowBits
	ClientMaxWindowBits     WindowBits
}

// WindowBits specifies window size accordingly to RFC.
// Use its Bytes() method to obtain actual size of window in bytes.
type WindowBits byte

// Defined reports whether window bits were specified.
func (b WindowBits) Defined() bool {
	return b > 0
}

// Bytes returns window size in number of bytes.
func (b WindowBits) Bytes() int {
	return 1 << uint(b)
}

const (
	MaxLZ77WindowSize = 32768 // 2^15
)

// Parse reads parameters from given HTTP header option accordingly to RFC.
//
// It returns non-nil error at least in these cases:
//   - The negotiation offer contains an extension parameter not defined for
//   use in an offer/response.
//   - The negotiation offer/response contains an extension parameter with an
//   invalid value.
//   - The negotiation offer/response contains multiple extension parameters
//   with
// the same name.
func (p *Parameters) Parse(opt httphead.Option) (err error) {
	const (
		clientMaxWindowBitsSeen = 1 << iota
		serverMaxWindowBitsSeen
		clientNoContextTakeoverSeen
		serverNoContextTakeoverSeen
	)

	// Reset to not mix parsed data from previous Parse() calls.
	*p = Parameters{}

	var seen byte
	opt.Parameters.ForEach(func(key, val []byte) (ok bool) {
		switch string(key) {
		case clientMaxWindowBits:
			if len(val) == 0 {
				p.ClientMaxWindowBits = 1
				return true
			}
			if seen&clientMaxWindowBitsSeen != 0 {
				err = paramError("duplicate", key, val)
				return false
			}
			seen |= clientMaxWindowBitsSeen
			if p.ClientMaxWindowBits, ok = bitsFromASCII(val); !ok {
				err = paramError("invalid", key, val)
				return false
			}

		case serverMaxWindowBits:
			if len(val) == 0 {
				err = paramError("invalid", key, val)
				return false
			}
			if seen&serverMaxWindowBitsSeen != 0 {
				err = paramError("duplicate", key, val)
				return false
			}
			seen |= serverMaxWindowBitsSeen
			if p.ServerMaxWindowBits, ok = bitsFromASCII(val); !ok {
				err = paramError("invalid", key, val)
				return false
			}

		case clientNoContextTakeover:
			if len(val) > 0 {
				err = paramError("invalid", key, val)
				return false
			}
			if seen&clientNoContextTakeoverSeen != 0 {
				err = paramError("duplicate", key, val)
				return false
			}
			seen |= clientNoContextTakeoverSeen
			p.ClientNoContextTakeover = true

		case serverNoContextTakeover:
			if len(val) > 0 {
				err = paramError("invalid", key, val)
				return false
			}
			if seen&serverNoContextTakeoverSeen != 0 {
				err = paramError("duplicate", key, val)
				return false
			}
			seen |= serverNoContextTakeoverSeen
			p.ServerNoContextTakeover = true

		default:
			err = paramError("unexpected", key, val)
			return false
		}
		return true
	})
	return
}

// Option encodes parameters into HTTP header option.
func (p Parameters) Option() httphead.Option {
	opt := httphead.Option{
		Name: ExtensionNameBytes,
	}
	setBool(&opt, serverNoContextTakeoverBytes, p.ServerNoContextTakeover)
	setBool(&opt, clientNoContextTakeoverBytes, p.ClientNoContextTakeover)
	setBits(&opt, serverMaxWindowBitsBytes, p.ServerMaxWindowBits)
	setBits(&opt, clientMaxWindowBitsBytes, p.ClientMaxWindowBits)
	return opt
}

func isValidBits(x int) bool {
	return 8 <= x && x <= 15
}

func bitsFromASCII(p []byte) (WindowBits, bool) {
	n, ok := httphead.IntFromASCII(p)
	if !ok || !isValidBits(n) {
		return 0, false
	}
	return WindowBits(n), true
}

func setBits(opt *httphead.Option, name []byte, bits WindowBits) {
	if bits == 0 {
		return
	}
	if bits == 1 {
		opt.Parameters.Set(name, nil)
		return
	}
	if !isValidBits(int(bits)) {
		panic(fmt.Sprintf("wsflate: invalid bits value: %d", bits))
	}
	opt.Parameters.Set(name, windowBits[bits-8])
}

func setBool(opt *httphead.Option, name []byte, flag bool) {
	if flag {
		opt.Parameters.Set(name, nil)
	}
}

func paramError(reason string, key, val []byte) error {
	return fmt.Errorf(
		"wsflate: %s extension parameter %q: %q",
		reason, key, val,
	)
}
