/*
 *
 * 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 (
	"time"

	"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
	// This is set when the child reports TransientFailure, and unset when it
	// reports Ready or Idle. It is used to decide whether the failover timer
	// should start when the child is transitioning into Connecting. The timer
	// will be restarted if the child has not reported TF more recently than it
	// reported Ready or Idle.
	reportedTF bool
	// The latest state the child balancer provided.
	state balancer.State
	// The timer to give a priority some time to connect. And if the priority
	// doesn't go into Ready/Failure, the next priority will be started.
	initTimer *timerWrapper
}

// 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 unless it is started.
func (cb *childBalancer) updateConfig(child *Child, rState resolver.State) {
	cb.ignoreReresolutionRequests = child.IgnoreReresolutionRequests
	cb.config = child.Config.Config
	cb.rState = rState
	if cb.started {
		cb.sendUpdate()
	}
}

// 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)
	cb.startInitTimer()
	cb.sendUpdate()
}

// 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.stopInitTimer()
	cb.parent.bg.Remove(cb.name)
	cb.started = false
	cb.state = balancer.State{
		ConnectivityState: connectivity.Connecting,
		Picker:            base.NewErrPicker(balancer.ErrNoSubConnAvailable),
	}
	// Clear child.reportedTF, so that if this child is started later, it will
	// be given time to connect.
	cb.reportedTF = false
}

func (cb *childBalancer) startInitTimer() {
	if cb.initTimer != nil {
		return
	}
	// Need this local variable to capture timerW in the AfterFunc closure
	// to check the stopped boolean.
	timerW := &timerWrapper{}
	cb.initTimer = timerW
	timerW.timer = time.AfterFunc(DefaultPriorityInitTimeout, func() {
		cb.parent.mu.Lock()
		defer cb.parent.mu.Unlock()
		if timerW.stopped {
			return
		}
		cb.initTimer = nil
		// Re-sync the priority. This will switch to the next priority if
		// there's any. Note that it's important sync() is called after setting
		// initTimer to nil.
		cb.parent.syncPriority("")
	})
}

func (cb *childBalancer) stopInitTimer() {
	timerW := cb.initTimer
	if timerW == nil {
		return
	}
	cb.initTimer = nil
	timerW.stopped = true
	timerW.timer.Stop()
}
