package dicom

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"

	"github.com/suyashkumar/dicom/pkg/frame"
	"github.com/suyashkumar/dicom/pkg/tag"
)

// ErrorUnexpectedDataType indicates that an unexpected (not allowed) data type was sent to NewValue.
var ErrorUnexpectedDataType = errors.New("the type of the data was unexpected or not allowed")

// Element represents a standard DICOM data element (see the DICOM standard:
// http://dicom.nema.org/medical/dicom/current/output/html/part05.html#sect_7.1 ).
// This Element can be serialized to JSON out of the box and pretty printed as a string via the String() method.
type Element struct {
	Tag                    tag.Tag    `json:"tag"`
	ValueRepresentation    tag.VRKind `json:"VR"`
	RawValueRepresentation string     `json:"rawVR"`
	ValueLength            uint32     `json:"valueLength"`
	Value                  Value      `json:"value"`
}

func (e *Element) String() string {
	var tagName string
	if tagInfo, err := tag.Find(e.Tag); err == nil {
		tagName = tagInfo.Name
	}
	return fmt.Sprintf("[\n  Tag: %s\n  Tag Name: %s\n  VR: %s\n  VR Raw: %s\n  VL: %d\n  Value: %s\n]\n\n",
		e.Tag.String(),
		tagName,
		e.ValueRepresentation.String(),
		e.RawValueRepresentation,
		e.ValueLength,
		e.Value.String())
}

// Value represents a DICOM value. The underlying data that a Value stores can be determined by inspecting its
// ValueType. DICOM values typically can be one of many types (ints, strings, bytes, sequences of other elements, etc),
// so this Value interface attempts to represent this as canoically as possible in Golang (since generics do not exist
// yet).
//
// Value is JSON serializable out of the box (implements json.Marshaler).
//
// If necessary, a Value's data can be efficiently unpacked by inspecting its underlying ValueType and either using a
// Golang type assertion or using the helper functions provided (like MustGetStrings). Because for each ValueType there
// is exactly one underlying Golang type, this should be safe, efficient, and straightforward.
//
//	switch(myvalue.ValueType()) {
//		case dicom.Strings:
//			// We know the underlying Golang type is []string
//			fmt.Println(dicom.MustGetStrings(myvalue)[0])
//			// or
//			s := myvalue.GetValue().([]string)
//			break;
// 		case dicom.Bytes:
//			// ...
//	}
//
// Unpacking the data like above is only necessary if something specific needs to be done with the underlying data.
// See the Element and Dataset examples as well to see how to work with this kind of data, and common patterns for doing
// so.
type Value interface {
	// All types that can be a "Value" for an element will implement this empty method, similar to how protocol buffers
	// implement "oneof" in Go
	isElementValue()
	// ValueType returns the underlying ValueType of this Value. This can be used to unpack the underlying data in this
	// Value.
	ValueType() ValueType
	// GetValue returns the underlying value that this Value holds. What type is returned here can be determined exactly
	// from the ValueType() of this Value (see the ValueType godoc).
	GetValue() interface{} // TODO: rename to Get to read cleaner
	String() string
	MarshalJSON() ([]byte, error)
}

// NewValue creates a new DICOM value for the supplied data. Likely most useful
// if creating an Element in testing or write scenarios.
//
// Data must be one of the following types, otherwise and error will be returned
// (ErrorUnexpectedDataType).
//
// Acceptable types: []int, []string, []byte, []float64, PixelDataInfo,
// [][]*Element (represents a sequence, which contains several
// items which each contain several elements).
func NewValue(data interface{}) (Value, error) {
	switch data.(type) {
	case []int:
		return &intsValue{value: data.([]int)}, nil
	case []string:
		return &stringsValue{value: data.([]string)}, nil
	case []byte:
		return &bytesValue{value: data.([]byte)}, nil
	case PixelDataInfo:
		return &pixelDataValue{PixelDataInfo: data.(PixelDataInfo)}, nil
	case []float64:
		return &floatsValue{value: data.([]float64)}, nil
	case [][]*Element:
		items := data.([][]*Element)
		sequenceItems := make([]*SequenceItemValue, 0, len(items))
		for _, item := range items {
			sequenceItems = append(sequenceItems, &SequenceItemValue{elements: item})
		}
		return &sequencesValue{value: sequenceItems}, nil
	default:
		return nil, ErrorUnexpectedDataType
	}
}

func mustNewValue(data interface{}) Value {
	v, err := NewValue(data)
	if err != nil {
		panic(err)
	}
	return v
}

// NewElement creates a new DICOM Element with the supplied tag and with a value
// built from the provided data. The data can be one of the types that is
// acceptable to NewValue.
func NewElement(t tag.Tag, data interface{}) (*Element, error) {
	tagInfo, err := tag.Find(t)
	if err != nil {
		return nil, err
	}
	rawVR := tagInfo.VR

	value, err := NewValue(data)
	if err != nil {
		return nil, err
	}

	return &Element{
		Tag:                    t,
		ValueRepresentation:    tag.GetVRKind(t, rawVR),
		RawValueRepresentation: rawVR,
		Value:                  value,
	}, nil
}

func mustNewElement(t tag.Tag, data interface{}) *Element {
	elem, err := NewElement(t, data)
	if err != nil {
		log.Panic(err)
	}
	return elem
}

func mustNewPrivateElement(t tag.Tag, rawVR string, data interface{}) *Element {
	value, err := NewValue(data)
	if err != nil {
		log.Panic(fmt.Errorf("error creating value: %w", err))
	}

	return &Element{
		Tag:                    t,
		ValueRepresentation:    tag.GetVRKind(t, rawVR),
		RawValueRepresentation: rawVR,
		ValueLength:            0,
		Value:                  value,
	}
}

// ValueType is a type that represents the type of a Value. It is an enumerated
// set, and the set of values can be found below.
type ValueType int

// Possible ValueTypes that represent the different value types for information parsed into DICOM element values.
// Each ValueType corresponds to exactly one underlying Golang type.
const (
	// Strings represents an underlying value of []string
	Strings ValueType = iota
	// Bytes represents an underlying value of []byte
	Bytes
	// Ints represents an underlying value of []int
	Ints
	// PixelData represents an underlying value of PixelDataInfo
	PixelData
	// SequenceItem represents an underlying value of []*Element
	SequenceItem
	// Sequences represents an underlying value of []SequenceItem
	Sequences
	// Floats represents an underlying value of []float64
	Floats
)

// Begin definitions of Values:

// bytesValue represents a value of []byte.
type bytesValue struct {
	value []byte
}

func (b *bytesValue) isElementValue()       {}
func (b *bytesValue) ValueType() ValueType  { return Bytes }
func (b *bytesValue) GetValue() interface{} { return b.value }
func (b *bytesValue) String() string {
	return fmt.Sprintf("%v", b.value)
}
func (b *bytesValue) MarshalJSON() ([]byte, error) {
	return json.Marshal(b.value)
}

// stringsValue represents a value of []string.
type stringsValue struct {
	value []string
}

func (s *stringsValue) isElementValue()       {}
func (s *stringsValue) ValueType() ValueType  { return Strings }
func (s *stringsValue) GetValue() interface{} { return s.value }
func (s *stringsValue) String() string {
	return fmt.Sprintf("%v", s.value)
}
func (s *stringsValue) MarshalJSON() ([]byte, error) {
	return json.Marshal(s.value)
}

// intsValue represents a value of []int.
type intsValue struct {
	value []int
}

func (s *intsValue) isElementValue()       {}
func (s *intsValue) ValueType() ValueType  { return Ints }
func (s *intsValue) GetValue() interface{} { return s.value }
func (s *intsValue) String() string {
	return fmt.Sprintf("%v", s.value)
}
func (s *intsValue) MarshalJSON() ([]byte, error) {
	return json.Marshal(s.value)
}

// floatsValue represents a value of []float64.
type floatsValue struct {
	value []float64
}

func (s *floatsValue) isElementValue()       {}
func (s *floatsValue) ValueType() ValueType  { return Floats }
func (s *floatsValue) GetValue() interface{} { return s.value }
func (s *floatsValue) String() string {
	return fmt.Sprintf("%v", s.value)
}
func (s *floatsValue) MarshalJSON() ([]byte, error) {
	return json.Marshal(s.value)
}

// SequenceItemValue is a Value that represents a single Sequence Item. Learn
// more about Sequences at
// http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_7.5.html.
type SequenceItemValue struct {
	elements []*Element
}

func (s *SequenceItemValue) isElementValue() {}

// ValueType returns the underlying ValueType of this Value. This can be used
// to unpack the underlying data in this Value.
func (s *SequenceItemValue) ValueType() ValueType { return SequenceItem }

// GetValue returns the underlying value that this Value holds. What type is
// returned here can be determined exactly from the ValueType() of this Value
// (see the ValueType godoc).
func (s *SequenceItemValue) GetValue() interface{} { return s.elements }

// String is used to get a string representation of this struct.
func (s *SequenceItemValue) String() string {
	// TODO: consider adding more sophisticated formatting
	return fmt.Sprintf("%+v", s.elements)
}

// MarshalJSON is the method used to marshal this struct to JSON.
func (s *SequenceItemValue) MarshalJSON() ([]byte, error) {
	return json.Marshal(s.elements)
}

// sequencesValue represents a set of items in a DICOM sequence.
type sequencesValue struct {
	value []*SequenceItemValue
}

func (s *sequencesValue) isElementValue()       {}
func (s *sequencesValue) ValueType() ValueType  { return Sequences }
func (s *sequencesValue) GetValue() interface{} { return s.value }
func (s *sequencesValue) String() string {
	// TODO: consider adding more sophisticated formatting
	return fmt.Sprintf("%+v", s.value)
}
func (s *sequencesValue) MarshalJSON() ([]byte, error) {
	return json.Marshal(s.value)
}

// PixelDataInfo is a representation of DICOM PixelData.
type PixelDataInfo struct {
	Frames         []frame.Frame
	IsEncapsulated bool `json:"isEncapsulated"`
	Offsets        []uint32
}

// pixelDataValue represents DICOM PixelData
type pixelDataValue struct {
	PixelDataInfo
}

func (e *pixelDataValue) isElementValue()       {}
func (e *pixelDataValue) ValueType() ValueType  { return PixelData }
func (e *pixelDataValue) GetValue() interface{} { return e.PixelDataInfo }
func (e *pixelDataValue) String() string {
	// TODO: consider adding more sophisticated formatting
	return ""
}

func (e *pixelDataValue) MarshalJSON() ([]byte, error) {
	return json.Marshal(e.PixelDataInfo)
}

// MustGetInts attempts to get an Ints value out of the provided value, and will
// panic if it is unable to do so.
func MustGetInts(v Value) []int {
	if v.ValueType() != Ints {
		log.Panicf("MustGetInts expected ValueType of Ints, got: %v", v.ValueType())
	}
	return v.GetValue().([]int)
}

// MustGetStrings attempts to get a Strings value out of the provided Value, and
// will panic if it is unable to do so.
func MustGetStrings(v Value) []string {
	if v.ValueType() != Strings {
		log.Panicf("MustGetStrings expected ValueType of Strings, got: %v", v.ValueType())
	}
	return v.GetValue().([]string)
}

// MustGetBytes attempts to get a Bytes value out of the provided Value, and
// will panic if it is unable to do so.
func MustGetBytes(v Value) []byte {
	if v.ValueType() != Bytes {
		log.Panicf("MustGetBytes expected ValueType of Bytes, got: %v", v.ValueType())
	}
	return v.GetValue().([]byte)
}

// MustGetFloats attempts to get a Floats value out of the provided Value, and
// will panic if it is unable to do so.
func MustGetFloats(v Value) []float64 {
	if v.ValueType() != Floats {
		log.Panicf("MustGetFloats expected ValueType of Floats, got: %v", v.ValueType())
	}
	return v.GetValue().([]float64)
}

// MustGetPixelDataInfo attempts to get a PixelDataInfo value out of the
// provided Value, and will panic if it is unable to do so.
func MustGetPixelDataInfo(v Value) PixelDataInfo {
	if v.ValueType() != PixelData {
		log.Panicf("MustGetPixelDataInfo expected ValueType of PixelData, got: %v", v.ValueType())
	}
	return v.GetValue().(PixelDataInfo)
}

// allValues is used for tests that need to pass in instances of the unexported
// value structs to cmp.AllowUnexported.
var allValues = []interface{}{
	floatsValue{},
	intsValue{},
	stringsValue{},
	pixelDataValue{},
	sequencesValue{},
	bytesValue{},
	SequenceItemValue{},
}