// Copyright 2019 Google LLC
//
// 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.

package bigquery

import (
	"fmt"

	bq "google.golang.org/api/bigquery/v2"
)

// StandardSQLDataType conveys type information using the Standard SQL type
// system.
type StandardSQLDataType struct {
	// ArrayElementType indicates the type of an array's elements, when the
	// TypeKind is ARRAY.
	ArrayElementType *StandardSQLDataType
	// StructType indicates the struct definition (fields), when the
	// TypeKind is STRUCT.
	StructType *StandardSQLStructType
	// The top-level type of this type definition.
	// Can be any standard SQL data type.  For more information about BigQuery
	// data types, see
	// https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types
	//
	// Additional information is available in the REST documentation:
	// https://cloud.google.com/bigquery/docs/reference/rest/v2/StandardSqlDataType
	TypeKind string
}

func (ssdt *StandardSQLDataType) toBQ() (*bq.StandardSqlDataType, error) {
	if ssdt == nil {
		return nil, nil
	}
	bqdt := &bq.StandardSqlDataType{
		TypeKind: ssdt.TypeKind,
	}
	if ssdt.ArrayElementType != nil {
		dt, err := ssdt.ArrayElementType.toBQ()
		if err != nil {
			return nil, err
		}
		bqdt.ArrayElementType = dt
	}
	if ssdt.StructType != nil {
		dt, err := ssdt.StructType.toBQ()
		if err != nil {
			return nil, err
		}
		bqdt.StructType = dt
	}
	return bqdt, nil
}

func (ssdt StandardSQLDataType) toBQParamType() *bq.QueryParameterType {
	if ssdt.ArrayElementType != nil {
		return &bq.QueryParameterType{Type: "ARRAY", ArrayType: ssdt.ArrayElementType.toBQParamType()}
	}
	if ssdt.StructType != nil {
		var fts []*bq.QueryParameterTypeStructTypes
		for _, field := range ssdt.StructType.Fields {
			fts = append(fts, &bq.QueryParameterTypeStructTypes{
				Name: field.Name,
				Type: field.Type.toBQParamType(),
			})
		}
		return &bq.QueryParameterType{Type: "STRUCT", StructTypes: fts}
	}
	return &bq.QueryParameterType{Type: ssdt.TypeKind}
}

func bqToStandardSQLDataType(bqdt *bq.StandardSqlDataType) (*StandardSQLDataType, error) {
	if bqdt == nil {
		return nil, nil
	}
	ssdt := &StandardSQLDataType{
		TypeKind: bqdt.TypeKind,
	}

	if bqdt.ArrayElementType != nil {
		dt, err := bqToStandardSQLDataType(bqdt.ArrayElementType)
		if err != nil {
			return nil, err
		}
		ssdt.ArrayElementType = dt
	}
	if bqdt.StructType != nil {
		st, err := bqToStandardSQLStructType(bqdt.StructType)
		if err != nil {
			return nil, err
		}
		ssdt.StructType = st
	}
	return ssdt, nil
}

// StandardSQLField represents a field using the Standard SQL data type system.
type StandardSQLField struct {
	// The name of this field.  Can be absent for struct fields.
	Name string
	// Data type for the field.
	Type *StandardSQLDataType
}

func (ssf *StandardSQLField) toBQ() (*bq.StandardSqlField, error) {
	if ssf == nil {
		return nil, nil
	}
	bqf := &bq.StandardSqlField{
		Name: ssf.Name,
	}
	if ssf.Type != nil {
		dt, err := ssf.Type.toBQ()
		if err != nil {
			return nil, err
		}
		bqf.Type = dt
	}
	return bqf, nil
}

func bqToStandardSQLField(bqf *bq.StandardSqlField) (*StandardSQLField, error) {
	if bqf == nil {
		return nil, nil
	}
	t, err := bqToStandardSQLDataType(bqf.Type)
	if err != nil {
		return nil, err
	}
	return &StandardSQLField{
		Name: bqf.Name,
		Type: t,
	}, nil
}

// StandardSQLStructType represents a structure type, which is a list of Standard SQL fields.
// For more information, see:
// https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#struct-type
type StandardSQLStructType struct {
	Fields []*StandardSQLField
}

func (ssst *StandardSQLStructType) toBQ() (*bq.StandardSqlStructType, error) {
	if ssst == nil {
		return nil, nil
	}
	fields, err := standardSQLStructFieldsToBQ(ssst.Fields)
	if err != nil {
		return nil, err
	}
	return &bq.StandardSqlStructType{
		Fields: fields,
	}, nil
}

func bqToStandardSQLStructType(bqst *bq.StandardSqlStructType) (*StandardSQLStructType, error) {
	if bqst == nil {
		return nil, nil
	}
	var fields []*StandardSQLField
	for _, v := range bqst.Fields {
		f, err := bqToStandardSQLField(v)
		if err != nil {
			return nil, err
		}
		fields = append(fields, f)
	}
	return &StandardSQLStructType{
		Fields: fields,
	}, nil
}

func standardSQLStructFieldsToBQ(fields []*StandardSQLField) ([]*bq.StandardSqlField, error) {
	var bqFields []*bq.StandardSqlField
	for _, v := range fields {
		bqf, err := v.toBQ()
		if err != nil {
			return nil, fmt.Errorf("error converting struct fields: %w", err)
		}
		bqFields = append(bqFields, bqf)
	}
	return bqFields, nil
}

// StandardSQLTableType models a table-like resource, which has a set of columns.
type StandardSQLTableType struct {

	// The columns of the table.
	Columns []*StandardSQLField
}

func (sstt *StandardSQLTableType) toBQ() (*bq.StandardSqlTableType, error) {
	if sstt == nil {
		return nil, nil
	}
	out := &bq.StandardSqlTableType{}
	for k, v := range sstt.Columns {
		bq, err := v.toBQ()
		if err != nil {
			return nil, fmt.Errorf("error converting column %d: %v", k, err)
		}
		out.Columns = append(out.Columns, bq)
	}
	return out, nil
}

func bqToStandardSQLTableType(in *bq.StandardSqlTableType) (*StandardSQLTableType, error) {
	if in == nil {
		return nil, nil
	}
	out := &StandardSQLTableType{}
	for k, v := range in.Columns {
		f, err := bqToStandardSQLField(v)
		if err != nil {
			return nil, fmt.Errorf("error converting column %d: %v", k, err)
		}
		out.Columns = append(out.Columns, f)
	}
	return out, nil
}
