package lightning

import (
	"context"
	"fmt"
	"path"
	"path/filepath"
	"sort"
)

type (
	customDataFileNameGenerator func(dbName string, tblName string, idx int, suffix string) string
	ignoreFileNameGenerator     func(idx int) string
)

type mockTableSetting struct {
	DBName                string
	TableName             string
	DataFileCount         int
	Suffix                string
	CustomDataFileNameGen customDataFileNameGenerator
}

type ignoreFileSetting struct {
	IgnoreFileCount   int
	IgnoreFileNameGen ignoreFileNameGenerator
}

type mockDataSourceSetting struct {
	TableSettings     []*mockTableSetting
	IgnoreFileSetting *ignoreFileSetting
}

type mockObjectLister struct {
	prefix   string
	setting  *mockDataSourceSetting
	fileList []string
}

// ListObjects list objects for an S3 data source, and returns an iterator.
// It implements the ObjectLister interface.
func (l *mockObjectLister) ListObjects(ctx context.Context, req *ListRequest) (ListResultIterator, error) {
	if l.fileList == nil {
		l.fileList = generateFileList(l.prefix, l.setting)
	}

	startIdx := 0
	if len(req.StartOffsetKey) > 0 {
		startIdx = getStartIndex(l.fileList, req.StartOffsetKey)
	}
	return &mockListResultIterator{
		prefix:   l.prefix,
		curIndex: 0,
		fileList: l.fileList[startIdx:],
		maxKeys:  req.MaxKeys,
	}, nil
}

func generateFileList(prefix string, setting *mockDataSourceSetting) []string {
	var resultList []string
	fileNamesMap := make(map[string]struct{})
	dbNameMap := make(map[string]struct{})
	for _, tblSetting := range setting.TableSettings {
		// add db schema
		dbNameMap[tblSetting.DBName] = struct{}{}
		// table schema file
		schemaFileName := filepath.ToSlash(path.Join(
			prefix,
			fmt.Sprintf("%s.%s-schema.sql", tblSetting.DBName, tblSetting.TableName),
		))
		fileNamesMap[schemaFileName] = struct{}{}
		// insert the data file
		for i := 0; i < tblSetting.DataFileCount; i++ {
			var dataFileName string
			if tblSetting.CustomDataFileNameGen != nil {
				dataFileName = filepath.ToSlash(path.Join(
					prefix,
					tblSetting.CustomDataFileNameGen(tblSetting.DBName, tblSetting.TableName, i, tblSetting.Suffix),
				))
			} else {
				dataFileName = filepath.ToSlash(path.Join(
					prefix,
					fmt.Sprintf("%s.%s.%08d.%s", tblSetting.DBName, tblSetting.TableName, i, tblSetting.Suffix),
				))
			}
			fileNamesMap[dataFileName] = struct{}{}
		}
	}
	// insert the db schema file
	for dbName := range dbNameMap {
		schemaFileName := filepath.ToSlash(path.Join(
			prefix,
			fmt.Sprintf("%s-schema-create.sql", dbName),
		))
		fileNamesMap[schemaFileName] = struct{}{}
	}
	// generate ignored files
	if setting.IgnoreFileSetting != nil {
		ignoreFileSetting := setting.IgnoreFileSetting
		for i := 0; i < ignoreFileSetting.IgnoreFileCount; i++ {
			var ignoreFileName string
			if ignoreFileSetting.IgnoreFileNameGen != nil {
				ignoreFileName = filepath.ToSlash(path.Join(
					prefix,
					ignoreFileSetting.IgnoreFileNameGen(i),
				))
			} else {
				ignoreFileName = filepath.ToSlash(path.Join(
					prefix,
					fmt.Sprintf(".ignore-%08d", i),
				))
			}
			fileNamesMap[ignoreFileName] = struct{}{}
		}
	}
	for fileName := range fileNamesMap {
		resultList = append(resultList, fileName)
	}
	sort.Strings(resultList)
	return resultList
}

func getStartIndex(fileList []string, startKey string) int {
	listLen := len(fileList)
	switch listLen {
	case 0:
		return 0
	case 1:
		if startKey < fileList[0] {
			return 0
		} else {
			return 1
		}
	}
	middleIdx := listLen / 2
	if startKey < fileList[middleIdx] {
		return getStartIndex(fileList[:middleIdx], startKey)
	} else {
		return middleIdx + getStartIndex(fileList[middleIdx:], startKey)
	}
}

type mockListResultIterator struct {
	prefix   string
	curIndex int
	fileList []string
	maxKeys  int
}

// Next retrieves one S3 object and ready to fetch the next.
// It implements the ListResultIterator interface.
func (iter *mockListResultIterator) Next() (*ObjectInfo, error) {
	if len(iter.fileList) <= 0 {
		return nil, errNoItems
	}
	if iter.curIndex >= len(iter.fileList) {
		return nil, errIterDone
	}
	if iter.maxKeys > 0 && iter.curIndex >= iter.maxKeys {
		return nil, errIterDone
	}
	objName := iter.fileList[iter.curIndex]
	iter.curIndex++

	return &ObjectInfo{
		Name:   objName,
		Size:   100,
		Prefix: iter.prefix,
	}, nil
}
