package sourcemap

import (
	"encoding/base64"
	"errors"
	"slices"
	"strings"

	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/json"
	"github.com/microsoft/typescript-go/internal/tspath"
)

type (
	SourceIndex int
	NameIndex   int
)

const (
	sourceIndexNotSet SourceIndex      = -1
	nameIndexNotSet   NameIndex        = -1
	notSet            int              = -1
	notSetUTF16       core.UTF16Offset = -1
)

type Generator struct {
	pathOptions               tspath.ComparePathsOptions
	file                      string
	sourceRoot                string
	sourcesDirectoryPath      string
	rawSources                []string
	sources                   []string
	sourceToSourceIndexMap    map[string]SourceIndex
	sourcesContent            []*string
	names                     []string
	nameToNameIndexMap        map[string]NameIndex
	mappings                  strings.Builder
	lastGeneratedLine         int
	lastGeneratedCharacter    core.UTF16Offset
	lastSourceIndex           SourceIndex
	lastSourceLine            int
	lastSourceCharacter       core.UTF16Offset
	lastNameIndex             NameIndex
	hasLast                   bool
	pendingGeneratedLine      int
	pendingGeneratedCharacter core.UTF16Offset
	pendingSourceIndex        SourceIndex
	pendingSourceLine         int
	pendingSourceCharacter    core.UTF16Offset
	pendingNameIndex          NameIndex
	hasPending                bool
	hasPendingSource          bool
	hasPendingName            bool
}

type RawSourceMap struct {
	Version        int       `json:"version"`
	File           string    `json:"file"`
	SourceRoot     string    `json:"sourceRoot"`
	Sources        []string  `json:"sources"`
	Names          []string  `json:"names"`
	Mappings       string    `json:"mappings"`
	SourcesContent []*string `json:"sourcesContent,omitzero"`
}

func NewGenerator(file string, sourceRoot string, sourcesDirectoryPath string, options tspath.ComparePathsOptions) *Generator {
	return &Generator{
		file:                 file,
		sourceRoot:           sourceRoot,
		sourcesDirectoryPath: sourcesDirectoryPath,
		pathOptions:          options,
	}
}

func (gen *Generator) Sources() []string { return gen.rawSources }

// Adds a source to the source map
func (gen *Generator) AddSource(fileName string) SourceIndex {
	source := tspath.GetRelativePathToDirectoryOrUrl(
		gen.sourcesDirectoryPath,
		fileName,
		true, /*isAbsolutePathAnUrl*/
		gen.pathOptions,
	)

	sourceIndex, found := gen.sourceToSourceIndexMap[source]
	if !found {
		sourceIndex = SourceIndex(len(gen.sources))
		gen.sources = append(gen.sources, source)
		gen.rawSources = append(gen.rawSources, fileName)
		if gen.sourceToSourceIndexMap == nil {
			gen.sourceToSourceIndexMap = make(map[string]SourceIndex)
		}
		gen.sourceToSourceIndexMap[source] = sourceIndex
	}

	return sourceIndex
}

// Sets the content for a source
func (gen *Generator) SetSourceContent(sourceIndex SourceIndex, content string) error {
	if sourceIndex < 0 || int(sourceIndex) >= len(gen.sources) {
		return errors.New("sourceIndex is out of range")
	}
	for len(gen.sourcesContent) <= int(sourceIndex) {
		gen.sourcesContent = append(gen.sourcesContent, nil)
	}
	gen.sourcesContent[sourceIndex] = &content
	return nil
}

// Declares a name in the source map, returning the index of the name
func (gen *Generator) AddName(name string) NameIndex {
	nameIndex, found := gen.nameToNameIndexMap[name]
	if !found {
		nameIndex = NameIndex(len(gen.names))
		gen.names = append(gen.names, name)
		if gen.nameToNameIndexMap == nil {
			gen.nameToNameIndexMap = make(map[string]NameIndex)
		}
		gen.nameToNameIndexMap[name] = nameIndex
	}
	return nameIndex
}

func (gen *Generator) isNewGeneratedPosition(generatedLine int, generatedCharacter core.UTF16Offset) bool {
	return !gen.hasPending ||
		gen.pendingGeneratedLine != generatedLine ||
		gen.pendingGeneratedCharacter != generatedCharacter
}

func (gen *Generator) isBacktrackingSourcePosition(sourceIndex SourceIndex, sourceLine int, sourceCharacter core.UTF16Offset) bool {
	return sourceIndex != sourceIndexNotSet &&
		sourceLine != notSet &&
		sourceCharacter != notSetUTF16 &&
		gen.pendingSourceIndex == sourceIndex &&
		(gen.pendingSourceLine > sourceLine ||
			gen.pendingSourceLine == sourceLine && gen.pendingSourceCharacter > sourceCharacter)
}

func (gen *Generator) shouldCommitMapping() bool {
	return gen.hasPending && (!gen.hasLast ||
		gen.lastGeneratedLine != gen.pendingGeneratedLine ||
		gen.lastGeneratedCharacter != gen.pendingGeneratedCharacter ||
		gen.lastSourceIndex != gen.pendingSourceIndex ||
		gen.lastSourceLine != gen.pendingSourceLine ||
		gen.lastSourceCharacter != gen.pendingSourceCharacter ||
		gen.lastNameIndex != gen.pendingNameIndex)
}

func (gen *Generator) appendMappingCharCode(charCode rune) {
	gen.mappings.WriteRune(charCode)
}

func (gen *Generator) appendBase64VLQ(inValue int) {
	// Add a new least significant bit that has the sign of the value.
	// if negative number the least significant bit that gets added to the number has value 1
	// else least significant bit value that gets added is 0
	// eg. -1 changes to binary : 01 [1] => 3
	//     +1 changes to binary : 01 [0] => 2
	if inValue < 0 {
		inValue = ((-inValue) << 1) + 1
	} else {
		inValue = inValue << 1
	}

	// Encode 5 bits at a time starting from least significant bits
	for {
		currentDigit := inValue & 31 // 11111
		inValue = inValue >> 5
		if inValue > 0 {
			// There are still more digits to decode, set the msb (6th bit)
			currentDigit = currentDigit | 32
		}
		gen.appendMappingCharCode(base64FormatEncode(currentDigit))
		if inValue <= 0 {
			break
		}
	}
}

func (gen *Generator) commitPendingMapping() {
	if !gen.shouldCommitMapping() {
		return
	}

	// Line/Comma delimiters
	if gen.lastGeneratedLine < gen.pendingGeneratedLine {
		// Emit line delimiters
		for {
			gen.appendMappingCharCode(';')
			gen.lastGeneratedLine++
			if gen.lastGeneratedLine >= gen.pendingGeneratedLine {
				break
			}
		}
		// Only need to set this once
		gen.lastGeneratedCharacter = 0
	} else {
		if gen.lastGeneratedLine != gen.pendingGeneratedLine {
			// panic rather than error as an invariant has been violated
			panic("generatedLine cannot backtrack")
		}
		// Emit comma to separate the entry
		if gen.hasLast {
			gen.appendMappingCharCode(',')
		}
	}

	// 1. Relative generated character
	gen.appendBase64VLQ(int(gen.pendingGeneratedCharacter - gen.lastGeneratedCharacter))
	gen.lastGeneratedCharacter = gen.pendingGeneratedCharacter

	if gen.hasPendingSource {
		// 2. Relative sourceIndex
		gen.appendBase64VLQ(int(gen.pendingSourceIndex - gen.lastSourceIndex))
		gen.lastSourceIndex = gen.pendingSourceIndex

		// 3. Relative source line
		gen.appendBase64VLQ(gen.pendingSourceLine - gen.lastSourceLine)
		gen.lastSourceLine = gen.pendingSourceLine

		// 4. Relative source character
		gen.appendBase64VLQ(int(gen.pendingSourceCharacter - gen.lastSourceCharacter))
		gen.lastSourceCharacter = gen.pendingSourceCharacter

		if gen.hasPendingName {
			// 5. Relative nameIndex
			gen.appendBase64VLQ(int(gen.pendingNameIndex - gen.lastNameIndex))
			gen.lastNameIndex = gen.pendingNameIndex
		}
	}

	gen.hasLast = true
}

func (gen *Generator) addMapping(generatedLine int, generatedCharacter core.UTF16Offset, sourceIndex SourceIndex, sourceLine int, sourceCharacter core.UTF16Offset, nameIndex NameIndex) {
	if gen.isNewGeneratedPosition(generatedLine, generatedCharacter) ||
		gen.isBacktrackingSourcePosition(sourceIndex, sourceLine, sourceCharacter) {
		gen.commitPendingMapping()
		gen.pendingGeneratedLine = generatedLine
		gen.pendingGeneratedCharacter = generatedCharacter
		gen.hasPendingSource = false
		gen.hasPendingName = false
		gen.hasPending = true
	}

	if sourceIndex != sourceIndexNotSet && sourceLine != notSet && sourceCharacter != notSetUTF16 {
		gen.pendingSourceIndex = sourceIndex
		gen.pendingSourceLine = sourceLine
		gen.pendingSourceCharacter = sourceCharacter
		gen.hasPendingSource = true
		if nameIndex != nameIndexNotSet {
			gen.pendingNameIndex = nameIndex
			gen.hasPendingName = true
		}
	}
}

// Adds a mapping without source information
func (gen *Generator) AddGeneratedMapping(generatedLine int, generatedCharacter core.UTF16Offset) error {
	if generatedLine < gen.pendingGeneratedLine {
		return errors.New("generatedLine cannot backtrack")
	}
	if generatedCharacter < 0 {
		return errors.New("generatedCharacter cannot be negative")
	}
	gen.addMapping(generatedLine, generatedCharacter, sourceIndexNotSet, notSet /*sourceLine*/, notSetUTF16 /*sourceCharacter*/, nameIndexNotSet)
	return nil
}

// Adds a mapping with source information
func (gen *Generator) AddSourceMapping(generatedLine int, generatedCharacter core.UTF16Offset, sourceIndex SourceIndex, sourceLine int, sourceCharacter core.UTF16Offset) error {
	if generatedLine < gen.pendingGeneratedLine {
		return errors.New("generatedLine cannot backtrack")
	}
	if generatedCharacter < 0 {
		return errors.New("generatedCharacter cannot be negative")
	}
	if sourceIndex < 0 || int(sourceIndex) >= len(gen.sources) {
		return errors.New("sourceIndex is out of range")
	}
	if sourceLine < 0 {
		return errors.New("sourceLine cannot be negative")
	}
	if sourceCharacter < 0 {
		return errors.New("sourceCharacter cannot be negative")
	}
	gen.addMapping(generatedLine, generatedCharacter, sourceIndex, sourceLine, sourceCharacter, nameIndexNotSet)
	return nil
}

// Adds a mapping with source and name information
func (gen *Generator) AddNamedSourceMapping(generatedLine int, generatedCharacter core.UTF16Offset, sourceIndex SourceIndex, sourceLine int, sourceCharacter core.UTF16Offset, nameIndex NameIndex) error {
	if generatedLine < gen.pendingGeneratedLine {
		return errors.New("generatedLine cannot backtrack")
	}
	if generatedCharacter < 0 {
		return errors.New("generatedCharacter cannot be negative")
	}
	if sourceIndex < 0 || int(sourceIndex) >= len(gen.sources) {
		return errors.New("sourceIndex is out of range")
	}
	if sourceLine < 0 {
		return errors.New("sourceLine cannot be negative")
	}
	if sourceCharacter < 0 {
		return errors.New("sourceCharacter cannot be negative")
	}
	if nameIndex < 0 || int(nameIndex) >= len(gen.names) {
		return errors.New("nameIndex is out of range")
	}
	gen.addMapping(generatedLine, generatedCharacter, sourceIndex, sourceLine, sourceCharacter, nameIndex)
	return nil
}

// Gets the source map as a `RawSourceMap` object
func (gen *Generator) RawSourceMap() *RawSourceMap {
	gen.commitPendingMapping()
	sources := slices.Clone(gen.sources)
	if sources == nil {
		sources = []string{}
	}
	names := slices.Clone(gen.names)
	if names == nil {
		names = []string{}
	}
	return &RawSourceMap{
		Version:        3,
		File:           gen.file,
		SourceRoot:     gen.sourceRoot,
		Sources:        sources,
		Names:          names,
		Mappings:       gen.mappings.String(),
		SourcesContent: slices.Clone(gen.sourcesContent),
	}
}

func (gen *Generator) bytes() []byte {
	buf, err := json.Marshal(gen.RawSourceMap())
	if err != nil {
		panic(err.Error())
	}
	return buf
}

// Gets the string representation of the source map
func (gen *Generator) String() string {
	return string(gen.bytes())
}

func (gen *Generator) Base64DataURL() string {
	const prefix = "data:application/json;base64,"
	data := gen.bytes()
	var sb strings.Builder
	sb.Grow(len(prefix) + base64.StdEncoding.EncodedLen(len(data)))
	sb.WriteString(prefix)
	encoder := base64.NewEncoder(base64.StdEncoding, &sb)
	_, _ = encoder.Write(data)
	encoder.Close()
	return sb.String()
}

func base64FormatEncode(value int) rune {
	switch {
	case value >= 0 && value < 26:
		return 'A' + rune(value)
	case value >= 26 && value < 52:
		return 'a' + rune(value) - 26
	case value >= 52 && value < 62:
		return '0' + rune(value) - 52
	case value == 62:
		return '+'
	case value == 63:
		return '/'
	default:
		panic("not a base64 value")
	}
}
