// Copyright 2021 Molecula Corp. All rights reserved.
package message

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

// Type is a byte indicating the type of a Postgres message.
type Type byte

const (
	// TypeAuthentication is a message used to transfer authentication info.
	TypeAuthentication Type = 'R'

	// TypeReadyForQuery is a message used to indicate that the server is ready for another query.
	TypeReadyForQuery Type = 'Z'

	// TypeCommandComplete is a Backend message used to indicate that a query has completed.
	TypeCommandComplete Type = 'C'

	// TypeClos is a message used to indicate that a query has completed. Frontend
	TypeClose Type = 'C'

	// TypeRowDescription is a message indicating the column types of the result rows from a query.
	TypeRowDescription Type = 'T'

	// TypeDataRow is a message with the contents of a single row.
	TypeDataRow Type = 'D'

	// TypeTermination is a message indicating a request to terminate a connection.
	TypeTermination Type = 'X'

	// TypeNegotiateProtocolVersion is a message used when a client attempts to connect with a newer minor version than the server supports.
	TypeNegotiateProtocolVersion Type = 'v'

	// TypeSimpleQuery is a simple query request.
	TypeSimpleQuery Type = 'Q'

	// TypeBackendKeyData contains a cancellation key for the client to use later.
	TypeBackendKeyData Type = 'K'

	TypeParse         Type = 'P'
	TypeParseComplete Type = '1'

	TypeBind         Type = 'B'
	TypeBindComplete Type = '2'

	TypeExecute            Type = 'E' // Frontend
	TypeError              Type = 'E' // Backend
	TypeSync               Type = 'S' // Frontend
	TypeParameterStatus    Type = 'S' // Backend
	TypeDescribe           Type = 'D' // Frontend
	TypeNoData             Type = 'n' // backend
	TypeEmptyQueryResponse Type = 'I' // backend
)

// AuthenticationOK is a message indicating that authentication has completed.
var AuthenticationOK = Message{
	Type: TypeAuthentication,
	Data: []byte{0, 0, 0, 0},
}
var ParseOK = Message{
	Type: TypeParseComplete,
	Data: []byte{},
}
var BindComplete = Message{
	Type: TypeBindComplete,
	Data: []byte{},
}
var NoData = Message{
	Type: TypeNoData,
	Data: []byte{},
}
var EmptyQueryResponse = Message{
	Type: TypeEmptyQueryResponse,
	Data: []byte{},
}

// Message is a Postgres message value.
type Message struct {
	Type Type
	Data []byte
}

//debug tools
func viewString(b []byte) string {
	r := []rune(string(b))
	for i := range r {
		if r[i] < 32 || r[i] > 126 {
			r[i] = '.'
		}
	}
	return string(r)
}
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}
func (m *Message) Dump(prefix string) {
	n := len(m.Data)
	rowcount := 0
	stop := (n / 8) * 8
	k := 0
	fmt.Printf("\n %s type: '%c'\n", prefix, m.Type)
	for i := 0; i <= stop; i += 8 {
		k++
		if i+8 < n {
			rowcount = 8
		} else {
			rowcount = min(k*8, n) % 8
		}

		fmt.Printf("pos %02d  hex:  ", i)
		for j := 0; j < rowcount; j++ {
			fmt.Printf("%02x  ", m.Data[i+j])
		}
		for j := rowcount; j < 8; j++ {
			fmt.Printf("    ")
		}
		fmt.Printf("  '%s'\n", viewString(m.Data[i:(i+rowcount)]))
	}
}

//endTools

// TransactionStatus is the current transaction state.
type TransactionStatus byte

const (
	// TransactionStatusIdle indicates that there is no active transaction.
	TransactionStatusIdle TransactionStatus = 'I'

	// TransactionStatusActive indicates that the connection currently has an active transaction.
	TransactionStatusActive TransactionStatus = 'T'

	// TransactionStatusFailed indicates that the connection currently has a failed transaction.
	TransactionStatusFailed TransactionStatus = 'E'
)

// Encoder encodes messages.
type Encoder struct {
	buf     bytes.Buffer
	scratch [4]byte
}

func (e *Encoder) i16(i int16) error {
	binary.BigEndian.PutUint16(e.scratch[:2], uint16(i))
	_, err := e.buf.Write(e.scratch[:2])
	return err
}

func (e *Encoder) i32(i int32) error {
	binary.BigEndian.PutUint32(e.scratch[:], uint32(i))
	_, err := e.buf.Write(e.scratch[:])
	return err
}

/* removed for linter now
func (e *Encoder) u32(i uint32) error {
	binary.BigEndian.PutUint32(e.scratch[:], i)
	_, err := e.buf.Write(e.scratch[:])
	return err
}
*/

// ReadyForQuery encodes a "ready for query" message.
func (e *Encoder) ReadyForQuery(status TransactionStatus) (Message, error) {
	e.buf.Reset()

	err := e.buf.WriteByte(byte(status))
	if err != nil {
		return Message{}, err
	}

	return Message{
		Type: TypeReadyForQuery,
		Data: e.buf.Bytes(),
	}, nil
}

// CommandComplete encodes a command completion message.
func (e *Encoder) CommandComplete(tag string) (Message, error) {
	e.buf.Reset()

	_, err := e.buf.WriteString(tag)
	if err != nil {
		return Message{}, err
	}

	err = e.buf.WriteByte(0)
	if err != nil {
		return Message{}, err
	}

	return Message{
		Type: TypeCommandComplete,
		Data: e.buf.Bytes(),
	}, nil
}

// NoticeFieldType indicates the type of a notice/error field.
// https://www.postgresql.org/docs/9.3/protocol-error-fields.html
type NoticeFieldType byte

const (
	// NoticeFieldSeverity indicates the severity of a notice/error.
	NoticeFieldSeverity NoticeFieldType = 'S'

	// NoticeFieldMessage is a short human-readable error/notice message.
	NoticeFieldMessage NoticeFieldType = 'M'

	// NoticeFieldDetail is an optional extended description of the error.
	NoticeFieldDetail NoticeFieldType = 'D'

	// NoticeFieldHint is a suggestion of how to address the issue.
	NoticeFieldHint NoticeFieldType = 'H'

	// NoticeFieldHint the SQLSTATE code for the error (see Appendix A). Not localizable. Always present.
	NoticeFieldCode NoticeFieldType = 'C'
)

// NoticeField is a field in an error or notice.
type NoticeField struct {
	Type NoticeFieldType
	Data string
}

func (e *Encoder) messageOrNotice(fields ...NoticeField) error {
	for _, f := range fields {
		err := e.buf.WriteByte(byte(f.Type))
		if err != nil {
			return err
		}

		_, err = e.buf.WriteString(f.Data)
		if err != nil {
			return err
		}

		err = e.buf.WriteByte(0)
		if err != nil {
			return err
		}
	}

	return e.buf.WriteByte(0)
}

// Error encodes a Postgres error message.
func (e *Encoder) Error(fields ...NoticeField) (Message, error) {
	e.buf.Reset()
	err := e.messageOrNotice(fields...)
	if err != nil {
		return Message{}, err
	}
	return Message{
		Type: TypeError,
		Data: e.buf.Bytes(),
	}, nil
}

// GoError creates a simple Postgres error message from a Go error value.
func (e *Encoder) GoError(err error) (Message, error) {
	return e.Error(
		NoticeField{
			Type: NoticeFieldSeverity,
			Data: "ERROR",
		},
		NoticeField{
			Type: NoticeFieldMessage,
			Data: err.Error(),
		},
	)
}

// ColumnDescription is a description of a data column.
type ColumnDescription struct {
	Name         string
	TableID      int32 //either a table/col id or 0
	FieldID      int16 //either a table/col id or 0
	TypeID       int32 //field type
	TypeLen      int16 //size in bytes of field
	TypeModifier int32 //type modifer?
	Mode         int16 //0=text 1=binary
}

// RowDescription describes the response rows from a query.
func (e *Encoder) RowDescription(cols ...ColumnDescription) (Message, error) {
	if len(cols) >= 1<<15 {
		return Message{}, ErrMessageTooBig
	}

	e.buf.Reset()

	err := e.i16(int16(len(cols)))
	if err != nil {
		return Message{}, nil
	}

	for _, col := range cols {
		_, err := e.buf.WriteString(col.Name)
		if err != nil {
			return Message{}, err
		}
		err = e.buf.WriteByte(0)
		if err != nil {
			return Message{}, err
		}

		err = e.i32(col.TableID)
		if err != nil {
			return Message{}, err
		}

		err = e.i16(col.FieldID)
		if err != nil {
			return Message{}, err
		}

		err = e.i32(col.TypeID)
		if err != nil {
			return Message{}, err
		}

		err = e.i16(col.TypeLen)
		if err != nil {
			return Message{}, err
		}

		err = e.i32(col.TypeModifier)
		if err != nil {
			return Message{}, err
		}

		err = e.i16(col.Mode)
		if err != nil {
			return Message{}, err
		}
	}

	return Message{
		Type: TypeRowDescription,
		Data: e.buf.Bytes(),
	}, nil
}

// TextRow encodes a data row in textual format.
func (e *Encoder) TextRow(row ...string) (Message, error) {
	if len(row) >= 1<<15 {
		return Message{}, ErrMessageTooBig
	}

	e.buf.Reset()

	err := e.i16(int16(len(row)))
	if err != nil {
		return Message{}, err
	}

	for _, val := range row {
		if uint(len(val)) >= 1<<31 {
			return Message{}, ErrMessageTooBig
		}

		err = e.i32(int32(len(val)))
		if err != nil {
			return Message{}, err
		}

		_, err = e.buf.WriteString(val)
		if err != nil {
			return Message{}, err
		}
	}

	return Message{
		Type: TypeDataRow,
		Data: e.buf.Bytes(),
	}, nil
}

// NegotiateProtocolVersion encodes a protocol negotiation packet.
func (e *Encoder) NegotiateProtocolVersion(maxMinor int32, unrecognizedOptions ...string) (Message, error) {
	if uint64(len(unrecognizedOptions)) >= 1<<31 {
		return Message{}, ErrMessageTooBig
	}

	e.buf.Reset()

	err := e.i32(maxMinor)
	if err != nil {
		return Message{}, err
	}

	err = e.i32(int32(len(unrecognizedOptions)))
	if err != nil {
		return Message{}, err
	}
	for _, opt := range unrecognizedOptions {
		_, err = e.buf.WriteString(opt)
		if err != nil {
			return Message{}, err
		}

		err = e.buf.WriteByte(0)
		if err != nil {
			return Message{}, err
		}
	}

	return Message{
		Type: TypeNegotiateProtocolVersion,
		Data: e.buf.Bytes(),
	}, nil
}

// BackendKeyData encodes a Message with a cancellation key.
func (e *Encoder) BackendKeyData(pid, key int32) (Message, error) {
	e.buf.Reset()

	err := e.i32(pid)
	if err != nil {
		return Message{}, err
	}

	err = e.i32(key)
	if err != nil {
		return Message{}, err
	}

	return Message{
		Type: TypeBackendKeyData,
		Data: e.buf.Bytes(),
	}, nil
}

func (e *Encoder) ParameterStatus(param, value string) (Message, error) {
	e.buf.Reset()
	//param + NULL + value+ NULL
	_, err := e.buf.WriteString(param)
	if err != nil {
		return Message{}, err
	}
	err = e.buf.WriteByte(0)
	if err != nil {
		return Message{}, err
	}
	_, err = e.buf.WriteString(value)
	if err != nil {
		return Message{}, err
	}
	err = e.buf.WriteByte(0)
	if err != nil {
		return Message{}, err
	}
	return Message{
		Type: TypeParameterStatus,
		Data: e.buf.Bytes(),
	}, nil
}

type SimpleColumn struct {
	Name    string
	Typeid  int32
	Typelen int16
}

func (e *Encoder) EncodeColumn(name string, typeid int32, typelen int16) (Message, error) {
	return e.EncodeColumns(SimpleColumn{
		Name:    name,
		Typeid:  typeid,
		Typelen: typelen,
	})
}
func (e *Encoder) EncodeColumns(cols ...SimpleColumn) (Message, error) {

	e.buf.Reset()
	err := e.i16(int16(len(cols))) // number of columns in result
	if err != nil {
		return Message{}, nil
	}
	for _, col := range cols {

		_, err = e.buf.WriteString(col.Name) // column name
		if err != nil {
			return Message{}, err
		}
		err = e.buf.WriteByte(0) //null terminate
		if err != nil {
			return Message{}, err
		}

		err = e.i32(0) //tabel id
		if err != nil {
			return Message{}, err
		}

		err = e.i16(0) //field id(attnum)
		if err != nil {
			return Message{}, err
		}

		err = e.i32(col.Typeid) //type_id
		if err != nil {
			return Message{}, err
		}

		err = e.i16(col.Typelen) //type_len
		if err != nil {
			return Message{}, err
		}

		err = e.i32(-1) //type_mod
		if err != nil {
			return Message{}, err
		}

		err = e.i16(0) //format  0 text 1 binary
		if err != nil {
			return Message{}, err
		}
	}
	return Message{
		Type: TypeRowDescription,
		Data: e.buf.Bytes(),
	}, nil
}
