// Copyright 2016 Google LLC
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package disco represents Google API discovery documents.
package disco

import (
	"encoding/json"
	"fmt"
	"reflect"
	"sort"
	"strings"
)

// A Document is an API discovery document.
type Document struct {
	ID                string             `json:"id"`
	Name              string             `json:"name"`
	Version           string             `json:"version"`
	Title             string             `json:"title"`
	RootURL           string             `json:"rootUrl"`
	ServicePath       string             `json:"servicePath"`
	BasePath          string             `json:"basePath"`
	DocumentationLink string             `json:"documentationLink"`
	Auth              Auth               `json:"auth"`
	Features          []string           `json:"features"`
	Methods           MethodList         `json:"methods"`
	Schemas           map[string]*Schema `json:"schemas"`
	Resources         ResourceList       `json:"resources"`
}

// init performs additional initialization and checks that
// were not done during unmarshaling.
func (d *Document) init() error {
	schemasByID := map[string]*Schema{}
	for _, s := range d.Schemas {
		schemasByID[s.ID] = s
	}
	for name, s := range d.Schemas {
		if s.Ref != "" {
			return fmt.Errorf("top level schema %q is a reference", name)
		}
		s.Name = name
		if err := s.init(schemasByID); err != nil {
			return err
		}
	}
	for _, m := range d.Methods {
		if err := m.init(schemasByID); err != nil {
			return err
		}
	}
	for _, r := range d.Resources {
		if err := r.init("", schemasByID); err != nil {
			return err
		}
	}
	return nil
}

// NewDocument unmarshals the bytes into a Document.
// It also validates the document to make sure it is error-free.
func NewDocument(bytes []byte) (*Document, error) {
	// The discovery service returns JSON with this format if there's an error, e.g.
	// the document isn't found.
	var errDoc struct {
		Error struct {
			Code    int
			Message string
			Status  string
		}
	}
	if err := json.Unmarshal(bytes, &errDoc); err == nil && errDoc.Error.Code != 0 {
		return nil, fmt.Errorf("bad discovery doc: %+v", errDoc.Error)
	}

	var doc Document
	if err := json.Unmarshal(bytes, &doc); err != nil {
		return nil, err
	}
	if err := doc.init(); err != nil {
		return nil, err
	}
	return &doc, nil
}

// Auth represents the auth section of a discovery document.
// Only OAuth2 information is retained.
type Auth struct {
	OAuth2Scopes []Scope
}

// A Scope is an OAuth2 scope.
type Scope struct {
	URL         string
	Description string
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (a *Auth) UnmarshalJSON(data []byte) error {
	// Pull out the oauth2 scopes and turn them into nice structs.
	// Ignore other auth information.
	var m struct {
		OAuth2 struct {
			Scopes map[string]struct {
				Description string
			}
		}
	}
	if err := json.Unmarshal(data, &m); err != nil {
		return err
	}
	// Sort keys to provide a deterministic ordering, mainly for testing.
	for _, k := range sortedKeys(m.OAuth2.Scopes) {
		a.OAuth2Scopes = append(a.OAuth2Scopes, Scope{
			URL:         k,
			Description: m.OAuth2.Scopes[k].Description,
		})
	}
	return nil
}

// A Schema holds a JSON Schema as defined by
// https://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.1.
// We only support the subset of JSON Schema needed for Google API generation.
type Schema struct {
	ID                   string // union types not supported
	Type                 string // union types not supported
	Format               string
	Description          string
	Properties           PropertyList
	ItemSchema           *Schema `json:"items"` // array of schemas not supported
	AdditionalProperties *Schema // boolean not supported
	Ref                  string  `json:"$ref"`
	Default              string
	Pattern              string
	Enums                []string `json:"enum"`
	// Google extensions to JSON Schema
	EnumDescriptions []string
	Variant          *Variant

	RefSchema *Schema `json:"-"` // Schema referred to by $ref
	Name      string  `json:"-"` // Schema name, if top level
	Kind      Kind    `json:"-"`
}

type Variant struct {
	Discriminant string
	Map          []*VariantMapItem
}

type VariantMapItem struct {
	TypeValue string `json:"type_value"`
	Ref       string `json:"$ref"`
}

func (s *Schema) init(topLevelSchemas map[string]*Schema) error {
	if s == nil {
		return nil
	}
	var err error
	if s.Ref != "" {
		if s.RefSchema, err = resolveRef(s.Ref, topLevelSchemas); err != nil {
			return err
		}
	}
	s.Kind, err = s.initKind()
	if err != nil {
		return err
	}
	if s.Kind == ArrayKind && s.ItemSchema == nil {
		return fmt.Errorf("schema %+v: array does not have items", s)
	}
	if s.Kind != ArrayKind && s.ItemSchema != nil {
		return fmt.Errorf("schema %+v: non-array has items", s)
	}
	if err := s.AdditionalProperties.init(topLevelSchemas); err != nil {
		return err
	}
	if err := s.ItemSchema.init(topLevelSchemas); err != nil {
		return err
	}
	for _, p := range s.Properties {
		if err := p.Schema.init(topLevelSchemas); err != nil {
			return err
		}
	}
	return nil
}

func resolveRef(ref string, topLevelSchemas map[string]*Schema) (*Schema, error) {
	rs, ok := topLevelSchemas[ref]
	if !ok {
		return nil, fmt.Errorf("could not resolve schema reference %q", ref)
	}
	return rs, nil
}

func (s *Schema) initKind() (Kind, error) {
	if s.Ref != "" {
		return ReferenceKind, nil
	}
	switch s.Type {
	case "string", "number", "integer", "boolean", "any":
		return SimpleKind, nil
	case "object":
		if s.AdditionalProperties != nil {
			if s.AdditionalProperties.Type == "any" {
				return AnyStructKind, nil
			}
			return MapKind, nil
		}
		return StructKind, nil
	case "array":
		return ArrayKind, nil
	default:
		return 0, fmt.Errorf("unknown type %q for schema %q", s.Type, s.ID)
	}
}

// ElementSchema returns the schema for the element type of s. For maps,
// this is the schema of the map values. For arrays, it is the schema
// of the array item type.
//
// ElementSchema panics if called on a schema that is not of kind map or array.
func (s *Schema) ElementSchema() *Schema {
	switch s.Kind {
	case MapKind:
		return s.AdditionalProperties
	case ArrayKind:
		return s.ItemSchema
	default:
		panic("ElementSchema called on schema of type " + s.Type)
	}
}

// IsIntAsString reports whether the schema represents an integer value
// formatted as a string.
func (s *Schema) IsIntAsString() bool {
	return s.Type == "string" && strings.Contains(s.Format, "int")
}

// Kind classifies a Schema.
type Kind int

const (
	// SimpleKind is the category for any JSON Schema that maps to a
	// primitive Go type: strings, numbers, booleans, and "any" (since it
	// maps to interface{}).
	SimpleKind Kind = iota

	// StructKind is the category for a JSON Schema that declares a JSON
	// object without any additional (arbitrary) properties.
	StructKind

	// MapKind is the category for a JSON Schema that declares a JSON
	// object with additional (arbitrary) properties that have a non-"any"
	// schema type.
	MapKind

	// AnyStructKind is the category for a JSON Schema that declares a
	// JSON object with additional (arbitrary) properties that can be any
	// type.
	AnyStructKind

	// ArrayKind is the category for a JSON Schema that declares an
	// "array" type.
	ArrayKind

	// ReferenceKind is the category for a JSON Schema that is a reference
	// to another JSON Schema.  During code generation, these references
	// are resolved using the API.schemas map.
	// See https://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.28
	// for more details on the format.
	ReferenceKind
)

type Property struct {
	Name   string
	Schema *Schema
}

type PropertyList []*Property

func (pl *PropertyList) UnmarshalJSON(data []byte) error {
	// In the discovery doc, properties are a map. Convert to a list.
	var m map[string]*Schema
	if err := json.Unmarshal(data, &m); err != nil {
		return err
	}
	for _, k := range sortedKeys(m) {
		*pl = append(*pl, &Property{
			Name:   k,
			Schema: m[k],
		})
	}
	return nil
}

type ResourceList []*Resource

func (rl *ResourceList) UnmarshalJSON(data []byte) error {
	// In the discovery doc, resources are a map. Convert to a list.
	var m map[string]*Resource
	if err := json.Unmarshal(data, &m); err != nil {
		return err
	}
	for _, k := range sortedKeys(m) {
		r := m[k]
		r.Name = k
		*rl = append(*rl, r)
	}
	return nil
}

// A Resource holds information about a Google API Resource.
type Resource struct {
	Name      string
	FullName  string // {parent.FullName}.{Name}
	Methods   MethodList
	Resources ResourceList
}

func (r *Resource) init(parentFullName string, topLevelSchemas map[string]*Schema) error {
	r.FullName = fmt.Sprintf("%s.%s", parentFullName, r.Name)
	for _, m := range r.Methods {
		if err := m.init(topLevelSchemas); err != nil {
			return err
		}
	}
	for _, r2 := range r.Resources {
		if err := r2.init(r.FullName, topLevelSchemas); err != nil {
			return err
		}
	}
	return nil
}

type MethodList []*Method

func (ml *MethodList) UnmarshalJSON(data []byte) error {
	// In the discovery doc, resources are a map. Convert to a list.
	var m map[string]*Method
	if err := json.Unmarshal(data, &m); err != nil {
		return err
	}
	for _, k := range sortedKeys(m) {
		meth := m[k]
		meth.Name = k
		*ml = append(*ml, meth)
	}
	return nil
}

// A Method holds information about a resource method.
type Method struct {
	Name                  string
	ID                    string
	Path                  string
	HTTPMethod            string
	Description           string
	Parameters            ParameterList
	ParameterOrder        []string
	Request               *Schema
	Response              *Schema
	Scopes                []string
	MediaUpload           *MediaUpload
	SupportsMediaDownload bool

	JSONMap map[string]interface{} `json:"-"`
}

type MediaUpload struct {
	Accept    []string
	MaxSize   string
	Protocols map[string]Protocol
}

type Protocol struct {
	Multipart bool
	Path      string
}

func (m *Method) init(topLevelSchemas map[string]*Schema) error {
	if err := m.Request.init(topLevelSchemas); err != nil {
		return err
	}
	if err := m.Response.init(topLevelSchemas); err != nil {
		return err
	}
	return nil
}

func (m *Method) UnmarshalJSON(data []byte) error {
	type T Method // avoid a recursive call to UnmarshalJSON
	if err := json.Unmarshal(data, (*T)(m)); err != nil {
		return err
	}
	// Keep the unmarshalled map around, because the generator
	// outputs it as a comment after the method body.
	// TODO(jba): make this unnecessary.
	return json.Unmarshal(data, &m.JSONMap)
}

type ParameterList []*Parameter

func (pl *ParameterList) UnmarshalJSON(data []byte) error {
	// In the discovery doc, resources are a map. Convert to a list.
	var m map[string]*Parameter
	if err := json.Unmarshal(data, &m); err != nil {
		return err
	}
	for _, k := range sortedKeys(m) {
		p := m[k]
		p.Name = k
		*pl = append(*pl, p)
	}
	return nil
}

// A Parameter holds information about a method parameter.
type Parameter struct {
	Name string
	Schema
	Required bool
	Repeated bool
	Location string
}

// sortedKeys returns the keys of m, which must be a map[string]T, in sorted order.
func sortedKeys(m interface{}) []string {
	vkeys := reflect.ValueOf(m).MapKeys()
	var keys []string
	for _, vk := range vkeys {
		keys = append(keys, vk.Interface().(string))
	}
	sort.Strings(keys)
	return keys
}
