package js_parser

import (
	"fmt"
	"net/url"
	"sort"
	"strings"

	"github.com/evanw/esbuild/internal/ast"
	"github.com/evanw/esbuild/internal/helpers"
	"github.com/evanw/esbuild/internal/js_ast"
	"github.com/evanw/esbuild/internal/logger"
	"github.com/evanw/esbuild/internal/sourcemap"
)

// New specification: https://tc39.es/ecma426/
// Old specification: https://sourcemaps.info/spec.html
func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {
	expr, ok := ParseJSON(log, source, JSONOptions{ErrorSuffix: " in source map"})
	if !ok {
		return nil
	}

	obj, ok := expr.Data.(*js_ast.EObject)
	tracker := logger.MakeLineColumnTracker(&source)
	if !ok {
		log.AddError(&tracker, logger.Range{Loc: expr.Loc}, "Invalid source map")
		return nil
	}

	type sourceMapSection struct {
		lineOffset   int32
		columnOffset int32
		sourceMap    *js_ast.EObject
	}

	var sections []sourceMapSection
	hasSections := false

	for _, prop := range obj.Properties {
		if !helpers.UTF16EqualsString(prop.Key.Data.(*js_ast.EString).Value, "sections") {
			continue
		}

		if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
			for _, item := range value.Items {
				if element, ok := item.Data.(*js_ast.EObject); ok {
					var sectionLineOffset int32
					var sectionColumnOffset int32
					var sectionSourceMap *js_ast.EObject

					for _, sectionProp := range element.Properties {
						switch helpers.UTF16ToString(sectionProp.Key.Data.(*js_ast.EString).Value) {
						case "offset":
							if offsetValue, ok := sectionProp.ValueOrNil.Data.(*js_ast.EObject); ok {
								for _, offsetProp := range offsetValue.Properties {
									switch helpers.UTF16ToString(offsetProp.Key.Data.(*js_ast.EString).Value) {
									case "line":
										if lineValue, ok := offsetProp.ValueOrNil.Data.(*js_ast.ENumber); ok {
											sectionLineOffset = int32(lineValue.Value)
										}

									case "column":
										if columnValue, ok := offsetProp.ValueOrNil.Data.(*js_ast.ENumber); ok {
											sectionColumnOffset = int32(columnValue.Value)
										}
									}
								}
							} else {
								log.AddError(&tracker, logger.Range{Loc: sectionProp.ValueOrNil.Loc}, "Expected \"offset\" to be an object")
								return nil
							}

						case "map":
							if mapValue, ok := sectionProp.ValueOrNil.Data.(*js_ast.EObject); ok {
								sectionSourceMap = mapValue
							} else {
								log.AddError(&tracker, logger.Range{Loc: sectionProp.ValueOrNil.Loc}, "Expected \"map\" to be an object")
								return nil
							}
						}
					}

					if sectionSourceMap != nil {
						sections = append(sections, sourceMapSection{
							lineOffset:   sectionLineOffset,
							columnOffset: sectionColumnOffset,
							sourceMap:    sectionSourceMap,
						})
					}
				}
			}
		} else {
			log.AddError(&tracker, logger.Range{Loc: prop.ValueOrNil.Loc}, "Expected \"sections\" to be an array")
			return nil
		}

		hasSections = true
		break
	}

	if !hasSections {
		sections = append(sections, sourceMapSection{
			sourceMap: obj,
		})
	}

	var sources []string
	var sourcesContent []sourcemap.SourceContent
	var names []string
	var mappings mappingArray
	var generatedLine int32
	var generatedColumn int32
	needSort := false

	for _, section := range sections {
		var sourcesArray []js_ast.Expr
		var sourcesContentArray []js_ast.Expr
		var namesArray []js_ast.Expr
		var mappingsRaw []uint16
		var mappingsStart int32
		var sourceRoot string
		hasVersion := false

		for _, prop := range section.sourceMap.Properties {
			switch helpers.UTF16ToString(prop.Key.Data.(*js_ast.EString).Value) {
			case "version":
				if value, ok := prop.ValueOrNil.Data.(*js_ast.ENumber); ok && value.Value == 3 {
					hasVersion = true
				}

			case "mappings":
				if value, ok := prop.ValueOrNil.Data.(*js_ast.EString); ok {
					mappingsRaw = value.Value
					mappingsStart = prop.ValueOrNil.Loc.Start + 1
				}

			case "sourceRoot":
				if value, ok := prop.ValueOrNil.Data.(*js_ast.EString); ok {
					sourceRoot = helpers.UTF16ToString(value.Value)
				}

			case "sources":
				if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
					sourcesArray = value.Items
				}

			case "sourcesContent":
				if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
					sourcesContentArray = value.Items
				}

			case "names":
				if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
					namesArray = value.Items
				}
			}
		}

		// Silently ignore the section if the version was missing or incorrect
		if !hasVersion {
			continue
		}

		mappingsLen := len(mappingsRaw)
		sourcesLen := len(sourcesArray)
		namesLen := len(namesArray)

		// Silently ignore the section if the source map is pointless (i.e. empty)
		if mappingsLen == 0 || sourcesLen == 0 {
			continue
		}

		if section.lineOffset < generatedLine || (section.lineOffset == generatedLine && section.columnOffset < generatedColumn) {
			needSort = true
		}

		lineOffset := section.lineOffset
		columnOffset := section.columnOffset
		sourceOffset := int32(len(sources))
		nameOffset := int32(len(names))

		generatedLine = lineOffset
		generatedColumn = columnOffset
		sourceIndex := sourceOffset
		var originalLine int32
		var originalColumn int32
		originalName := nameOffset

		current := 0
		errorText := ""
		errorLen := 0

		// Parse the mappings
		for current < mappingsLen {
			// Handle a line break
			if mappingsRaw[current] == ';' {
				generatedLine++
				generatedColumn = 0
				current++
				continue
			}

			// Read the generated column
			generatedColumnDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
			if !ok {
				errorText = "Missing generated column"
				errorLen = i
				break
			}
			if generatedColumnDelta < 0 {
				// This would mess up binary search
				needSort = true
			}
			generatedColumn += generatedColumnDelta
			if (generatedLine == lineOffset && generatedColumn < columnOffset) || generatedColumn < 0 {
				errorText = fmt.Sprintf("Invalid generated column value: %d", generatedColumn)
				errorLen = i
				break
			}
			current += i

			// According to the specification, it's valid for a mapping to have 1,
			// 4, or 5 variable-length fields. Having one field means there's no
			// original location information, which is pretty useless. Just ignore
			// those entries.
			if current == mappingsLen {
				break
			}
			switch mappingsRaw[current] {
			case ',':
				current++
				continue
			case ';':
				continue
			}

			// Read the original source
			sourceIndexDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
			if !ok {
				errorText = "Missing source index"
				errorLen = i
				break
			}
			sourceIndex += sourceIndexDelta
			if sourceIndex < sourceOffset || sourceIndex >= sourceOffset+int32(sourcesLen) {
				errorText = fmt.Sprintf("Invalid source index value: %d", sourceIndex)
				errorLen = i
				break
			}
			current += i

			// Read the original line
			originalLineDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
			if !ok {
				errorText = "Missing original line"
				errorLen = i
				break
			}
			originalLine += originalLineDelta
			if originalLine < 0 {
				errorText = fmt.Sprintf("Invalid original line value: %d", originalLine)
				errorLen = i
				break
			}
			current += i

			// Read the original column
			originalColumnDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
			if !ok {
				errorText = "Missing original column"
				errorLen = i
				break
			}
			originalColumn += originalColumnDelta
			if originalColumn < 0 {
				errorText = fmt.Sprintf("Invalid original column value: %d", originalColumn)
				errorLen = i
				break
			}
			current += i

			// Read the original name
			var optionalName ast.Index32
			if originalNameDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:]); ok {
				originalName += originalNameDelta
				if originalName < nameOffset || originalName >= nameOffset+int32(namesLen) {
					errorText = fmt.Sprintf("Invalid name index value: %d", originalName)
					errorLen = i
					break
				}
				optionalName = ast.MakeIndex32(uint32(originalName))
				current += i
			}

			// Handle the next character
			if current < mappingsLen {
				if c := mappingsRaw[current]; c == ',' {
					current++
				} else if c != ';' {
					errorText = fmt.Sprintf("Invalid character after mapping: %q",
						helpers.UTF16ToString(mappingsRaw[current:current+1]))
					errorLen = 1
					break
				}
			}

			mappings = append(mappings, sourcemap.Mapping{
				GeneratedLine:   generatedLine,
				GeneratedColumn: generatedColumn,
				SourceIndex:     sourceIndex,
				OriginalLine:    originalLine,
				OriginalColumn:  originalColumn,
				OriginalName:    optionalName,
			})
		}

		if errorText != "" {
			r := logger.Range{Loc: logger.Loc{Start: mappingsStart + int32(current)}, Len: int32(errorLen)}
			log.AddID(logger.MsgID_SourceMap_InvalidSourceMappings, logger.Warning, &tracker, r,
				fmt.Sprintf("Bad \"mappings\" data in source map at character %d: %s", current, errorText))
			return nil
		}

		// Try resolving relative source URLs into absolute source URLs.
		// See https://tc39.es/ecma426/#resolving-sources for details.
		var sourceURLPrefix string
		var baseURL *url.URL
		if sourceRoot != "" {
			if index := strings.LastIndexByte(sourceRoot, '/'); index != -1 {
				sourceURLPrefix = sourceRoot[:index+1]
			} else {
				sourceURLPrefix = sourceRoot + "/"
			}
		}
		if source.KeyPath.Namespace == "file" {
			baseURL = helpers.FileURLFromFilePath(source.KeyPath.Text)
		}

		for _, item := range sourcesArray {
			if element, ok := item.Data.(*js_ast.EString); ok {
				sourcePath := sourceURLPrefix + helpers.UTF16ToString(element.Value)
				sourceURL, err := url.Parse(sourcePath)

				// Ignore URL parse errors (such as "%XY" being an invalid escape)
				if err != nil {
					sources = append(sources, sourcePath)
					continue
				}

				// Resolve this URL relative to the enclosing directory
				if baseURL != nil {
					sourceURL = baseURL.ResolveReference(sourceURL)
				}
				sources = append(sources, sourceURL.String())
			} else {
				sources = append(sources, "")
			}
		}

		if len(sourcesContentArray) > 0 {
			// It's possible that one of the source maps inside "sections" has
			// different lengths for the "sources" and "sourcesContent" arrays.
			// This is bad because we need to us a single index to get the name
			// of the source from "sources[i]" and the content of the source
			// from "sourcesContent[i]".
			//
			// So if a previous source map had a shorter "sourcesContent" array
			// than its "sources" array (or if the previous source map just had
			// no "sourcesContent" array), expand our aggregated array to the
			// right length by padding it out with empty entries.
			sourcesContent = append(sourcesContent, make([]sourcemap.SourceContent, int(sourceOffset)-len(sourcesContent))...)

			for i, item := range sourcesContentArray {
				// Make sure we don't ever record more "sourcesContent" entries
				// than there are "sources" entries, which is possible because
				// these are two separate arrays in the source map JSON. We need
				// to avoid this because that would mess up our shared indexing
				// of the "sources" and "sourcesContent" arrays. See the above
				// comment for more details.
				if i == sourcesLen {
					break
				}

				if element, ok := item.Data.(*js_ast.EString); ok {
					sourcesContent = append(sourcesContent, sourcemap.SourceContent{
						Value:  element.Value,
						Quoted: source.TextForRange(source.RangeOfString(item.Loc)),
					})
				} else {
					sourcesContent = append(sourcesContent, sourcemap.SourceContent{})
				}
			}
		}

		for _, item := range namesArray {
			if element, ok := item.Data.(*js_ast.EString); ok {
				names = append(names, helpers.UTF16ToString(element.Value))
			} else {
				names = append(names, "")
			}
		}
	}

	// Silently fail if the source map is pointless (i.e. empty)
	if len(sources) == 0 || len(mappings) == 0 {
		return nil
	}

	if needSort {
		// If we get here, some mappings are out of order. Lines can't be out of
		// order by construction but columns can. This is a pretty rare situation
		// because almost all source map generators always write out mappings in
		// order as they write the output instead of scrambling the order.
		sort.Stable(mappings)
	}

	return &sourcemap.SourceMap{
		Sources:        sources,
		SourcesContent: sourcesContent,
		Mappings:       mappings,
		Names:          names,
	}
}

// This type is just so we can use Go's native sort function
type mappingArray []sourcemap.Mapping

func (a mappingArray) Len() int          { return len(a) }
func (a mappingArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }

func (a mappingArray) Less(i int, j int) bool {
	ai := a[i]
	aj := a[j]
	return ai.GeneratedLine < aj.GeneratedLine || (ai.GeneratedLine == aj.GeneratedLine && ai.GeneratedColumn <= aj.GeneratedColumn)
}
