/* Go IPP - IPP core protocol implementation in pure Go
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * IPP Message decoder
 */

package goipp

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

// DecoderOptions represents message decoder options
type DecoderOptions struct {
	// EnableWorkarounds, if set to true, enables various workarounds
	// for decoding IPP messages that violate IPP protocol specification
	//
	// Currently it includes the following workarounds:
	// * Pantum M7300FDW violates collection encoding rules.
	//   Instead of using TagMemberName, it uses named attributes
	//   within the collection
	//
	// The list of implemented workarounds may grow in the
	// future
	EnableWorkarounds bool
}

// messageDecoder represents Message decoder
type messageDecoder struct {
	in  io.Reader      // Input stream
	off int            // Offset of last read
	cnt int            // Count of read bytes
	opt DecoderOptions // Options
}

// Decode the message
func (md *messageDecoder) decode(m *Message) error {
	// Wire format:
	//
	//   2 bytes:  Version
	//   2 bytes:  Code (Operation or Status)
	//   4 bytes:  RequestID
	//   variable: attributes
	//   1 byte:   TagEnd

	// Parse message header
	var err error
	m.Version, err = md.decodeVersion()
	if err == nil {
		m.Code, err = md.decodeCode()
	}
	if err == nil {
		m.RequestID, err = md.decodeU32()
	}

	// Now parse attributes
	done := false
	var group *Attributes
	var attr Attribute
	var prev *Attribute

	for err == nil && !done {
		var tag Tag
		tag, err = md.decodeTag()

		if err != nil {
			break
		}

		if tag.IsDelimiter() {
			prev = nil
		}

		if tag.IsGroup() {
			m.Groups.Add(Group{tag, nil})
		}

		switch tag {
		case TagZero:
			err = errors.New("Invalid tag 0")
		case TagEnd:
			done = true

		case TagOperationGroup:
			group = &m.Operation
		case TagJobGroup:
			group = &m.Job
		case TagPrinterGroup:
			group = &m.Printer
		case TagUnsupportedGroup:
			group = &m.Unsupported
		case TagSubscriptionGroup:
			group = &m.Subscription
		case TagEventNotificationGroup:
			group = &m.EventNotification
		case TagResourceGroup:
			group = &m.Resource
		case TagDocumentGroup:
			group = &m.Document
		case TagSystemGroup:
			group = &m.System
		case TagFuture11Group:
			group = &m.Future11
		case TagFuture12Group:
			group = &m.Future12
		case TagFuture13Group:
			group = &m.Future13
		case TagFuture14Group:
			group = &m.Future14
		case TagFuture15Group:
			group = &m.Future15

		default:
			// Decode attribute
			if tag == TagMemberName || tag == TagEndCollection {
				err = fmt.Errorf("Unexpected tag %s", tag)
			} else {
				attr, err = md.decodeAttribute(tag)
			}

			if err == nil && tag == TagBeginCollection {
				attr.Values[0].V, err = md.decodeCollection()
			}

			// If everything is OK, save attribute
			switch {
			case err != nil:
			case attr.Name == "":
				if prev != nil {
					prev.Values.Add(attr.Values[0].T, attr.Values[0].V)

					// Append value to the last Attribute of the
					// last Group in the m.Groups
					//
					// Note, if we are here, this last Attribute definitely exists,
					// because:
					//   * prev != nil
					//   * prev is set when new named attribute is added
					//   * prev is reset when delimiter tag is encountered
					gLast := &m.Groups[len(m.Groups)-1]
					aLast := &gLast.Attrs[len(gLast.Attrs)-1]
					aLast.Values.Add(attr.Values[0].T, attr.Values[0].V)
				} else {
					err = errors.New("Additional value without preceding attribute")
				}
			case group != nil:
				group.Add(attr)
				prev = &(*group)[len(*group)-1]
				m.Groups[len(m.Groups)-1].Add(attr)
			default:
				err = errors.New("Attribute without a group")
			}
		}
	}

	if err != nil {
		err = fmt.Errorf("%s at 0x%x", err, md.off)
	}

	return err
}

// Decode a Collection
//
// Collection is like a nested object - an attribute which value is a sequence
// of named attributes. Collections can be nested.
//
// Wire format:
//
//	ATTR: Tag = TagBeginCollection,            - the outer attribute that
//	      Name = "name", value - ignored         contains the collection
//
//	ATTR: Tag = TagMemberName, name = "",      - member name \
//	      value - string, name of the next                    |
//	      member                                              | repeated for
//	                                                          | each member
//	ATTR: Tag = any attribute tag, name = "",  - repeated for |
//	      value = member value                   multi-value /
//	                                             members
//
//	ATTR: Tag = TagEndCollection, name = "",
//	      value - ignored
//
// The format looks a bit baroque, but please note that it was added
// in the IPP 2.0. For IPP 1.x collection looks like a single multi-value
// TagBeginCollection attribute (attributes without names considered
// next value for the previously defined named attributes) and so
// 1.x parser silently ignores collections and doesn't get confused
// with them.
func (md *messageDecoder) decodeCollection() (Collection, error) {
	collection := make(Collection, 0)

	memberName := ""

	for {
		tag, err := md.decodeTag()
		if err != nil {
			return nil, err
		}

		// Delimiter cannot be inside a collection
		if tag.IsDelimiter() {
			err = fmt.Errorf("Collection: unexpected tag %s", tag)
			return nil, err
		}

		// Check for TagMemberName without the subsequent value attribute
		if (tag == TagMemberName || tag == TagEndCollection) && memberName != "" {
			err = fmt.Errorf("Collection: unexpected %s, expected value tag", tag)
			return nil, err
		}

		// Fetch next attribute
		attr, err := md.decodeAttribute(tag)
		if err != nil {
			return nil, err
		}

		// Process next attribute
		switch tag {
		case TagEndCollection:
			return collection, nil

		case TagMemberName:
			memberName = string(attr.Values[0].V.(String))
			if memberName == "" {
				err = fmt.Errorf("Collection: %s value is empty", tag)
				return nil, err
			}

		case TagBeginCollection:
			// Decode nested collection
			attr.Values[0].V, err = md.decodeCollection()
			if err != nil {
				return nil, err
			}
			fallthrough

		default:
			if md.opt.EnableWorkarounds &&
				memberName == "" && attr.Name != "" {
				// Workaround for: Pantum M7300FDW
				//
				// This device violates collection encoding rules.
				// Instead of using TagMemberName, it uses named
				// attributes within the collection
				memberName = attr.Name
			}

			if memberName != "" {
				attr.Name = memberName
				collection = append(collection, attr)
				memberName = ""
			} else if len(collection) > 0 {
				l := len(collection)
				collection[l-1].Values.Add(tag, attr.Values[0].V)
			} else {
				// We've got a value without preceding TagMemberName
				err = fmt.Errorf("Collection: unexpected %s, expected %s", tag, TagMemberName)
				return nil, err
			}
		}
	}
}

// Decode a tag
func (md *messageDecoder) decodeTag() (Tag, error) {
	t, err := md.decodeU8()

	return Tag(t), err
}

// Decode a Version
func (md *messageDecoder) decodeVersion() (Version, error) {
	code, err := md.decodeU16()
	return Version(code), err
}

// Decode a Code
func (md *messageDecoder) decodeCode() (Code, error) {
	code, err := md.decodeU16()
	return Code(code), err
}

// Decode a single attribute
//
// Wire format:
//
//	1   byte:   Tag
//	2+N bytes:  Name length (2 bytes) + name string
//	2+N bytes:  Value length (2 bytes) + value bytes
//
// For the extended tag format, Tag is encoded as TagExtension and
// 4 bytes of the actual tag value prepended to the value bytes
func (md *messageDecoder) decodeAttribute(tag Tag) (Attribute, error) {
	var attr Attribute
	var value []byte
	var err error

	// Obtain attribute name and raw value
	attr.Name, err = md.decodeString()
	if err != nil {
		goto ERROR
	}

	value, err = md.decodeBytes()
	if err != nil {
		goto ERROR
	}

	// Handle TagExtension
	if tag == TagExtension {
		if len(value) < 4 {
			err = errors.New("Extension tag truncated")
			goto ERROR
		}

		t := binary.BigEndian.Uint32(value[:4])

		if t > 0x7fffffff {
			err = fmt.Errorf(
				"Extension tag 0x%8.8x out of range", t)
			goto ERROR
		}
	}

	// Unpack value
	err = attr.unpack(tag, value)
	if err != nil {
		goto ERROR
	}

	return attr, nil

	// Return a error
ERROR:
	return Attribute{}, err
}

// Decode a 8-bit integer
func (md *messageDecoder) decodeU8() (uint8, error) {
	buf := make([]byte, 1)
	err := md.read(buf)
	return buf[0], err
}

// Decode a 16-bit integer
func (md *messageDecoder) decodeU16() (uint16, error) {
	buf := make([]byte, 2)
	err := md.read(buf)
	return binary.BigEndian.Uint16(buf[:]), err
}

// Decode a 32-bit integer
func (md *messageDecoder) decodeU32() (uint32, error) {
	buf := make([]byte, 4)
	err := md.read(buf)
	return binary.BigEndian.Uint32(buf[:]), err
}

// Decode sequence of bytes
func (md *messageDecoder) decodeBytes() ([]byte, error) {
	length, err := md.decodeU16()
	if err != nil {
		return nil, err
	}

	data := make([]byte, length)
	err = md.read(data)
	if err != nil {
		return nil, err
	}

	return data, nil
}

// Decode string
func (md *messageDecoder) decodeString() (string, error) {
	data, err := md.decodeBytes()
	if err != nil {
		return "", err
	}

	return string(data), nil
}

// Read a piece of raw data from input stream
func (md *messageDecoder) read(data []byte) error {
	md.off = md.cnt

	for len(data) > 0 {
		n, err := md.in.Read(data)
		if n > 0 {
			md.cnt += n
			data = data[n:]
		} else {
			md.off = md.cnt
			if err == nil || err == io.EOF {
				err = errors.New("Message truncated")
			}
			return err
		}

	}

	return nil
}
