// Copyright 2018 PingCAP, Inc.
//
// 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,
// See the License for the specific language governing permissions and
// limitations under the License.

package domain

import (
	"fmt"
	"sync"
	"time"

	"github.com/pingcap/parser/ast"
	"github.com/pingcap/tidb/sessionctx/variable"
	"github.com/pingcap/tidb/util/chunk"
	"github.com/pingcap/tidb/util/logutil"
	"github.com/pingcap/tidb/util/stmtsummary"
	"go.uber.org/zap"
	"golang.org/x/sync/singleflight"
)

// GlobalVariableCache caches global variables.
type GlobalVariableCache struct {
	sync.RWMutex
	lastModify time.Time
	rows       []chunk.Row
	fields     []*ast.ResultField

	// Unit test may like to disable it.
	disable     bool
	SingleFight singleflight.Group
}

// GlobalVariableCacheExpiry is the global variable cache TTL.
const GlobalVariableCacheExpiry = 2 * time.Second

// Update updates the global variable cache.
func (gvc *GlobalVariableCache) Update(rows []chunk.Row, fields []*ast.ResultField) {
	gvc.Lock()
	gvc.lastModify = time.Now()
	gvc.rows = rows
	gvc.fields = fields
	gvc.Unlock()

	checkEnableServerGlobalVar(rows)
}

// Get gets the global variables from cache.
func (gvc *GlobalVariableCache) Get() (succ bool, rows []chunk.Row, fields []*ast.ResultField) {
	gvc.RLock()
	defer gvc.RUnlock()
	if time.Since(gvc.lastModify) < GlobalVariableCacheExpiry {
		succ, rows, fields = !gvc.disable, gvc.rows, gvc.fields
		return
	}
	succ = false
	return
}

type loadResult struct {
	rows   []chunk.Row
	fields []*ast.ResultField
}

// LoadGlobalVariables will load from global cache first, loadFn will be executed if cache is not valid
func (gvc *GlobalVariableCache) LoadGlobalVariables(loadFn func() ([]chunk.Row, []*ast.ResultField, error)) ([]chunk.Row, []*ast.ResultField, error) {
	succ, rows, fields := gvc.Get()
	if succ {
		return rows, fields, nil
	}
	fn := func() (interface{}, error) {
		resRows, resFields, loadErr := loadFn()
		if loadErr != nil {
			return nil, loadErr
		}
		gvc.Update(resRows, resFields)
		return &loadResult{resRows, resFields}, nil
	}
	res, err, _ := gvc.SingleFight.Do("loadGlobalVariable", fn)
	if err != nil {
		return nil, nil, err
	}
	loadRes := res.(*loadResult)
	return loadRes.rows, loadRes.fields, nil
}

// Disable disables the global variable cache, used in test only.
func (gvc *GlobalVariableCache) Disable() {
	gvc.Lock()
	defer gvc.Unlock()
	gvc.disable = true
}

// checkEnableServerGlobalVar processes variables that acts in server and global level.
func checkEnableServerGlobalVar(rows []chunk.Row) {
	for _, row := range rows {
		sVal := ""
		if !row.IsNull(1) {
			sVal = row.GetString(1)
		}
		var err error
		switch row.GetString(0) {
		case variable.TiDBEnableStmtSummary:
			err = stmtsummary.StmtSummaryByDigestMap.SetEnabled(sVal, false)
		case variable.TiDBStmtSummaryInternalQuery:
			err = stmtsummary.StmtSummaryByDigestMap.SetEnabledInternalQuery(sVal, false)
		case variable.TiDBStmtSummaryRefreshInterval:
			err = stmtsummary.StmtSummaryByDigestMap.SetRefreshInterval(sVal, false)
		case variable.TiDBStmtSummaryHistorySize:
			err = stmtsummary.StmtSummaryByDigestMap.SetHistorySize(sVal, false)
		case variable.TiDBStmtSummaryMaxStmtCount:
			err = stmtsummary.StmtSummaryByDigestMap.SetMaxStmtCount(sVal, false)
		case variable.TiDBStmtSummaryMaxSQLLength:
			err = stmtsummary.StmtSummaryByDigestMap.SetMaxSQLLength(sVal, false)
		case variable.TiDBCapturePlanBaseline:
			variable.CapturePlanBaseline.Set(sVal, false)
		}
		if err != nil {
			logutil.BgLogger().Error(fmt.Sprintf("load global variable %s error", row.GetString(0)), zap.Error(err))
		}
	}
}

// GetGlobalVarsCache gets the global variable cache.
func (do *Domain) GetGlobalVarsCache() *GlobalVariableCache {
	return &do.gvc
}
