package core

// (C) Copyright IBM Corp. 2019.
//
// 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.
// See the License for the specific language governing permissions and
// limitations under the License.

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"os"
	"reflect"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/go-openapi/strfmt"
	validator "gopkg.in/go-playground/validator.v9"
)

// Validate is a shared validator instance used to perform validation of structs.
var Validate *validator.Validate
var re = regexp.MustCompile(`(?s)\[(\S*)\]`)

func init() {
	Validate = validator.New()
}

const (
	jsonMimePattern      = "(?i)^application\\/((json)|(merge\\-patch\\+json))(;.*)?$"
	jsonPatchMimePattern = "(?i)^application\\/json\\-patch\\+json(;.*)?$"
)

// IsNil checks if the specified object is nil or not.
func IsNil(object interface{}) bool {
	if object == nil {
		return true
	}

	switch reflect.TypeOf(object).Kind() {
	case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
		return reflect.ValueOf(object).IsNil()
	}

	return false
}

// ValidateNotNil returns the specified error if 'object' is nil, nil otherwise.
func ValidateNotNil(object interface{}, errorMsg string) error {
	if IsNil(object) {
		return errors.New(errorMsg)
	}
	return nil
}

// ValidateStruct validates 'param' (assumed to be a ptr to a struct) according to the
// annotations attached to its fields.
func ValidateStruct(param interface{}, paramName string) error {
	err := ValidateNotNil(param, paramName+" cannot be nil")
	if err != nil {
		return err
	}

	err = Validate.Struct(param)
	if err != nil {
		// If there were validation errors then return an error containing the field errors
		if fieldErrors, ok := err.(validator.ValidationErrors); ok {
			return fmt.Errorf("%s failed validation:\n%s", paramName, fieldErrors.Error())
		}
		return err
	}

	return nil
}

// StringPtr returns a pointer to string literal.
func StringPtr(literal string) *string {
	return &literal
}

// BoolPtr returns a pointer to boolean literal.
func BoolPtr(literal bool) *bool {
	return &literal
}

// Int64Ptr returns a pointer to int64 literal.
func Int64Ptr(literal int64) *int64 {
	return &literal
}

// Float32Ptr returns a pointer to float32 literal.
func Float32Ptr(literal float32) *float32 {
	return &literal
}

// Float64Ptr returns a pointer to float64 literal.
func Float64Ptr(literal float64) *float64 {
	return &literal
}

// UUIDPtr returns a pointer to strfmt.UUID literal.
func UUIDPtr(literal strfmt.UUID) *strfmt.UUID {
	return &literal
}

// IsJSONMimeType Returns true iff the specified mimeType value represents a
// "JSON" mimetype.
func IsJSONMimeType(mimeType string) bool {
	if mimeType == "" {
		return false
	}
	matched, err := regexp.MatchString(jsonMimePattern, mimeType)
	if err != nil {
		return false
	}
	return matched
}

// IsJSONPatchMimeType returns true iff the specified mimeType value represents
// a "JSON Patch" mimetype.
func IsJSONPatchMimeType(mimeType string) bool {
	if mimeType == "" {
		return false
	}
	matched, err := regexp.MatchString(jsonPatchMimePattern, mimeType)
	if err != nil {
		return false
	}
	return matched
}

// StringNilMapper de-references the parameter 's' and returns the result, or ""
// if 's' is nil.
func StringNilMapper(s *string) string {
	if s == nil {
		return ""
	}
	return *s
}

// HasBadFirstOrLastChar checks if the string starts with `{` or `"`
// or ends with `}` or `"`.
func HasBadFirstOrLastChar(str string) bool {
	return strings.HasPrefix(str, "{") || strings.HasPrefix(str, "\"") ||
		strings.HasSuffix(str, "}") || strings.HasSuffix(str, "\"")
}

// UserHomeDir returns the user home directory.
func UserHomeDir() string {
	if runtime.GOOS == "windows" {
		home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
		if home == "" {
			home = os.Getenv("USERPROFILE")
		}
		return home
	}
	return os.Getenv("HOME")
}

// SystemInfo returns the system information.
func SystemInfo() string {
	return fmt.Sprintf("(arch=%s; os=%s; go.version=%s)", runtime.GOARCH, runtime.GOOS, runtime.Version())
}

// PrettyPrint print pretty.
func PrettyPrint(result interface{}, resultName string) {
	output, err := json.MarshalIndent(result, "", "    ")

	if err == nil {
		fmt.Printf("%v:\n%+v\n\n", resultName, string(output))
	}
}

// GetCurrentTime returns the current Unix time.
func GetCurrentTime() int64 {
	return time.Now().Unix()
}

// ConvertSlice Marshals 'slice' to a json string, performs
// string manipulation on the resulting string, and converts
// the string to a '[]string'. If 'slice' is nil, not a 'slice' type,
// or an error occurred during conversion, an error will be returned
func ConvertSlice(slice interface{}) (s []string, err error) {
	inputIsSlice := false

	if IsNil(slice) {
		err = fmt.Errorf(ERRORMSG_NIL_SLICE)
		return
	}

	// Reflect on 'slice' to validate the input is in fact a slice
	rResultType := reflect.TypeOf(slice)

	switch rResultType.Kind() {
	case reflect.Slice:
		inputIsSlice = true
	default:
	}

	// If it's not a slice, just return an error
	if !inputIsSlice {
		err = fmt.Errorf(ERRORMSG_PARAM_NOT_SLICE)
		return
	} else if reflect.ValueOf(slice).Len() == 0 {
		s = []string{}
		return
	}

	jsonBuffer, err := json.Marshal(slice)
	if err != nil {
		err = fmt.Errorf(ERRORMSG_MARSHAL_SLICE, err.Error())
		return
	}

	jsonString := string(jsonBuffer)

	// Use regex to convert the json string to a string slice
	match := re.FindStringSubmatch(jsonString)
	if match != nil && match[1] != "" {
		newString := match[1]
		s = strings.Split(newString, ",")
		// For each slice element, attempt to remove any surrounding quotes
		// added by marshaling into a json string
		for i := range s {
			unquotedString, unquoteErr := strconv.Unquote(s[i])
			if unquoteErr == nil && unquotedString != "" {
				s[i] = unquotedString
			}
		}
		return
	}

	// If we returned a plain string that's not in "slice format",
	// then attempt to just convert it to a string slice.
	if jsonString != "" {
		s = strings.Split(jsonString, ",")
		return
	}

	return nil, fmt.Errorf(ERRORMSG_CONVERT_SLICE)
}

// SliceContains returns true iff "contains" is an element of "slice"
func SliceContains(slice []string, contains string) bool {
	for _, elem := range slice {
		if elem == contains {
			return true
		}
	}
	return false
}

// return a pointer to the value of query parameter `param` from url,
// or nil when not found
func GetQueryParam(urlStr *string, param string) (*string, error) {
	if urlStr == nil || *urlStr == "" {
		return nil, nil
	}

	u, err := url.Parse(*urlStr)
	if err != nil {
		return nil, err
	}

	q, err := url.ParseQuery(u.RawQuery)
	if err != nil {
		return nil, err
	}

	v := q.Get(param)
	if v == "" {
		return nil, nil
	}
	return &v, nil
}
