package generator

import (
	"regexp"
	"sort"
	"strings"

	"github.com/go-openapi/runtime"
	"github.com/go-openapi/swag"
)

const jsonSerializer = "json"

var mediaTypeNames = map[*regexp.Regexp]string{
	regexp.MustCompile("application/.*json"):                jsonSerializer,
	regexp.MustCompile("application/.*yaml"):                "yaml",
	regexp.MustCompile("application/.*protobuf"):            "protobuf",
	regexp.MustCompile("application/.*capnproto"):           "capnproto",
	regexp.MustCompile("application/.*thrift"):              "thrift",
	regexp.MustCompile("(?:application|text)/.*xml"):        "xml",
	regexp.MustCompile("text/.*markdown"):                   "markdown",
	regexp.MustCompile("text/.*html"):                       "html",
	regexp.MustCompile("text/.*csv"):                        "csv",
	regexp.MustCompile("text/.*tsv"):                        "tsv",
	regexp.MustCompile("text/.*javascript"):                 "js",
	regexp.MustCompile("text/.*css"):                        "css",
	regexp.MustCompile("text/.*plain"):                      "txt",
	regexp.MustCompile("application/.*octet-stream"):        "bin",
	regexp.MustCompile("application/.*tar"):                 "tar",
	regexp.MustCompile("application/.*gzip"):                "gzip",
	regexp.MustCompile("application/.*gz"):                  "gzip",
	regexp.MustCompile("application/.*raw-stream"):          "bin",
	regexp.MustCompile("application/x-www-form-urlencoded"): "urlform",
	regexp.MustCompile("application/javascript"):            "txt",
	regexp.MustCompile("multipart/form-data"):               "multipartform",
	regexp.MustCompile("image/.*"):                          "bin",
	regexp.MustCompile("audio/.*"):                          "bin",
	regexp.MustCompile("application/pdf"):                   "bin",
}

var knownProducers = map[string]string{
	jsonSerializer:  "runtime.JSONProducer()",
	"yaml":          "yamlpc.YAMLProducer()",
	"xml":           "runtime.XMLProducer()",
	"txt":           "runtime.TextProducer()",
	"bin":           "runtime.ByteStreamProducer()",
	"urlform":       "runtime.DiscardProducer",
	"multipartform": "runtime.DiscardProducer",
}

var knownConsumers = map[string]string{
	jsonSerializer:  "runtime.JSONConsumer()",
	"yaml":          "yamlpc.YAMLConsumer()",
	"xml":           "runtime.XMLConsumer()",
	"txt":           "runtime.TextConsumer()",
	"bin":           "runtime.ByteStreamConsumer()",
	"urlform":       "runtime.DiscardConsumer",
	"multipartform": "runtime.DiscardConsumer",
}

func wellKnownMime(tn string) (string, bool) {
	for k, v := range mediaTypeNames {
		if k.MatchString(tn) {
			return v, true
		}
	}
	return "", false
}

func mediaMime(orig string) string {
	return strings.SplitN(orig, ";", 2)[0]
}

func mediaGoName(media string) string {
	return pascalize(strings.ReplaceAll(media, "*", "Star"))
}

func mediaParameters(orig string) string {
	parts := strings.SplitN(orig, ";", 2)
	if len(parts) < 2 {
		return ""
	}
	return parts[1]
}

func (a *appGenerator) makeSerializers(mediaTypes []string, known func(string) (string, bool)) (GenSerGroups, bool) {
	supportsJSON := false
	uniqueSerializers := make(map[string]*GenSerializer, len(mediaTypes))
	uniqueSerializerGroups := make(map[string]*GenSerGroup, len(mediaTypes))

	// build all required serializers
	for _, media := range mediaTypes {
		key := mediaMime(media)
		nm, ok := wellKnownMime(key)
		if !ok {
			// keep this serializer named, even though its implementation is empty (cf. #1557)
			nm = key
		}
		name := swag.ToJSONName(nm)
		impl, _ := known(name)

		ser, ok := uniqueSerializers[key]
		if !ok {
			ser = &GenSerializer{
				AppName:        a.Name,
				ReceiverName:   a.Receiver,
				Name:           name,
				MediaType:      key,
				Implementation: impl,
				Parameters:     []string{},
			}
			uniqueSerializers[key] = ser
		}
		// provide all known parameters (currently unused by codegen templates)
		if params := strings.TrimSpace(mediaParameters(media)); params != "" {
			found := false
			for _, p := range ser.Parameters {
				if params == p {
					found = true
					break
				}
			}
			if !found {
				ser.Parameters = append(ser.Parameters, params)
			}
		}

		uniqueSerializerGroups[name] = &GenSerGroup{
			GenSerializer: GenSerializer{
				AppName:        a.Name,
				ReceiverName:   a.Receiver,
				Name:           name,
				Implementation: impl,
			},
		}
	}

	if len(uniqueSerializers) == 0 {
		impl, _ := known(jsonSerializer)
		uniqueSerializers[runtime.JSONMime] = &GenSerializer{
			AppName:        a.Name,
			ReceiverName:   a.Receiver,
			Name:           jsonSerializer,
			MediaType:      runtime.JSONMime,
			Implementation: impl,
			Parameters:     []string{},
		}
		uniqueSerializerGroups[jsonSerializer] = &GenSerGroup{
			GenSerializer: GenSerializer{
				AppName:        a.Name,
				ReceiverName:   a.Receiver,
				Name:           jsonSerializer,
				Implementation: impl,
			},
		}
		supportsJSON = true
	}

	// group serializers by consumer/producer to serve several mime media types
	serializerGroups := make(GenSerGroups, 0, len(uniqueSerializers))

	for _, group := range uniqueSerializerGroups {
		if group.Name == jsonSerializer {
			supportsJSON = true
		}
		serializers := make(GenSerializers, 0, len(uniqueSerializers))
		for _, ser := range uniqueSerializers {
			if group.Name == ser.Name {
				sort.Strings(ser.Parameters)
				serializers = append(serializers, *ser)
			}
		}
		sort.Sort(serializers)
		group.AllSerializers = serializers // provides the full list of mime media types for this serializer group
		serializerGroups = append(serializerGroups, *group)
	}
	sort.Sort(serializerGroups)
	return serializerGroups, supportsJSON
}

func (a *appGenerator) makeConsumes() (GenSerGroups, bool) {
	// builds a codegen struct from all consumes in the spec
	return a.makeSerializers(a.Analyzed.RequiredConsumes(), func(media string) (string, bool) {
		c, ok := knownConsumers[media]
		return c, ok
	})
}

func (a *appGenerator) makeProduces() (GenSerGroups, bool) {
	// builds a codegen struct from all produces in the spec
	return a.makeSerializers(a.Analyzed.RequiredProduces(), func(media string) (string, bool) {
		p, ok := knownProducers[media]
		return p, ok
	})
}
