package compiler

import (
	"fmt"
	"sync"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/diagnostics"
	"github.com/microsoft/typescript-go/internal/module"
	"github.com/microsoft/typescript-go/internal/scanner"
	"github.com/microsoft/typescript-go/internal/tsoptions"
	"github.com/microsoft/typescript-go/internal/tspath"
)

type fileIncludeKind int

const (
	// References from file
	fileIncludeKindImport = iota
	fileIncludeKindReferenceFile
	fileIncludeKindTypeReferenceDirective
	fileIncludeKindLibReferenceDirective

	fileIncludeKindRootFile
	fileIncludeKindLibFile
	fileIncludeKindAutomaticTypeDirectiveFile
)

type FileIncludeReason struct {
	kind fileIncludeKind
	data any

	// Uses relative file name
	relativeFileNameDiag     *ast.Diagnostic
	relativeFileNameDiagOnce sync.Once

	// Uses file name as is
	diag     *ast.Diagnostic
	diagOnce sync.Once
}

type referencedFileData struct {
	file      tspath.Path
	index     int
	synthetic *ast.Node
}

type referenceFileLocation struct {
	file        *ast.SourceFile
	node        *ast.Node
	ref         *ast.FileReference
	packageId   module.PackageId
	isSynthetic bool
}

func (r *referenceFileLocation) text() string {
	if r.node != nil {
		if !ast.NodeIsSynthesized(r.node) {
			return r.file.Text()[scanner.SkipTrivia(r.file.Text(), r.node.Loc.Pos()):r.node.End()]
		} else {
			return fmt.Sprintf(`"%s"`, r.node.Text())
		}
	} else {
		return r.file.Text()[r.ref.Pos():r.ref.End()]
	}
}

func (r *referenceFileLocation) diagnosticAt(message *diagnostics.Message, args ...any) *ast.Diagnostic {
	if r.node != nil {
		return tsoptions.CreateDiagnosticForNodeInSourceFile(r.file, r.node, message, args...)
	} else {
		return ast.NewDiagnostic(r.file, r.ref.TextRange, message, args...)
	}
}

type automaticTypeDirectiveFileData struct {
	typeReference string
	packageId     module.PackageId
}

func (r *FileIncludeReason) asIndex() int {
	return r.data.(int)
}

func (r *FileIncludeReason) asLibFileIndex() (int, bool) {
	index, ok := r.data.(int)
	return index, ok
}

func (r *FileIncludeReason) isReferencedFile() bool {
	return r != nil && r.kind <= fileIncludeKindLibReferenceDirective
}

func (r *FileIncludeReason) asReferencedFileData() *referencedFileData {
	return r.data.(*referencedFileData)
}

func (r *FileIncludeReason) asAutomaticTypeDirectiveFileData() *automaticTypeDirectiveFileData {
	return r.data.(*automaticTypeDirectiveFileData)
}

func (r *FileIncludeReason) getReferencedLocation(program *Program) *referenceFileLocation {
	ref := r.asReferencedFileData()
	file := program.GetSourceFileByPath(ref.file)
	switch r.kind {
	case fileIncludeKindImport:
		var specifier *ast.Node
		var isSynthetic bool
		if ref.synthetic != nil {
			specifier = ref.synthetic
			isSynthetic = true
		} else if ref.index < len(file.Imports()) {
			specifier = file.Imports()[ref.index]
		} else {
			augIndex := len(file.Imports())
			for _, imp := range file.ModuleAugmentations {
				if imp.Kind == ast.KindStringLiteral {
					if augIndex == ref.index {
						specifier = imp
						break
					}
					augIndex++
				}
			}
		}
		resolution := program.GetResolvedModuleFromModuleSpecifier(file, specifier)
		return &referenceFileLocation{
			file:        file,
			node:        specifier,
			packageId:   resolution.PackageId,
			isSynthetic: isSynthetic,
		}
	case fileIncludeKindReferenceFile:
		return &referenceFileLocation{
			file: file,
			ref:  file.ReferencedFiles[ref.index],
		}
	case fileIncludeKindTypeReferenceDirective:
		return &referenceFileLocation{
			file: file,
			ref:  file.TypeReferenceDirectives[ref.index],
		}
	case fileIncludeKindLibReferenceDirective:
		return &referenceFileLocation{
			file: file,
			ref:  file.LibReferenceDirectives[ref.index],
		}
	default:
		panic(fmt.Sprintf("unknown reason: %v", r.kind))
	}
}

func (r *FileIncludeReason) toDiagnostic(program *Program, relativeFileName bool) *ast.Diagnostic {
	if relativeFileName {
		r.relativeFileNameDiagOnce.Do(func() {
			r.relativeFileNameDiag = r.computeDiagnostic(program, func(fileName string) string {
				return tspath.GetRelativePathFromDirectory(program.GetCurrentDirectory(), fileName, program.comparePathsOptions)
			})
		})
		return r.relativeFileNameDiag
	} else {
		r.diagOnce.Do(func() {
			r.diag = r.computeDiagnostic(program, func(fileName string) string { return fileName })
		})
		return r.diag
	}
}

func (r *FileIncludeReason) computeDiagnostic(program *Program, toFileName func(string) string) *ast.Diagnostic {
	if r.isReferencedFile() {
		return r.computeReferenceFileDiagnostic(program, toFileName)
	}
	switch r.kind {
	case fileIncludeKindRootFile:
		if program.opts.Config.ConfigFile != nil {
			config := program.opts.Config
			fileName := tspath.GetNormalizedAbsolutePath(config.FileNames()[r.asIndex()], program.GetCurrentDirectory())
			if matchedFileSpec := config.GetMatchedFileSpec(fileName); matchedFileSpec != "" {
				return ast.NewCompilerDiagnostic(diagnostics.Part_of_files_list_in_tsconfig_json, matchedFileSpec, toFileName(fileName))
			} else if matchedIncludeSpec, isDefaultIncludeSpec := config.GetMatchedIncludeSpec(fileName); matchedIncludeSpec != "" {
				if isDefaultIncludeSpec {
					return ast.NewCompilerDiagnostic(diagnostics.Matched_by_default_include_pattern_Asterisk_Asterisk_Slash_Asterisk)
				} else {
					return ast.NewCompilerDiagnostic(diagnostics.Matched_by_include_pattern_0_in_1, matchedIncludeSpec, toFileName(config.ConfigName()))
				}
			} else {
				return ast.NewCompilerDiagnostic(diagnostics.Root_file_specified_for_compilation)
			}
		} else {
			return ast.NewCompilerDiagnostic(diagnostics.Root_file_specified_for_compilation)
		}
	case fileIncludeKindAutomaticTypeDirectiveFile:
		data := r.asAutomaticTypeDirectiveFileData()
		if !program.Options().UsesWildcardTypes() {
			if data.packageId.Name != "" {
				return ast.NewCompilerDiagnostic(diagnostics.Entry_point_of_type_library_0_specified_in_compilerOptions_with_packageId_1, data.typeReference, data.packageId.String())
			} else {
				return ast.NewCompilerDiagnostic(diagnostics.Entry_point_of_type_library_0_specified_in_compilerOptions, data.typeReference)
			}
		} else {
			if data.packageId.Name != "" {
				return ast.NewCompilerDiagnostic(diagnostics.Entry_point_for_implicit_type_library_0_with_packageId_1, data.typeReference, data.packageId.String())
			} else {
				return ast.NewCompilerDiagnostic(diagnostics.Entry_point_for_implicit_type_library_0, data.typeReference)
			}
		}
	case fileIncludeKindLibFile:
		if index, ok := r.asLibFileIndex(); ok {
			return ast.NewCompilerDiagnostic(diagnostics.Library_0_specified_in_compilerOptions, program.Options().Lib[index])
		} else if target := program.Options().GetEmitScriptTarget().String(); target != "" {
			return ast.NewCompilerDiagnostic(diagnostics.Default_library_for_target_0, target)
		} else {
			return ast.NewCompilerDiagnostic(diagnostics.Default_library)
		}
	default:
		panic(fmt.Sprintf("unknown reason: %v", r.kind))
	}
}

func (r *FileIncludeReason) computeReferenceFileDiagnostic(program *Program, toFileName func(string) string) *ast.Diagnostic {
	referenceLocation := program.includeProcessor.getReferenceLocation(r, program)
	referenceText := referenceLocation.text()
	switch r.kind {
	case fileIncludeKindImport:
		if !referenceLocation.isSynthetic {
			if referenceLocation.packageId.Name != "" {
				return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_with_packageId_2, referenceText, toFileName(referenceLocation.file.FileName()), referenceLocation.packageId.String())
			} else {
				return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1, referenceText, toFileName(referenceLocation.file.FileName()))
			}
		} else if specifier, ok := program.importHelpersImportSpecifiers[referenceLocation.file.Path()]; ok && specifier == referenceLocation.node {
			if referenceLocation.packageId.Name != "" {
				return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_with_packageId_2_to_import_importHelpers_as_specified_in_compilerOptions, referenceText, toFileName(referenceLocation.file.FileName()), referenceLocation.packageId.String())
			} else {
				return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_to_import_importHelpers_as_specified_in_compilerOptions, referenceText, toFileName(referenceLocation.file.FileName()))
			}
		} else {
			if referenceLocation.packageId.Name != "" {
				return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_with_packageId_2_to_import_jsx_and_jsxs_factory_functions, referenceText, toFileName(referenceLocation.file.FileName()), referenceLocation.packageId.String())
			} else {
				return ast.NewCompilerDiagnostic(diagnostics.Imported_via_0_from_file_1_to_import_jsx_and_jsxs_factory_functions, referenceText, toFileName(referenceLocation.file.FileName()))
			}
		}
	case fileIncludeKindReferenceFile:
		return ast.NewCompilerDiagnostic(diagnostics.Referenced_via_0_from_file_1, referenceText, toFileName(referenceLocation.file.FileName()))
	case fileIncludeKindTypeReferenceDirective:
		if referenceLocation.packageId.Name != "" {
			return ast.NewCompilerDiagnostic(diagnostics.Type_library_referenced_via_0_from_file_1_with_packageId_2, referenceText, toFileName(referenceLocation.file.FileName()), referenceLocation.packageId.String())
		} else {
			return ast.NewCompilerDiagnostic(diagnostics.Type_library_referenced_via_0_from_file_1, referenceText, toFileName(referenceLocation.file.FileName()))
		}
	case fileIncludeKindLibReferenceDirective:
		return ast.NewCompilerDiagnostic(diagnostics.Library_referenced_via_0_from_file_1, referenceText, toFileName(referenceLocation.file.FileName()))
	default:
		panic(fmt.Sprintf("unknown reason: %v", r.kind))
	}
}

func (r *FileIncludeReason) toRelatedInfo(program *Program) *ast.Diagnostic {
	if r.isReferencedFile() {
		return r.computeReferenceFileRelatedInfo(program)
	}
	if program.opts.Config.ConfigFile == nil {
		return nil
	}
	config := program.opts.Config
	switch r.kind {
	case fileIncludeKindRootFile:
		fileName := tspath.GetNormalizedAbsolutePath(config.FileNames()[r.asIndex()], program.GetCurrentDirectory())
		if matchedFileSpec := config.GetMatchedFileSpec(fileName); matchedFileSpec != "" {
			if filesNode := tsoptions.GetTsConfigPropArrayElementValue(config.ConfigFile.SourceFile, "files", matchedFileSpec); filesNode != nil {
				return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, filesNode.AsNode(), diagnostics.File_is_matched_by_files_list_specified_here)
			}
		} else if matchedIncludeSpec, isDefaultIncludeSpec := config.GetMatchedIncludeSpec(fileName); matchedIncludeSpec != "" && !isDefaultIncludeSpec {
			if includeNode := tsoptions.GetTsConfigPropArrayElementValue(config.ConfigFile.SourceFile, "include", matchedIncludeSpec); includeNode != nil {
				return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, includeNode.AsNode(), diagnostics.File_is_matched_by_include_pattern_specified_here)
			}
		}
	case fileIncludeKindAutomaticTypeDirectiveFile:
		if !program.Options().UsesWildcardTypes() {
			data := r.asAutomaticTypeDirectiveFileData()
			if typesSyntax := tsoptions.GetOptionsSyntaxByArrayElementValue(program.includeProcessor.getCompilerOptionsObjectLiteralSyntax(program), "types", data.typeReference); typesSyntax != nil {
				return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, typesSyntax.AsNode(), diagnostics.File_is_entry_point_of_type_library_specified_here)
			}
		}
	case fileIncludeKindLibFile:
		if index, ok := r.asLibFileIndex(); ok {
			if libSyntax := tsoptions.GetOptionsSyntaxByArrayElementValue(program.includeProcessor.getCompilerOptionsObjectLiteralSyntax(program), "lib", program.Options().Lib[index]); libSyntax != nil {
				return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, libSyntax.AsNode(), diagnostics.File_is_library_specified_here)
			}
		} else if target := program.Options().GetEmitScriptTarget().String(); target != "" {
			if targetValueSyntax := tsoptions.ForEachPropertyAssignment(program.includeProcessor.getCompilerOptionsObjectLiteralSyntax(program), "target", tsoptions.GetCallbackForFindingPropertyAssignmentByValue(target)); targetValueSyntax != nil {
				return tsoptions.CreateDiagnosticForNodeInSourceFile(config.ConfigFile.SourceFile, targetValueSyntax.AsNode(), diagnostics.File_is_default_library_for_target_specified_here)
			}
		}
	default:
		panic(fmt.Sprintf("unknown reason: %v", r.kind))
	}
	return nil
}

func (r *FileIncludeReason) computeReferenceFileRelatedInfo(program *Program) *ast.Diagnostic {
	referenceLocation := program.includeProcessor.getReferenceLocation(r, program)
	if referenceLocation.isSynthetic {
		return nil
	}
	switch r.kind {
	case fileIncludeKindImport:
		return referenceLocation.diagnosticAt(diagnostics.File_is_included_via_import_here)
	case fileIncludeKindReferenceFile:
		return referenceLocation.diagnosticAt(diagnostics.File_is_included_via_reference_here)
	case fileIncludeKindTypeReferenceDirective:
		return referenceLocation.diagnosticAt(diagnostics.File_is_included_via_type_library_reference_here)
	case fileIncludeKindLibReferenceDirective:
		return referenceLocation.diagnosticAt(diagnostics.File_is_included_via_library_reference_here)
	default:
		panic(fmt.Sprintf("unknown reason: %v", r.kind))
	}
}
