/*
 *
 * Copyright 2021 gRPC authors.
 *
 * 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 priority

import (
	"google.golang.org/grpc/balancer"
	"google.golang.org/grpc/balancer/base"
	"google.golang.org/grpc/connectivity"
	"google.golang.org/grpc/resolver"
	"google.golang.org/grpc/serviceconfig"
)

type childBalancer struct {
	name   string
	parent *priorityBalancer
	bb     *ignoreResolveNowBalancerBuilder

	ignoreReresolutionRequests bool
	config                     serviceconfig.LoadBalancingConfig
	rState                     resolver.State

	started bool
	state   balancer.State
}

// newChildBalancer creates a child balancer place holder, but doesn't
// build/start the child balancer.
func newChildBalancer(name string, parent *priorityBalancer, bb balancer.Builder) *childBalancer {
	return &childBalancer{
		name:    name,
		parent:  parent,
		bb:      newIgnoreResolveNowBalancerBuilder(bb, false),
		started: false,
		// Start with the connecting state and picker with re-pick error, so
		// that when a priority switch causes this child picked before it's
		// balancing policy is created, a re-pick will happen.
		state: balancer.State{
			ConnectivityState: connectivity.Connecting,
			Picker:            base.NewErrPicker(balancer.ErrNoSubConnAvailable),
		},
	}
}

// updateBuilder updates builder for the child, but doesn't build.
func (cb *childBalancer) updateBuilder(bb balancer.Builder) {
	cb.bb = newIgnoreResolveNowBalancerBuilder(bb, cb.ignoreReresolutionRequests)
}

// updateConfig sets childBalancer's config and state, but doesn't send update to
// the child balancer.
func (cb *childBalancer) updateConfig(child *Child, rState resolver.State) {
	cb.ignoreReresolutionRequests = child.IgnoreReresolutionRequests
	cb.config = child.Config.Config
	cb.rState = rState
}

// start builds the child balancer if it's not already started.
//
// It doesn't do it directly. It asks the balancer group to build it.
func (cb *childBalancer) start() {
	if cb.started {
		return
	}
	cb.started = true
	cb.parent.bg.Add(cb.name, cb.bb)
}

// sendUpdate sends the addresses and config to the child balancer.
func (cb *childBalancer) sendUpdate() {
	cb.bb.updateIgnoreResolveNow(cb.ignoreReresolutionRequests)
	// TODO: return and aggregate the returned error in the parent.
	err := cb.parent.bg.UpdateClientConnState(cb.name, balancer.ClientConnState{
		ResolverState:  cb.rState,
		BalancerConfig: cb.config,
	})
	if err != nil {
		cb.parent.logger.Warningf("failed to update ClientConn state for child %v: %v", cb.name, err)
	}
}

// stop stops the child balancer and resets the state.
//
// It doesn't do it directly. It asks the balancer group to remove it.
//
// Note that the underlying balancer group could keep the child in a cache.
func (cb *childBalancer) stop() {
	if !cb.started {
		return
	}
	cb.parent.bg.Remove(cb.name)
	cb.started = false
	cb.state = balancer.State{
		ConnectivityState: connectivity.Connecting,
		Picker:            base.NewErrPicker(balancer.ErrNoSubConnAvailable),
	}
}
