package compiler

import (
	"strings"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/collections"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/module"
	"github.com/microsoft/typescript-go/internal/tsoptions"
	"github.com/microsoft/typescript-go/internal/tspath"
)

type projectReferenceFileMapper struct {
	opts   ProgramOptions
	host   module.ResolutionHost
	loader *fileLoader // Only present during populating the mapper and parsing, released after that

	configToProjectReference    map[tspath.Path]*tsoptions.ParsedCommandLine // All the resolved references needed
	referencesInConfigFile      map[tspath.Path][]tspath.Path                // Map of config file to its references
	sourceToProjectReference    map[tspath.Path]*tsoptions.SourceOutputAndProjectReference
	outputDtsToProjectReference map[tspath.Path]*tsoptions.SourceOutputAndProjectReference

	// Store all the realpath from dts in node_modules to source file from project reference needed during parsing so it can be used later
	realpathDtsToSource collections.SyncMap[tspath.Path, *tsoptions.SourceOutputAndProjectReference]
}

func (mapper *projectReferenceFileMapper) getParseFileRedirect(file ast.HasFileName) string {
	if mapper.opts.canUseProjectReferenceSource() {
		// Map to source file from project reference
		source := mapper.getProjectReferenceFromOutputDts(file.Path())
		if source == nil {
			source = mapper.getSourceToDtsIfSymlink(file)
		}
		if source != nil {
			return source.Source
		}
	} else {
		// Map to dts file from project reference
		output := mapper.getProjectReferenceFromSource(file.Path())
		if output != nil && output.OutputDts != "" {
			return output.OutputDts
		}
	}
	return ""
}

func (mapper *projectReferenceFileMapper) getResolvedProjectReferences() []*tsoptions.ParsedCommandLine {
	if mapper.opts.Config.ConfigFile == nil {
		return nil
	}
	refs, ok := mapper.referencesInConfigFile[mapper.opts.Config.ConfigFile.SourceFile.Path()]
	var result []*tsoptions.ParsedCommandLine
	if ok {
		result = make([]*tsoptions.ParsedCommandLine, 0, len(refs))
		for _, refPath := range refs {
			refConfig, _ := mapper.configToProjectReference[refPath]
			result = append(result, refConfig)
		}
	}
	return result
}

func (mapper *projectReferenceFileMapper) getProjectReferenceFromSource(path tspath.Path) *tsoptions.SourceOutputAndProjectReference {
	return mapper.sourceToProjectReference[path]
}

func (mapper *projectReferenceFileMapper) getProjectReferenceFromOutputDts(path tspath.Path) *tsoptions.SourceOutputAndProjectReference {
	return mapper.outputDtsToProjectReference[path]
}

func (mapper *projectReferenceFileMapper) isSourceFromProjectReference(path tspath.Path) bool {
	return mapper.opts.canUseProjectReferenceSource() && mapper.getProjectReferenceFromSource(path) != nil
}

func (mapper *projectReferenceFileMapper) getCompilerOptionsForFile(file ast.HasFileName) *core.CompilerOptions {
	redirect := mapper.getRedirectParsedCommandLineForResolution(file)
	return module.GetCompilerOptionsWithRedirect(mapper.opts.Config.CompilerOptions(), redirect)
}

func (mapper *projectReferenceFileMapper) getRedirectParsedCommandLineForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine {
	redirect, _ := mapper.getRedirectForResolution(file)
	return redirect
}

func (mapper *projectReferenceFileMapper) getRedirectForResolution(file ast.HasFileName) (*tsoptions.ParsedCommandLine, string) {
	path := file.Path()
	// Check if outputdts of source file from project reference
	output := mapper.getProjectReferenceFromSource(path)
	if output != nil {
		return output.Resolved, output.Source
	}

	// Source file from project reference
	resultFromDts := mapper.getProjectReferenceFromOutputDts(path)
	if resultFromDts != nil {
		return resultFromDts.Resolved, resultFromDts.Source
	}

	realpathDtsToSource := mapper.getSourceToDtsIfSymlink(file)
	if realpathDtsToSource != nil {
		return realpathDtsToSource.Resolved, realpathDtsToSource.Source
	}
	return nil, file.FileName()
}

func (mapper *projectReferenceFileMapper) getResolvedReferenceFor(path tspath.Path) (*tsoptions.ParsedCommandLine, bool) {
	config, ok := mapper.configToProjectReference[path]
	return config, ok
}

func (mapper *projectReferenceFileMapper) rangeResolvedProjectReference(
	f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool,
) bool {
	if mapper.opts.Config.ConfigFile == nil {
		return false
	}
	seenRef := collections.NewSetWithSizeHint[tspath.Path](len(mapper.referencesInConfigFile))
	seenRef.Add(mapper.opts.Config.ConfigFile.SourceFile.Path())
	refs := mapper.referencesInConfigFile[mapper.opts.Config.ConfigFile.SourceFile.Path()]
	return mapper.rangeResolvedReferenceWorker(refs, f, mapper.opts.Config, seenRef)
}

func (mapper *projectReferenceFileMapper) rangeResolvedReferenceWorker(
	references []tspath.Path,
	f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool,
	parent *tsoptions.ParsedCommandLine,
	seenRef *collections.Set[tspath.Path],
) bool {
	for index, path := range references {
		if !seenRef.AddIfAbsent(path) {
			continue
		}
		config, _ := mapper.configToProjectReference[path]
		if !f(path, config, parent, index) {
			return false
		}
		if !mapper.rangeResolvedReferenceWorker(mapper.referencesInConfigFile[path], f, config, seenRef) {
			return false
		}
	}
	return true
}

func (mapper *projectReferenceFileMapper) rangeResolvedProjectReferenceInChildConfig(
	childConfig *tsoptions.ParsedCommandLine,
	f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool,
) bool {
	if childConfig == nil || childConfig.ConfigFile == nil {
		return false
	}
	seenRef := collections.NewSetWithSizeHint[tspath.Path](len(mapper.referencesInConfigFile))
	seenRef.Add(childConfig.ConfigFile.SourceFile.Path())
	refs := mapper.referencesInConfigFile[childConfig.ConfigFile.SourceFile.Path()]
	return mapper.rangeResolvedReferenceWorker(refs, f, mapper.opts.Config, seenRef)
}

func (mapper *projectReferenceFileMapper) getSourceToDtsIfSymlink(file ast.HasFileName) *tsoptions.SourceOutputAndProjectReference {
	// If preserveSymlinks is true, module resolution wont jump the symlink
	// but the resolved real path may be the .d.ts from project reference
	// Note:: Currently we try the real path only if the
	// file is from node_modules to avoid having to run real path on all file paths
	path := file.Path()
	realpathDtsToSource, ok := mapper.realpathDtsToSource.Load(path)
	if ok {
		return realpathDtsToSource
	}
	if mapper.loader != nil && mapper.opts.Config.CompilerOptions().PreserveSymlinks == core.TSTrue {
		fileName := file.FileName()
		if !strings.Contains(fileName, "/node_modules/") {
			mapper.realpathDtsToSource.Store(path, nil)
		} else {
			realDeclarationPath := mapper.loader.toPath(mapper.host.FS().Realpath(fileName))
			if realDeclarationPath == path {
				mapper.realpathDtsToSource.Store(path, nil)
			} else {
				realpathDtsToSource := mapper.getProjectReferenceFromOutputDts(realDeclarationPath)
				if realpathDtsToSource != nil {
					mapper.realpathDtsToSource.Store(path, realpathDtsToSource)
					return realpathDtsToSource
				}
				mapper.realpathDtsToSource.Store(path, nil)
			}
		}
	}
	return nil
}
