package wire

import (
	"crypto/sha256"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
)

const (
	RequestHeaderSize  = 29
	ResponseHeaderSize = 5
	MaxPayload         = 2 * 1024 * 1024

	ModeProbe    byte = 0
	ModeOpen     byte = 1
	ModeUpload   byte = 2
	ModeDownload byte = 3
	ModeClose    byte = 4

	StatusOK    byte = 0
	StatusError byte = 1
	StatusData  byte = 2
	StatusWait  byte = 3
	StatusEOF   byte = 4

	ProbeUpload    byte = 1
	ProbeDownload  byte = 2
	ProbeKeepalive byte = 3
	ProbeBatch     byte = 4
)

var ProbeMagic = [4]byte{'D', 'T', 'P', '2'}

type SessionID [16]byte

type Request struct {
	Mode    byte
	Session SessionID
	Seq     uint64
	Payload []byte
}

func MaskInPlace(data []byte, sid SessionID, mode byte, seq uint64, response bool) {
	if len(data) == 0 {
		return
	}

	var seed [30]byte
	copy(seed[:16], sid[:])
	seed[16] = mode
	binary.BigEndian.PutUint64(seed[17:25], seq)
	if response {
		seed[25] = 1
	}

	var counter uint32
	for off := 0; off < len(data); {
		binary.BigEndian.PutUint32(seed[26:30], counter)
		block := sha256.Sum256(seed[:])
		n := len(data) - off
		if n > len(block) {
			n = len(block)
		}
		for i := 0; i < n; i++ {
			data[off+i] ^= block[i]
		}
		off += n
		counter++
	}
}

func WriteRequest(w io.Writer, mode byte, sid SessionID, seq uint64, plaintext []byte) error {
	if len(plaintext) > MaxPayload {
		return fmt.Errorf("request payload too large: %d", len(plaintext))
	}

	packet := make([]byte, RequestHeaderSize+len(plaintext))
	packet[0] = mode
	copy(packet[1:17], sid[:])
	binary.BigEndian.PutUint64(packet[17:25], seq)
	binary.BigEndian.PutUint32(packet[25:29], uint32(len(plaintext)))
	copy(packet[29:], plaintext)
	MaskInPlace(packet[29:], sid, mode, seq, false)
	return writeAll(w, packet)
}

func ReadRequest(r io.Reader) (Request, error) {
	var req Request
	var header [RequestHeaderSize]byte
	if _, err := io.ReadFull(r, header[:]); err != nil {
		return req, err
	}

	req.Mode = header[0]
	copy(req.Session[:], header[1:17])
	req.Seq = binary.BigEndian.Uint64(header[17:25])
	n := binary.BigEndian.Uint32(header[25:29])
	if n > MaxPayload {
		return req, errors.New("request payload too large")
	}

	if n > 0 {
		req.Payload = make([]byte, int(n))
		if _, err := io.ReadFull(r, req.Payload); err != nil {
			return req, err
		}
		MaskInPlace(req.Payload, req.Session, req.Mode, req.Seq, false)
	}
	return req, nil
}

func WriteResponse(w io.Writer, status byte, body []byte) error {
	if len(body) > MaxPayload {
		return fmt.Errorf("response body too large: %d", len(body))
	}
	packet := make([]byte, ResponseHeaderSize+len(body))
	packet[0] = status
	binary.BigEndian.PutUint32(packet[1:5], uint32(len(body)))
	copy(packet[5:], body)
	return writeAll(w, packet)
}

func WriteMaskedResponse(w io.Writer, status byte, body []byte, sid SessionID, mode byte, seq uint64) error {
	if len(body) > MaxPayload {
		return fmt.Errorf("response body too large: %d", len(body))
	}
	packet := make([]byte, ResponseHeaderSize+len(body))
	packet[0] = status
	binary.BigEndian.PutUint32(packet[1:5], uint32(len(body)))
	copy(packet[5:], body)
	MaskInPlace(packet[5:], sid, mode, seq, true)
	return writeAll(w, packet)
}

func ReadResponse(r io.Reader) (byte, []byte, error) {
	var header [ResponseHeaderSize]byte
	if _, err := io.ReadFull(r, header[:]); err != nil {
		return 0, nil, err
	}
	n := binary.BigEndian.Uint32(header[1:5])
	if n > MaxPayload {
		return 0, nil, errors.New("response body too large")
	}
	var body []byte
	if n > 0 {
		body = make([]byte, int(n))
		if _, err := io.ReadFull(r, body); err != nil {
			return 0, nil, err
		}
	}
	return header[0], body, nil
}

func DecodeMaskedResponse(status byte, body []byte, sid SessionID, mode byte, seq uint64) []byte {
	if len(body) == 0 || status == StatusError {
		return body
	}
	out := append([]byte(nil), body...)
	MaskInPlace(out, sid, mode, seq, true)
	return out
}

func writeAll(w io.Writer, b []byte) error {
	for len(b) > 0 {
		n, err := w.Write(b)
		if err != nil {
			return err
		}
		if n <= 0 {
			return io.ErrShortWrite
		}
		b = b[n:]
	}
	return nil
}
