// Copyright [2019] LinkedIn Corp. Licensed under the Apache License, Version
// 2.0 (the "License"); you may not use this file except in compliance with the
// License.  You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

package goavro

import (
	"errors"
	"fmt"
	"strings"
)

const nullNamespace = ""

// ErrInvalidName is the error returned when one or more parts of an Avro name
// is invalid.
type ErrInvalidName struct {
	Message string
}

func (e ErrInvalidName) Error() string {
	return "schema name ought to " + e.Message
}

// NOTE: This function designed to work with name components, after they have
// been split on the period rune.
func isRuneInvalidForFirstCharacter(r rune) bool {
	return (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') && r != '_'
}

func isRuneInvalidForOtherCharacters(r rune) bool {
	return isRuneInvalidForFirstCharacter(r) && (r < '0' || r > '9')
}

func checkNameComponent(s string) error {
	err := checkString(s)
	if err != nil {
		return &ErrInvalidName{err.Error()}
	}
	return err
}

func checkString(s string) error {
	if len(s) == 0 {
		return errors.New("be non-empty string")
	}
	if strings.IndexFunc(s[:1], isRuneInvalidForFirstCharacter) != -1 {
		return errors.New("start with [A-Za-z_]: " + s)
	}
	if strings.IndexFunc(s[1:], isRuneInvalidForOtherCharacters) != -1 {
		return errors.New("have second and remaining characters contain only [A-Za-z0-9_]: " + s)
	}
	return nil
}

// name describes an Avro name in terms of its full name and namespace.
type name struct {
	fullName  string // the instance's Avro name
	namespace string // for use when building new name from existing one
}

// newName returns a new Name instance after first ensuring the arguments do not
// violate any of the Avro naming rules.
func newName(n, ns, ens string) (*name, error) {
	var nn name

	if index := strings.LastIndexByte(n, '.'); index > -1 {
		// inputName does contain a dot, so ignore everything else and use it as the full name
		nn.fullName = n
		nn.namespace = n[:index]
	} else {
		// inputName does not contain a dot, therefore is not the full name
		if ns != nullNamespace {
			// if namespace provided in the schema in the same schema level, use it
			nn.fullName = ns + "." + n
			nn.namespace = ns
		} else if ens != nullNamespace {
			// otherwise if enclosing namespace provided, use it
			nn.fullName = ens + "." + n
			nn.namespace = ens
		} else {
			// otherwise no namespace, so use null namespace, the empty string
			nn.fullName = n
		}
	}

	// verify all components of the full name for adherence to Avro naming rules
	for i, component := range strings.Split(nn.fullName, ".") {
		if i == 0 && RelaxedNameValidation && component == "" {
			continue
		}
		if err := checkNameComponent(component); err != nil {
			return nil, err
		}
	}

	return &nn, nil
}

var (
	// RelaxedNameValidation causes name validation to allow the first component
	// of an Avro namespace to be the empty string.
	RelaxedNameValidation bool
)

func newNameFromSchemaMap(enclosingNamespace string, schemaMap map[string]interface{}) (*name, error) {
	var nameString, namespaceString string

	name, ok := schemaMap["name"]
	if !ok {
		return nil, errors.New("schema ought to have name key")
	}
	nameString, ok = name.(string)
	if !ok || nameString == nullNamespace {
		return nil, fmt.Errorf("schema name ought to be non-empty string; received: %T: %v", name, name)
	}
	if namespace, ok := schemaMap["namespace"]; ok {
		namespaceString, ok = namespace.(string)
		if !ok {
			return nil, fmt.Errorf("schema namespace, if provided, ought to be a string; received: %T: %v", namespace, namespace)
		}
	}

	return newName(nameString, namespaceString, enclosingNamespace)
}

func (n *name) String() string {
	return n.fullName
}

// short returns the name without the prefixed namespace.
func (n *name) short() string {
	if index := strings.LastIndexByte(n.fullName, '.'); index > -1 {
		return n.fullName[index+1:]
	}
	return n.fullName
}
