package sourcemap

import (
	"bytes"
	"unicode/utf8"

	"github.com/evanw/esbuild/internal/helpers"
	"github.com/evanw/esbuild/internal/logger"
)

type Mapping struct {
	GeneratedLine   int32 // 0-based
	GeneratedColumn int32 // 0-based count of UTF-16 code units

	SourceIndex    int32 // 0-based
	OriginalLine   int32 // 0-based
	OriginalColumn int32 // 0-based count of UTF-16 code units
}

type SourceMap struct {
	Sources        []string
	SourcesContent []SourceContent
	Mappings       []Mapping
}

type SourceContent struct {
	// This stores both the unquoted and the quoted values. We try to use the
	// already-quoted value if possible so we don't need to re-quote it
	// unnecessarily for maximum performance.
	Quoted string

	// But sometimes we need to re-quote the value, such as when it contains
	// non-ASCII characters and we are in ASCII-only mode. In that case we quote
	// this parsed UTF-16 value.
	Value []uint16
}

func (sm *SourceMap) Find(line int32, column int32) *Mapping {
	mappings := sm.Mappings

	// Binary search
	count := len(mappings)
	index := 0
	for count > 0 {
		step := count / 2
		i := index + step
		mapping := mappings[i]
		if mapping.GeneratedLine < line || (mapping.GeneratedLine == line && mapping.GeneratedColumn <= column) {
			index = i + 1
			count -= step + 1
		} else {
			count = step
		}
	}

	// Handle search failure
	if index > 0 {
		mapping := &mappings[index-1]

		// Match the behavior of the popular "source-map" library from Mozilla
		if mapping.GeneratedLine == line {
			return mapping
		}
	}
	return nil
}

var base64 = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")

// A single base 64 digit can contain 6 bits of data. For the base 64 variable
// length quantities we use in the source map spec, the first bit is the sign,
// the next four bits are the actual value, and the 6th bit is the continuation
// bit. The continuation bit tells us whether there are more digits in this
// value following this digit.
//
//   Continuation
//   |    Sign
//   |    |
//   V    V
//   101011
//
func EncodeVLQ(value int) []byte {
	var vlq int
	if value < 0 {
		vlq = ((-value) << 1) | 1
	} else {
		vlq = value << 1
	}

	// Handle the common case up front without allocations
	if (vlq >> 5) == 0 {
		digit := vlq & 31
		return base64[digit : digit+1]
	}

	encoded := []byte{}
	for {
		digit := vlq & 31
		vlq >>= 5

		// If there are still more digits in this value, we must make sure the
		// continuation bit is marked
		if vlq != 0 {
			digit |= 32
		}

		encoded = append(encoded, base64[digit])

		if vlq == 0 {
			break
		}
	}

	return encoded
}

func DecodeVLQ(encoded []byte, start int) (int, int) {
	shift := 0
	vlq := 0

	// Scan over the input
	for {
		index := bytes.IndexByte(base64, encoded[start])
		if index < 0 {
			break
		}

		// Decode a single byte
		vlq |= (index & 31) << shift
		start++
		shift += 5

		// Stop if there's no continuation bit
		if (index & 32) == 0 {
			break
		}
	}

	// Recover the value
	value := vlq >> 1
	if (vlq & 1) != 0 {
		value = -value
	}
	return value, start
}

func DecodeVLQUTF16(encoded []uint16) (int, int, bool) {
	n := len(encoded)
	if n == 0 {
		return 0, 0, false
	}

	// Scan over the input
	current := 0
	shift := 0
	vlq := 0
	for {
		if current >= n {
			return 0, 0, false
		}
		index := bytes.IndexByte(base64, byte(encoded[current]))
		if index < 0 {
			return 0, 0, false
		}

		// Decode a single byte
		vlq |= (index & 31) << shift
		current++
		shift += 5

		// Stop if there's no continuation bit
		if (index & 32) == 0 {
			break
		}
	}

	// Recover the value
	var value = vlq >> 1
	if (vlq & 1) != 0 {
		value = -value
	}
	return value, current, true
}

type LineColumnOffset struct {
	Lines   int
	Columns int
}

func (a LineColumnOffset) ComesBefore(b LineColumnOffset) bool {
	return a.Lines < b.Lines || (a.Lines == b.Lines && a.Columns < b.Columns)
}

func (a *LineColumnOffset) Add(b LineColumnOffset) {
	if b.Lines == 0 {
		a.Columns += b.Columns
	} else {
		a.Lines += b.Lines
		a.Columns = b.Columns
	}
}

func (offset *LineColumnOffset) AdvanceBytes(bytes []byte) {
	columns := offset.Columns
	for len(bytes) > 0 {
		c, width := utf8.DecodeRune(bytes)
		bytes = bytes[width:]
		switch c {
		case '\r', '\n', '\u2028', '\u2029':
			// Handle Windows-specific "\r\n" newlines
			if c == '\r' && len(bytes) > 0 && bytes[0] == '\n' {
				columns++
				continue
			}

			offset.Lines++
			columns = 0

		default:
			// Mozilla's "source-map" library counts columns using UTF-16 code units
			if c <= 0xFFFF {
				columns++
			} else {
				columns += 2
			}
		}
	}
	offset.Columns = columns
}

func (offset *LineColumnOffset) AdvanceString(text string) {
	columns := offset.Columns
	for i, c := range text {
		switch c {
		case '\r', '\n', '\u2028', '\u2029':
			// Handle Windows-specific "\r\n" newlines
			if c == '\r' && i+1 < len(text) && text[i+1] == '\n' {
				columns++
				continue
			}

			offset.Lines++
			columns = 0

		default:
			// Mozilla's "source-map" library counts columns using UTF-16 code units
			if c <= 0xFFFF {
				columns++
			} else {
				columns += 2
			}
		}
	}
	offset.Columns = columns
}

type SourceMapPieces struct {
	Prefix   []byte
	Mappings []byte
	Suffix   []byte
}

func (pieces SourceMapPieces) HasContent() bool {
	return len(pieces.Prefix)+len(pieces.Mappings)+len(pieces.Suffix) > 0
}

type SourceMapShift struct {
	Before LineColumnOffset
	After  LineColumnOffset
}

func (pieces SourceMapPieces) Finalize(shifts []SourceMapShift) []byte {
	// An optimized path for when there are no shifts
	if len(shifts) == 1 {
		bytes := pieces.Prefix
		minCap := len(bytes) + len(pieces.Mappings) + len(pieces.Suffix)
		if cap(bytes) < minCap {
			bytes = append(make([]byte, 0, minCap), bytes...)
		}
		bytes = append(bytes, pieces.Mappings...)
		bytes = append(bytes, pieces.Suffix...)
		return bytes
	}

	startOfRun := 0
	current := 0
	generated := LineColumnOffset{}
	prevShiftColumnDelta := 0
	j := helpers.Joiner{}

	// Start the source map
	j.AddBytes(pieces.Prefix)

	// This assumes that a) all mappings are valid and b) all mappings are ordered
	// by increasing generated position. This should be the case for all mappings
	// generated by esbuild, which should be the only mappings we process here.
	for current < len(pieces.Mappings) {
		// Handle a line break
		if pieces.Mappings[current] == ';' {
			generated.Lines++
			generated.Columns = 0
			prevShiftColumnDelta = 0
			current++
			continue
		}

		potentialEndOfRun := current

		// Read the generated column
		generatedColumnDelta, next := DecodeVLQ(pieces.Mappings, current)
		generated.Columns += generatedColumnDelta
		current = next

		potentialStartOfRun := current

		// Skip over the original position information
		_, current = DecodeVLQ(pieces.Mappings, current) // The original source
		_, current = DecodeVLQ(pieces.Mappings, current) // The original line
		_, current = DecodeVLQ(pieces.Mappings, current) // The original column

		// Skip a trailing comma
		if current < len(pieces.Mappings) && pieces.Mappings[current] == ',' {
			current++
		}

		// Detect crossing shift boundaries
		didCrossBoundary := false
		for len(shifts) > 1 && shifts[1].Before.ComesBefore(generated) {
			shifts = shifts[1:]
			didCrossBoundary = true
		}
		if !didCrossBoundary {
			continue
		}

		// This shift isn't relevant if the next mapping after this shift is on a
		// following line. In that case, don't split and keep scanning instead.
		shift := shifts[0]
		if shift.After.Lines != generated.Lines {
			continue
		}

		// Add all previous mappings in a single run for efficiency. Since source
		// mappings are relative, no data needs to be modified inside this run.
		j.AddBytes(pieces.Mappings[startOfRun:potentialEndOfRun])

		// Then modify the first mapping across the shift boundary with the updated
		// generated column value. It's simplest to only support column shifts. This
		// is reasonable because import paths should not contain newlines.
		if shift.Before.Lines != shift.After.Lines {
			panic("Unexpected line change when shifting source maps")
		}
		shiftColumnDelta := shift.After.Columns - shift.Before.Columns
		j.AddBytes(EncodeVLQ(generatedColumnDelta + shiftColumnDelta - prevShiftColumnDelta))
		prevShiftColumnDelta = shiftColumnDelta

		// Finally, start the next run after the end of this generated column offset
		startOfRun = potentialStartOfRun
	}

	// Finish the source map
	j.AddBytes(pieces.Mappings[startOfRun:])
	j.AddBytes(pieces.Suffix)
	return j.Done()
}

// Coordinates in source maps are stored using relative offsets for size
// reasons. When joining together chunks of a source map that were emitted
// in parallel for different parts of a file, we need to fix up the first
// segment of each chunk to be relative to the end of the previous chunk.
type SourceMapState struct {
	// This isn't stored in the source map. It's only used by the bundler to join
	// source map chunks together correctly.
	GeneratedLine int

	// These are stored in the source map in VLQ format.
	GeneratedColumn int
	SourceIndex     int
	OriginalLine    int
	OriginalColumn  int
}

// Source map chunks are computed in parallel for speed. Each chunk is relative
// to the zero state instead of being relative to the end state of the previous
// chunk, since it's impossible to know the end state of the previous chunk in
// a parallel computation.
//
// After all chunks are computed, they are joined together in a second pass.
// This rewrites the first mapping in each chunk to be relative to the end
// state of the previous chunk.
func AppendSourceMapChunk(j *helpers.Joiner, prevEndState SourceMapState, startState SourceMapState, sourceMap []byte) {
	// Handle line breaks in between this mapping and the previous one
	if startState.GeneratedLine != 0 {
		j.AddBytes(bytes.Repeat([]byte{';'}, startState.GeneratedLine))
		prevEndState.GeneratedColumn = 0
	}

	// Skip past any leading semicolons, which indicate line breaks
	semicolons := 0
	for sourceMap[semicolons] == ';' {
		semicolons++
	}
	if semicolons > 0 {
		j.AddBytes(sourceMap[:semicolons])
		sourceMap = sourceMap[semicolons:]
		prevEndState.GeneratedColumn = 0
		startState.GeneratedColumn = 0
	}

	// Strip off the first mapping from the buffer. The first mapping should be
	// for the start of the original file (the printer always generates one for
	// the start of the file).
	generatedColumn, i := DecodeVLQ(sourceMap, 0)
	sourceIndex, i := DecodeVLQ(sourceMap, i)
	originalLine, i := DecodeVLQ(sourceMap, i)
	originalColumn, i := DecodeVLQ(sourceMap, i)
	sourceMap = sourceMap[i:]

	// Rewrite the first mapping to be relative to the end state of the previous
	// chunk. We now know what the end state is because we're in the second pass
	// where all chunks have already been generated.
	startState.SourceIndex += sourceIndex
	startState.GeneratedColumn += generatedColumn
	startState.OriginalLine += originalLine
	startState.OriginalColumn += originalColumn
	j.AddBytes(appendMappingToBuffer(nil, j.LastByte(), prevEndState, startState))

	// Then append everything after that without modification.
	j.AddBytes(sourceMap)
}

func appendMappingToBuffer(buffer []byte, lastByte byte, prevState SourceMapState, currentState SourceMapState) []byte {
	// Put commas in between mappings
	if lastByte != 0 && lastByte != ';' && lastByte != '"' {
		buffer = append(buffer, ',')
	}

	// Record the generated column (the line is recorded using ';' elsewhere)
	buffer = append(buffer, EncodeVLQ(currentState.GeneratedColumn-prevState.GeneratedColumn)...)
	prevState.GeneratedColumn = currentState.GeneratedColumn

	// Record the generated source
	buffer = append(buffer, EncodeVLQ(currentState.SourceIndex-prevState.SourceIndex)...)
	prevState.SourceIndex = currentState.SourceIndex

	// Record the original line
	buffer = append(buffer, EncodeVLQ(currentState.OriginalLine-prevState.OriginalLine)...)
	prevState.OriginalLine = currentState.OriginalLine

	// Record the original column
	buffer = append(buffer, EncodeVLQ(currentState.OriginalColumn-prevState.OriginalColumn)...)
	prevState.OriginalColumn = currentState.OriginalColumn

	return buffer
}

type LineOffsetTable struct {
	byteOffsetToStartOfLine int32

	// The source map specification is very loose and does not specify what
	// column numbers actually mean. The popular "source-map" library from Mozilla
	// appears to interpret them as counts of UTF-16 code units, so we generate
	// those too for compatibility.
	//
	// We keep mapping tables around to accelerate conversion from byte offsets
	// to UTF-16 code unit counts. However, this mapping takes up a lot of memory
	// and generates a lot of garbage. Since most JavaScript is ASCII and the
	// mapping for ASCII is 1:1, we avoid creating a table for ASCII-only lines
	// as an optimization.
	byteOffsetToFirstNonASCII int32
	columnsForNonASCII        []int32
}

func GenerateLineOffsetTables(contents string, approximateLineCount int32) []LineOffsetTable {
	var ColumnsForNonASCII []int32
	ByteOffsetToFirstNonASCII := int32(0)
	lineByteOffset := 0
	columnByteOffset := 0
	column := int32(0)

	// Preallocate the top-level table using the approximate line count from the lexer
	lineOffsetTables := make([]LineOffsetTable, 0, approximateLineCount)

	for i, c := range contents {
		// Mark the start of the next line
		if column == 0 {
			lineByteOffset = i
		}

		// Start the mapping if this character is non-ASCII
		if c > 0x7F && ColumnsForNonASCII == nil {
			columnByteOffset = i - lineByteOffset
			ByteOffsetToFirstNonASCII = int32(columnByteOffset)
			ColumnsForNonASCII = []int32{}
		}

		// Update the per-byte column offsets
		if ColumnsForNonASCII != nil {
			for lineBytesSoFar := i - lineByteOffset; columnByteOffset <= lineBytesSoFar; columnByteOffset++ {
				ColumnsForNonASCII = append(ColumnsForNonASCII, column)
			}
		}

		switch c {
		case '\r', '\n', '\u2028', '\u2029':
			// Handle Windows-specific "\r\n" newlines
			if c == '\r' && i+1 < len(contents) && contents[i+1] == '\n' {
				column++
				continue
			}

			lineOffsetTables = append(lineOffsetTables, LineOffsetTable{
				byteOffsetToStartOfLine:   int32(lineByteOffset),
				byteOffsetToFirstNonASCII: ByteOffsetToFirstNonASCII,
				columnsForNonASCII:        ColumnsForNonASCII,
			})
			columnByteOffset = 0
			ByteOffsetToFirstNonASCII = 0
			ColumnsForNonASCII = nil
			column = 0

		default:
			// Mozilla's "source-map" library counts columns using UTF-16 code units
			if c <= 0xFFFF {
				column++
			} else {
				column += 2
			}
		}
	}

	// Mark the start of the next line
	if column == 0 {
		lineByteOffset = len(contents)
	}

	// Do one last update for the column at the end of the file
	if ColumnsForNonASCII != nil {
		for lineBytesSoFar := len(contents) - lineByteOffset; columnByteOffset <= lineBytesSoFar; columnByteOffset++ {
			ColumnsForNonASCII = append(ColumnsForNonASCII, column)
		}
	}

	lineOffsetTables = append(lineOffsetTables, LineOffsetTable{
		byteOffsetToStartOfLine:   int32(lineByteOffset),
		byteOffsetToFirstNonASCII: ByteOffsetToFirstNonASCII,
		columnsForNonASCII:        ColumnsForNonASCII,
	})
	return lineOffsetTables
}

type Chunk struct {
	Buffer []byte

	// This end state will be used to rewrite the start of the following source
	// map chunk so that the delta-encoded VLQ numbers are preserved.
	EndState SourceMapState

	// There probably isn't a source mapping at the end of the file (nor should
	// there be) but if we're appending another source map chunk after this one,
	// we'll need to know how many characters were in the last line we generated.
	FinalGeneratedColumn int

	ShouldIgnore bool
}

type ChunkBuilder struct {
	inputSourceMap      *SourceMap
	sourceMap           []byte
	prevLoc             logger.Loc
	prevState           SourceMapState
	lastGeneratedUpdate int
	generatedColumn     int
	hasPrevState        bool
	lineOffsetTables    []LineOffsetTable

	// This is a workaround for a bug in the popular "source-map" library:
	// https://github.com/mozilla/source-map/issues/261. The library will
	// sometimes return null when querying a source map unless every line
	// starts with a mapping at column zero.
	//
	// The workaround is to replicate the previous mapping if a line ends
	// up not starting with a mapping. This is done lazily because we want
	// to avoid replicating the previous mapping if we don't need to.
	lineStartsWithMapping     bool
	coverLinesWithoutMappings bool
}

func MakeChunkBuilder(inputSourceMap *SourceMap, lineOffsetTables []LineOffsetTable) ChunkBuilder {
	return ChunkBuilder{
		inputSourceMap:   inputSourceMap,
		prevLoc:          logger.Loc{Start: -1},
		lineOffsetTables: lineOffsetTables,

		// We automatically repeat the previous source mapping if we ever generate
		// a line that doesn't start with a mapping. This helps give files more
		// complete mapping coverage without gaps.
		//
		// However, we probably shouldn't do this if the input file has a nested
		// source map that we will be remapping through. We have no idea what state
		// that source map is in and it could be pretty scrambled.
		//
		// I've seen cases where blindly repeating the last mapping for subsequent
		// lines gives very strange and unhelpful results with source maps from
		// other tools.
		coverLinesWithoutMappings: inputSourceMap == nil,
	}
}

func (b *ChunkBuilder) AddSourceMapping(loc logger.Loc, output []byte) {
	if loc == b.prevLoc {
		return
	}
	b.prevLoc = loc

	// Binary search to find the line
	lineOffsetTables := b.lineOffsetTables
	count := len(lineOffsetTables)
	originalLine := 0
	for count > 0 {
		step := count / 2
		i := originalLine + step
		if lineOffsetTables[i].byteOffsetToStartOfLine <= loc.Start {
			originalLine = i + 1
			count = count - step - 1
		} else {
			count = step
		}
	}
	originalLine--

	// Use the line to compute the column
	line := &lineOffsetTables[originalLine]
	originalColumn := int(loc.Start - line.byteOffsetToStartOfLine)
	if line.columnsForNonASCII != nil && originalColumn >= int(line.byteOffsetToFirstNonASCII) {
		originalColumn = int(line.columnsForNonASCII[originalColumn-int(line.byteOffsetToFirstNonASCII)])
	}

	b.updateGeneratedLineAndColumn(output)

	// If this line doesn't start with a mapping and we're about to add a mapping
	// that's not at the start, insert a mapping first so the line starts with one.
	if b.coverLinesWithoutMappings && !b.lineStartsWithMapping && b.generatedColumn > 0 && b.hasPrevState {
		b.appendMappingWithoutRemapping(SourceMapState{
			GeneratedLine:   b.prevState.GeneratedLine,
			GeneratedColumn: 0,
			SourceIndex:     b.prevState.SourceIndex,
			OriginalLine:    b.prevState.OriginalLine,
			OriginalColumn:  b.prevState.OriginalColumn,
		})
	}

	b.appendMapping(SourceMapState{
		GeneratedLine:   b.prevState.GeneratedLine,
		GeneratedColumn: b.generatedColumn,
		OriginalLine:    originalLine,
		OriginalColumn:  originalColumn,
	})

	// This line now has a mapping on it, so don't insert another one
	b.lineStartsWithMapping = true
}

func (b *ChunkBuilder) GenerateChunk(output []byte) Chunk {
	b.updateGeneratedLineAndColumn(output)
	shouldIgnore := true
	for _, c := range b.sourceMap {
		if c != ';' {
			shouldIgnore = false
			break
		}
	}
	return Chunk{
		Buffer:               b.sourceMap,
		EndState:             b.prevState,
		FinalGeneratedColumn: b.generatedColumn,
		ShouldIgnore:         shouldIgnore,
	}
}

// Scan over the printed text since the last source mapping and update the
// generated line and column numbers
func (b *ChunkBuilder) updateGeneratedLineAndColumn(output []byte) {
	for i, c := range string(output[b.lastGeneratedUpdate:]) {
		switch c {
		case '\r', '\n', '\u2028', '\u2029':
			// Handle Windows-specific "\r\n" newlines
			if c == '\r' {
				newlineCheck := b.lastGeneratedUpdate + i + 1
				if newlineCheck < len(output) && output[newlineCheck] == '\n' {
					continue
				}
			}

			// If we're about to move to the next line and the previous line didn't have
			// any mappings, add a mapping at the start of the previous line.
			if b.coverLinesWithoutMappings && !b.lineStartsWithMapping && b.hasPrevState {
				b.appendMappingWithoutRemapping(SourceMapState{
					GeneratedLine:   b.prevState.GeneratedLine,
					GeneratedColumn: 0,
					SourceIndex:     b.prevState.SourceIndex,
					OriginalLine:    b.prevState.OriginalLine,
					OriginalColumn:  b.prevState.OriginalColumn,
				})
			}

			b.prevState.GeneratedLine++
			b.prevState.GeneratedColumn = 0
			b.generatedColumn = 0
			b.sourceMap = append(b.sourceMap, ';')

			// This new line doesn't have a mapping yet
			b.lineStartsWithMapping = false

		default:
			// Mozilla's "source-map" library counts columns using UTF-16 code units
			if c <= 0xFFFF {
				b.generatedColumn++
			} else {
				b.generatedColumn += 2
			}
		}
	}

	b.lastGeneratedUpdate = len(output)
}

func (b *ChunkBuilder) appendMapping(currentState SourceMapState) {
	// If the input file had a source map, map all the way back to the original
	if b.inputSourceMap != nil {
		mapping := b.inputSourceMap.Find(
			int32(currentState.OriginalLine),
			int32(currentState.OriginalColumn))

		// Some locations won't have a mapping
		if mapping == nil {
			return
		}

		currentState.SourceIndex = int(mapping.SourceIndex)
		currentState.OriginalLine = int(mapping.OriginalLine)
		currentState.OriginalColumn = int(mapping.OriginalColumn)
	}

	b.appendMappingWithoutRemapping(currentState)
}

func (b *ChunkBuilder) appendMappingWithoutRemapping(currentState SourceMapState) {
	var lastByte byte
	if len(b.sourceMap) != 0 {
		lastByte = b.sourceMap[len(b.sourceMap)-1]
	}

	b.sourceMap = appendMappingToBuffer(b.sourceMap, lastByte, b.prevState, currentState)
	b.prevState = currentState
	b.hasPrevState = true
}
