package restmapper

import (
	"sync"

	kapi "k8s.io/kubernetes/pkg/api"
	"k8s.io/kubernetes/pkg/api/meta"
	"k8s.io/kubernetes/pkg/api/unversioned"
	"k8s.io/kubernetes/pkg/apimachinery/registered"
	"k8s.io/kubernetes/pkg/client/typed/discovery"
)

type discoveryRESTMapper struct {
	discoveryClient discovery.DiscoveryInterface

	delegate meta.RESTMapper

	initLock sync.Mutex
}

// NewDiscoveryRESTMapper that initializes using the discovery APIs, relying on group ordering and preferred versions
// to build its appropriate priorities.  Only versions are registered with API machinery are added now.
// TODO make this work with generic resources at some point.  For now, this handles enabled and disabled resources cleanly.
func NewDiscoveryRESTMapper(discoveryClient discovery.DiscoveryInterface) meta.RESTMapper {
	return &discoveryRESTMapper{discoveryClient: discoveryClient}
}

func (d *discoveryRESTMapper) getDelegate() (meta.RESTMapper, error) {
	d.initLock.Lock()
	defer d.initLock.Unlock()

	if d.delegate != nil {
		return d.delegate, nil
	}

	serverGroups, err := d.discoveryClient.ServerGroups()
	if err != nil {
		return nil, err
	}

	// always prefer our default group for now.  The version should be discovered from discovery, but this will hold us
	// for quite some time.
	resourcePriority := []unversioned.GroupVersionResource{
		{Group: kapi.GroupName, Version: meta.AnyVersion, Resource: meta.AnyResource},
	}
	kindPriority := []unversioned.GroupVersionKind{
		{Group: kapi.GroupName, Version: meta.AnyVersion, Kind: meta.AnyKind},
	}
	groupPriority := []string{}

	unionMapper := meta.MultiRESTMapper{}

	for _, group := range serverGroups.Groups {
		if len(group.Versions) == 0 {
			continue
		}
		groupPriority = append(groupPriority, group.Name)

		if len(group.PreferredVersion.Version) != 0 {
			preferredVersion := unversioned.GroupVersion{Group: group.Name, Version: group.PreferredVersion.Version}
			if registered.IsEnabledVersion(preferredVersion) {
				resourcePriority = append(resourcePriority, preferredVersion.WithResource(meta.AnyResource))
				kindPriority = append(kindPriority, preferredVersion.WithKind(meta.AnyKind))
			}
		}

		for _, discoveryVersion := range group.Versions {
			version := unversioned.GroupVersion{Group: group.Name, Version: discoveryVersion.Version}
			if !registered.IsEnabledVersion(version) {
				continue
			}
			groupMeta, err := registered.Group(group.Name)
			if err != nil {
				return nil, err
			}
			resources, err := d.discoveryClient.ServerResourcesForGroupVersion(version.String())
			if err != nil {
				return nil, err
			}

			versionMapper := meta.NewDefaultRESTMapper([]unversioned.GroupVersion{version}, groupMeta.InterfacesFor)
			for _, resource := range resources.APIResources {
				// TODO properly handle resource versus kind
				gvk := version.WithKind(resource.Kind)

				scope := meta.RESTScopeNamespace
				if !resource.Namespaced {
					scope = meta.RESTScopeRoot
				}
				versionMapper.Add(gvk, scope)

				// TODO formalize this by checking to see if they support listing
				versionMapper.Add(version.WithKind(resource.Kind+"List"), scope)
			}

			// we need to add List.  Its a special case of something we need that isn't in the discovery doc
			if group.Name == kapi.GroupName {
				versionMapper.Add(version.WithKind("List"), meta.RESTScopeNamespace)
			}

			unionMapper = append(unionMapper, versionMapper)
		}
	}

	for _, group := range groupPriority {
		resourcePriority = append(resourcePriority, unversioned.GroupVersionResource{Group: group, Version: meta.AnyVersion, Resource: meta.AnyResource})
		kindPriority = append(kindPriority, unversioned.GroupVersionKind{Group: group, Version: meta.AnyVersion, Kind: meta.AnyKind})
	}

	d.delegate = meta.PriorityRESTMapper{Delegate: unionMapper, ResourcePriority: resourcePriority, KindPriority: kindPriority}
	return d.delegate, nil
}

func (d *discoveryRESTMapper) KindFor(resource unversioned.GroupVersionResource) (unversioned.GroupVersionKind, error) {
	delegate, err := d.getDelegate()
	if err != nil {
		return unversioned.GroupVersionKind{}, err
	}
	return delegate.KindFor(resource)
}

func (d *discoveryRESTMapper) KindsFor(resource unversioned.GroupVersionResource) ([]unversioned.GroupVersionKind, error) {
	delegate, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	return delegate.KindsFor(resource)
}

func (d *discoveryRESTMapper) ResourceFor(input unversioned.GroupVersionResource) (unversioned.GroupVersionResource, error) {
	delegate, err := d.getDelegate()
	if err != nil {
		return unversioned.GroupVersionResource{}, err
	}
	return delegate.ResourceFor(input)
}

func (d *discoveryRESTMapper) ResourcesFor(input unversioned.GroupVersionResource) ([]unversioned.GroupVersionResource, error) {
	delegate, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	return delegate.ResourcesFor(input)
}

func (d *discoveryRESTMapper) RESTMapping(gk unversioned.GroupKind, versions ...string) (*meta.RESTMapping, error) {
	delegate, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	return delegate.RESTMapping(gk, versions...)
}

func (d *discoveryRESTMapper) RESTMappings(gk unversioned.GroupKind) ([]*meta.RESTMapping, error) {
	delegate, err := d.getDelegate()
	if err != nil {
		return nil, err
	}
	return delegate.RESTMappings(gk)
}

func (d *discoveryRESTMapper) AliasesForResource(resource string) ([]string, bool) {
	delegate, err := d.getDelegate()
	if err != nil {
		return nil, false
	}
	return delegate.AliasesForResource(resource)
}

func (d *discoveryRESTMapper) ResourceSingularizer(resource string) (singular string, err error) {
	delegate, err := d.getDelegate()
	if err != nil {
		return resource, err
	}
	return delegate.ResourceSingularizer(resource)
}