package jsonapi

import (
	"bytes"
	"encoding/json"
	"errors"
)

var objectSuffix = []byte("{")
var arraySuffix = []byte("[")
var stringSuffix = []byte(`"`)

// A Document represents a JSON API document as specified here: http://jsonapi.org.
type Document struct {
	Links    Links                  `json:"links,omitempty"`
	Data     *DataContainer         `json:"data"`
	Included []Data                 `json:"included,omitempty"`
	Meta     map[string]interface{} `json:"meta,omitempty"`
}

// A DataContainer is used to marshal and unmarshal single objects and arrays
// of objects.
type DataContainer struct {
	DataObject *Data
	DataArray  []Data
}

// UnmarshalJSON unmarshals the JSON-encoded data to the DataObject field if the
// root element is an object or to the DataArray field for arrays.
func (c *DataContainer) UnmarshalJSON(payload []byte) error {
	if bytes.HasPrefix(payload, objectSuffix) {
		return json.Unmarshal(payload, &c.DataObject)
	}

	if bytes.HasPrefix(payload, arraySuffix) {
		return json.Unmarshal(payload, &c.DataArray)
	}

	return errors.New("expected a JSON encoded object or array")
}

// MarshalJSON returns the JSON encoding of the DataArray field or the DataObject
// field. It will return "null" if neither of them is set.
func (c *DataContainer) MarshalJSON() ([]byte, error) {
	if c.DataArray != nil {
		return json.Marshal(c.DataArray)
	}

	return json.Marshal(c.DataObject)
}

// Link represents a link for return in the document.
type Link struct {
	Href string `json:"href"`
	Meta Meta   `json:"meta,omitempty"`
}

// UnmarshalJSON marshals a string value into the Href field or marshals an
// object value into the whole struct.
func (l *Link) UnmarshalJSON(payload []byte) error {
	if bytes.HasPrefix(payload, stringSuffix) {
		return json.Unmarshal(payload, &l.Href)
	}

	if bytes.HasPrefix(payload, objectSuffix) {
		obj := make(map[string]interface{})
		err := json.Unmarshal(payload, &obj)
		if err != nil {
			return err
		}
		var ok bool
		l.Href, ok = obj["href"].(string)
		if !ok {
			return errors.New(`link object expects a "href" key`)
		}

		l.Meta, _ = obj["meta"].(map[string]interface{})
		return nil
	}

	return errors.New("expected a JSON encoded string or object")
}

// MarshalJSON returns the JSON encoding of only the Href field if the Meta
// field is empty, otherwise it marshals the whole struct.
func (l Link) MarshalJSON() ([]byte, error) {
	if len(l.Meta) == 0 {
		return json.Marshal(l.Href)
	}
	return json.Marshal(map[string]interface{}{
		"href": l.Href,
		"meta": l.Meta,
	})
}

// Links contains a map of custom Link objects as given by an element.
type Links map[string]Link

// Meta contains unstructured metadata
type Meta map[string]interface{}

// Data is a general struct for document data and included data.
type Data struct {
	Type          string                  `json:"type"`
	ID            string                  `json:"id"`
	Attributes    json.RawMessage         `json:"attributes"`
	Relationships map[string]Relationship `json:"relationships,omitempty"`
	Links         Links                   `json:"links,omitempty"`
}

// Relationship contains reference IDs to the related structs
type Relationship struct {
	Links Links                      `json:"links,omitempty"`
	Data  *RelationshipDataContainer `json:"data,omitempty"`
	Meta  map[string]interface{}     `json:"meta,omitempty"`
}

// A RelationshipDataContainer is used to marshal and unmarshal single relationship
// objects and arrays of relationship objects.
type RelationshipDataContainer struct {
	DataObject *RelationshipData
	DataArray  []RelationshipData
}

// UnmarshalJSON unmarshals the JSON-encoded data to the DataObject field if the
// root element is an object or to the DataArray field for arrays.
func (c *RelationshipDataContainer) UnmarshalJSON(payload []byte) error {
	if bytes.HasPrefix(payload, objectSuffix) {
		// payload is an object
		return json.Unmarshal(payload, &c.DataObject)
	}

	if bytes.HasPrefix(payload, arraySuffix) {
		// payload is an array
		return json.Unmarshal(payload, &c.DataArray)
	}

	return errors.New("Invalid json for relationship data array/object")
}

// MarshalJSON returns the JSON encoding of the DataArray field or the DataObject
// field. It will return "null" if neither of them is set.
func (c *RelationshipDataContainer) MarshalJSON() ([]byte, error) {
	if c.DataArray != nil {
		return json.Marshal(c.DataArray)
	}
	return json.Marshal(c.DataObject)
}

// RelationshipData represents one specific reference ID.
type RelationshipData struct {
	Type string `json:"type"`
	ID   string `json:"id"`
}
