package compiler

import (
	"slices"
	"sync"

	"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/diagnostics"
	"github.com/microsoft/typescript-go/internal/tsoptions"
	"github.com/microsoft/typescript-go/internal/tspath"
)

type includeProcessor struct {
	fileIncludeReasons    map[tspath.Path][]*FileIncludeReason
	processingDiagnostics []*processingDiagnostic

	reasonToReferenceLocation  collections.SyncMap[*FileIncludeReason, *referenceFileLocation]
	includeReasonToRelatedInfo collections.SyncMap[*FileIncludeReason, *ast.Diagnostic]
	redirectAndFileFormat      collections.SyncMap[tspath.Path, []*ast.Diagnostic]
	computedDiagnostics        *ast.DiagnosticsCollection
	computedDiagnosticsOnce    sync.Once
	compilerOptionsSyntax      *ast.ObjectLiteralExpression
	compilerOptionsSyntaxOnce  sync.Once
}

func updateFileIncludeProcessor(p *Program) {
	p.includeProcessor = &includeProcessor{
		fileIncludeReasons:    p.includeProcessor.fileIncludeReasons,
		processingDiagnostics: p.includeProcessor.processingDiagnostics,
	}
}

func (i *includeProcessor) getDiagnostics(p *Program) *ast.DiagnosticsCollection {
	i.computedDiagnosticsOnce.Do(func() {
		i.computedDiagnostics = &ast.DiagnosticsCollection{}
		for _, d := range i.processingDiagnostics {
			i.computedDiagnostics.Add(d.toDiagnostic(p))
		}
		for _, resolutions := range p.resolvedModules {
			for _, resolvedModule := range resolutions {
				for _, diag := range resolvedModule.ResolutionDiagnostics {
					i.computedDiagnostics.Add(diag)
				}
			}
		}
		for _, typeResolutions := range p.typeResolutionsInFile {
			for _, resolvedTypeRef := range typeResolutions {
				for _, diag := range resolvedTypeRef.ResolutionDiagnostics {
					i.computedDiagnostics.Add(diag)
				}
			}
		}
	})
	return i.computedDiagnostics
}

func (i *includeProcessor) addProcessingDiagnostic(d ...*processingDiagnostic) {
	i.processingDiagnostics = append(i.processingDiagnostics, d...)
}

func (i *includeProcessor) addProcessingDiagnosticsForFileCasing(file tspath.Path, existingCasing string, currentCasing string, reason *FileIncludeReason) {
	if !reason.isReferencedFile() && slices.ContainsFunc(i.fileIncludeReasons[file], func(r *FileIncludeReason) bool {
		return r.isReferencedFile()
	}) {
		i.addProcessingDiagnostic(&processingDiagnostic{
			kind: processingDiagnosticKindExplainingFileInclude,
			data: &includeExplainingDiagnostic{
				file:             file,
				diagnosticReason: reason,
				message:          diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing,
				args:             []any{existingCasing, currentCasing},
			},
		})
	} else {
		i.addProcessingDiagnostic(&processingDiagnostic{
			kind: processingDiagnosticKindExplainingFileInclude,
			data: &includeExplainingDiagnostic{
				file:             file,
				diagnosticReason: reason,
				message:          diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing,
				args:             []any{currentCasing, existingCasing},
			},
		})
	}
}

func (i *includeProcessor) getReferenceLocation(r *FileIncludeReason, program *Program) *referenceFileLocation {
	if existing, ok := i.reasonToReferenceLocation.Load(r); ok {
		return existing
	}

	loc, _ := i.reasonToReferenceLocation.LoadOrStore(r, r.getReferencedLocation(program))
	return loc
}

func (i *includeProcessor) getCompilerOptionsObjectLiteralSyntax(program *Program) *ast.ObjectLiteralExpression {
	i.compilerOptionsSyntaxOnce.Do(func() {
		configFile := program.opts.Config.ConfigFile
		if configFile != nil {
			if compilerOptionsProperty := tsoptions.ForEachTsConfigPropArray(configFile.SourceFile, "compilerOptions", core.Identity); compilerOptionsProperty != nil &&
				compilerOptionsProperty.Initializer != nil &&
				ast.IsObjectLiteralExpression(compilerOptionsProperty.Initializer) {
				i.compilerOptionsSyntax = compilerOptionsProperty.Initializer.AsObjectLiteralExpression()
			}
		} else {
			i.compilerOptionsSyntax = nil
		}
	})
	return i.compilerOptionsSyntax
}

func (i *includeProcessor) getRelatedInfo(r *FileIncludeReason, program *Program) *ast.Diagnostic {
	if existing, ok := i.includeReasonToRelatedInfo.Load(r); ok {
		return existing
	}

	relatedInfo, _ := i.includeReasonToRelatedInfo.LoadOrStore(r, r.toRelatedInfo(program))
	return relatedInfo
}

func (i *includeProcessor) explainRedirectAndImpliedFormat(
	program *Program,
	filePath tspath.Path,
	toFileName func(fileName string) string,
) []*ast.Diagnostic {
	if existing, ok := i.redirectAndFileFormat.Load(filePath); ok {
		return existing
	}
	var file ast.HasFileName
	var sourceFile *ast.SourceFile
	redirectsFile := program.redirectFilesByPath[filePath]
	if redirectsFile != nil {
		file = redirectsFile
	} else {
		sourceFile = program.GetSourceFileByPath(filePath)
		if sourceFile == nil {
			return nil
		}
		file = sourceFile
	}
	var result []*ast.Diagnostic
	if source := program.GetSourceOfProjectReferenceIfOutputIncluded(file); source != file.FileName() {
		result = append(result, ast.NewCompilerDiagnostic(
			diagnostics.File_is_output_of_project_reference_source_0,
			toFileName(source),
		))
	}

	if redirectsFile != nil {
		targetFile := program.GetSourceFileByPath(redirectsFile.target)
		result = append(result, ast.NewCompilerDiagnostic(
			diagnostics.File_redirects_to_file_0,
			toFileName(targetFile.FileName()),
		))
	}

	if sourceFile != nil && ast.IsExternalOrCommonJSModule(sourceFile) {
		metaData := program.GetSourceFileMetaData(file.Path())
		switch program.GetImpliedNodeFormatForEmit(file) {
		case core.ModuleKindESNext:
			if metaData.PackageJsonType == "module" {
				result = append(result, ast.NewCompilerDiagnostic(
					diagnostics.File_is_ECMAScript_module_because_0_has_field_type_with_value_module,
					toFileName(metaData.PackageJsonDirectory+"/package.json"),
				))
			}
		case core.ModuleKindCommonJS:
			if metaData.PackageJsonType != "" {
				result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_0_has_field_type_whose_value_is_not_module, toFileName(metaData.PackageJsonDirectory+"/package.json")))
			} else if metaData.PackageJsonDirectory != "" {
				if metaData.PackageJsonType == "" {
					result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_0_does_not_have_field_type, toFileName(metaData.PackageJsonDirectory+"/package.json")))
				}
			} else {
				result = append(result, ast.NewCompilerDiagnostic(diagnostics.File_is_CommonJS_module_because_package_json_was_not_found))
			}
		}
	}

	result, _ = i.redirectAndFileFormat.LoadOrStore(filePath, result)
	return result
}
