package mimetype

import (
	stdmime "mime"
	"slices"
	"strings"

	"github.com/gabriel-vasile/mimetype/internal/charset"
	"github.com/gabriel-vasile/mimetype/internal/magic"
)

// MIME struct holds information about a file format: the string representation
// of the MIME type, the extension and the parent file format.
type MIME struct {
	mime      string
	aliases   []string
	extension string
	// detector receives the raw input and a limit for the number of bytes it is
	// allowed to check. It returns whether the input matches a signature or not.
	detector magic.Detector
	children []*MIME
	parent   *MIME
}

// String returns the string representation of the MIME type, e.g., "application/zip".
func (m *MIME) String() string {
	return m.mime
}

// Extension returns the file extension associated with the MIME type.
// It includes the leading dot, as in ".html". When the file format does not
// have an extension, the empty string is returned.
func (m *MIME) Extension() string {
	return m.extension
}

// Parent returns the parent MIME type from the hierarchy.
// Each MIME type has a non-nil parent, except for the root MIME type.
//
// For example, the application/json and text/html MIME types have text/plain as
// their parent because they are text files who happen to contain JSON or HTML.
// Another example is the ZIP format, which is used as container
// for Microsoft Office files, EPUB files, JAR files, and others.
func (m *MIME) Parent() *MIME {
	return m.parent
}

// Is checks whether this MIME type, or any of its aliases, is equal to the
// expected MIME type. MIME type equality test is done on the "type/subtype"
// section, ignores any optional MIME parameters, ignores any leading and
// trailing whitespace, and is case insensitive.
func (m *MIME) Is(expectedMIME string) bool {
	// Parsing is needed because some detected MIME types contain parameters
	// that need to be stripped for the comparison.
	expectedMIME, _, _ = stdmime.ParseMediaType(expectedMIME)
	found, _, _ := stdmime.ParseMediaType(m.mime)

	if expectedMIME == found {
		return true
	}

	if slices.Contains(m.aliases, expectedMIME) {
		return true
	}

	return false
}

func newMIME(
	mime, extension string,
	detector magic.Detector,
	children ...*MIME) *MIME {
	m := &MIME{
		mime:      mime,
		extension: extension,
		detector:  detector,
		children:  children,
	}

	for _, c := range children {
		c.parent = m
	}

	return m
}

func (m *MIME) alias(aliases ...string) *MIME {
	m.aliases = aliases
	return m
}

// match does a depth-first search on the signature tree. It returns the deepest
// successful node for which all the children detection functions fail.
func (m *MIME) match(in []byte, readLimit uint32) *MIME {
	for _, c := range m.children {
		if c.detector(in, readLimit) {
			return c.match(in, readLimit)
		}
	}

	needsCharset := map[string]func([]byte) string{
		"text/plain": charset.FromPlain,
		"text/html":  charset.FromHTML,
		"text/xml":   charset.FromXML,
	}
	charset := ""
	if f, ok := needsCharset[m.mime]; ok {
		// The charset comes from BOM, from HTML headers, from XML headers.
		// Limit the number of bytes searched for to 1024.
		charset = f(in[:min(len(in), 1024)])
	}
	if m == root || charset == "" {
		return m
	}

	return m.cloneHierarchy(charset)
}

// flatten transforms an hierarchy of MIMEs into a slice of MIMEs.
func (m *MIME) flatten() []*MIME {
	out := []*MIME{m} //nolint:prealloc
	for _, c := range m.children {
		out = append(out, c.flatten()...)
	}

	return out
}

// hierarchy returns an easy to read list of ancestors for m.
// For example, application/json would return json>txt>root.
func (m *MIME) hierarchy() string {
	h := ""
	for m := m; m != nil; m = m.Parent() {
		e := strings.TrimPrefix(m.Extension(), ".")
		if e == "" {
			// There are some MIME without extensions. When generating the hierarchy,
			// it would be confusing to use empty string as extension.
			// Use the subtype instead; ex: application/x-executable -> x-executable.
			e = strings.Split(m.String(), "/")[1]
			if m.Is("application/octet-stream") {
				// for octet-stream use root, because it's short and used in many places
				e = "root"
			}
		}
		h += ">" + e
	}
	return strings.TrimPrefix(h, ">")
}

// clone creates a new MIME with the provided optional MIME parameters.
func (m *MIME) clone(charset string) *MIME {
	clonedMIME := m.mime
	if charset != "" {
		clonedMIME = m.mime + "; charset=" + charset
	}

	return &MIME{
		mime:      clonedMIME,
		aliases:   m.aliases,
		extension: m.extension,
	}
}

// cloneHierarchy creates a clone of m and all its ancestors. The optional MIME
// parameters are set on the last child of the hierarchy.
func (m *MIME) cloneHierarchy(charset string) *MIME {
	ret := m.clone(charset)
	lastChild := ret
	for p := m.Parent(); p != nil; p = p.Parent() {
		pClone := p.clone("")
		lastChild.parent = pClone
		lastChild = pClone
	}

	return ret
}

func (m *MIME) lookup(mime string) *MIME {
	if mime == m.mime {
		return m
	}
	if slices.Contains(m.aliases, mime) {
		return m
	}

	for _, c := range m.children {
		if m := c.lookup(mime); m != nil {
			return m
		}
	}
	return nil
}

// Extend adds detection for a sub-format. The detector is a function
// returning true when the raw input file satisfies a signature.
// The sub-format will be detected if all the detectors in the parent chain return true.
// The extension should include the leading dot, as in ".html".
func (m *MIME) Extend(detector func(raw []byte, limit uint32) bool, mime, extension string, aliases ...string) {
	mime, _, _ = stdmime.ParseMediaType(mime)
	c := &MIME{
		mime:      mime,
		extension: extension,
		detector:  detector,
		parent:    m,
		aliases:   aliases,
	}

	mu.Lock()
	m.children = append([]*MIME{c}, m.children...)
	mu.Unlock()
}
