// Package internal contains some code that should not be exported but needs to
// be shared across more than one of the protoreflect sub-packages.
package internal

import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io/ioutil"

	"github.com/golang/protobuf/proto"
	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
)

// TODO: replace this alias configuration with desc.RegisterImportPath?

// StdFileAliases are the standard protos included with protoc, but older versions of
// their respective packages registered them using incorrect paths.
var StdFileAliases = map[string]string{
	// Files for the github.com/golang/protobuf/ptypes package at one point were
	// registered using the path where the proto files are mirrored in GOPATH,
	// inside the golang/protobuf repo.
	// (Fixed as of https://github.com/golang/protobuf/pull/412)
	"google/protobuf/any.proto":       "github.com/golang/protobuf/ptypes/any/any.proto",
	"google/protobuf/duration.proto":  "github.com/golang/protobuf/ptypes/duration/duration.proto",
	"google/protobuf/empty.proto":     "github.com/golang/protobuf/ptypes/empty/empty.proto",
	"google/protobuf/struct.proto":    "github.com/golang/protobuf/ptypes/struct/struct.proto",
	"google/protobuf/timestamp.proto": "github.com/golang/protobuf/ptypes/timestamp/timestamp.proto",
	"google/protobuf/wrappers.proto":  "github.com/golang/protobuf/ptypes/wrappers/wrappers.proto",
	// Files for the google.golang.org/genproto/protobuf package at one point
	// were registered with an anomalous "src/" prefix.
	// (Fixed as of https://github.com/google/go-genproto/pull/31)
	"google/protobuf/api.proto":            "src/google/protobuf/api.proto",
	"google/protobuf/field_mask.proto":     "src/google/protobuf/field_mask.proto",
	"google/protobuf/source_context.proto": "src/google/protobuf/source_context.proto",
	"google/protobuf/type.proto":           "src/google/protobuf/type.proto",

	// Other standard files (descriptor.proto and compiler/plugin.proto) are
	// registered correctly, so we don't need rules for them here.
}

func init() {
	// We provide aliasing in both directions, to support files with the
	// proper import path linked against older versions of the generated
	// files AND files that used the aliased import path but linked against
	// newer versions of the generated files (which register with the
	// correct path).

	// Get all files defined above
	keys := make([]string, 0, len(StdFileAliases))
	for k := range StdFileAliases {
		keys = append(keys, k)
	}
	// And add inverse mappings
	for _, k := range keys {
		alias := StdFileAliases[k]
		StdFileAliases[alias] = k
	}
}

type ErrNoSuchFile string

func (e ErrNoSuchFile) Error() string {
	return fmt.Sprintf("no such file: %q", string(e))
}

// LoadFileDescriptor loads a registered descriptor and decodes it. If the given
// name cannot be loaded but is a known standard name, an alias will be tried,
// so the standard files can be loaded even if linked against older "known bad"
// versions of packages.
func LoadFileDescriptor(file string) (*dpb.FileDescriptorProto, error) {
	fdb := proto.FileDescriptor(file)
	aliased := false
	if fdb == nil {
		var ok bool
		alias, ok := StdFileAliases[file]
		if ok {
			aliased = true
			if fdb = proto.FileDescriptor(alias); fdb == nil {
				return nil, ErrNoSuchFile(file)
			}
		} else {
			return nil, ErrNoSuchFile(file)
		}
	}

	fd, err := DecodeFileDescriptor(file, fdb)
	if err != nil {
		return nil, err
	}

	if aliased {
		// the file descriptor will have the alias used to load it, but
		// we need it to have the specified name in order to link it
		fd.Name = proto.String(file)
	}

	return fd, nil
}

// DecodeFileDescriptor decodes the bytes of a registered file descriptor.
// Registered file descriptors are first "proto encoded" (e.g. binary format
// for the descriptor protos) and then gzipped. So this function gunzips and
// then unmarshals into a descriptor proto.
func DecodeFileDescriptor(element string, fdb []byte) (*dpb.FileDescriptorProto, error) {
	raw, err := decompress(fdb)
	if err != nil {
		return nil, fmt.Errorf("failed to decompress %q descriptor: %v", element, err)
	}
	fd := dpb.FileDescriptorProto{}
	if err := proto.Unmarshal(raw, &fd); err != nil {
		return nil, fmt.Errorf("bad descriptor for %q: %v", element, err)
	}
	return &fd, nil
}

func decompress(b []byte) ([]byte, error) {
	r, err := gzip.NewReader(bytes.NewReader(b))
	if err != nil {
		return nil, fmt.Errorf("bad gzipped descriptor: %v", err)
	}
	out, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, fmt.Errorf("bad gzipped descriptor: %v", err)
	}
	return out, nil
}
