package sourcemap

import (
	"encoding/json"
	"fmt"
	"net/url"
	"path"
	"sort"
	"strconv"
)

type Consumer struct {
	sourceRootURL *url.URL
	smap          *sourceMap
	mappings      []mapping
}

func Parse(mapURL string, b []byte) (*Consumer, error) {
	smap := new(sourceMap)
	err := json.Unmarshal(b, smap)
	if err != nil {
		return nil, err
	}

	if smap.Version != 3 {
		return nil, fmt.Errorf(
			"sourcemap: got version=%d, but only 3rd version is supported",
			smap.Version,
		)
	}

	var sourceRootURL *url.URL
	if smap.SourceRoot != "" {
		u, err := url.Parse(smap.SourceRoot)
		if err != nil {
			return nil, err
		}
		if u.IsAbs() {
			sourceRootURL = u
		}
	} else if mapURL != "" {
		u, err := url.Parse(mapURL)
		if err != nil {
			return nil, err
		}
		if u.IsAbs() {
			u.Path = path.Dir(u.Path)
			sourceRootURL = u
		}
	}

	mappings, err := parseMappings(smap.Mappings)
	if err != nil {
		return nil, err
	}
	// Free memory.
	smap.Mappings = ""

	return &Consumer{
		sourceRootURL: sourceRootURL,
		smap:          smap,
		mappings:      mappings,
	}, nil
}

func (c *Consumer) File() string {
	return c.smap.File
}

func (c *Consumer) Source(genLine, genCol int) (source, name string, line, col int, ok bool) {
	i := sort.Search(len(c.mappings), func(i int) bool {
		m := &c.mappings[i]
		if m.genLine == genLine {
			return m.genCol >= genCol
		}
		return m.genLine >= genLine
	})

	// Mapping not found.
	if i == len(c.mappings) {
		return
	}

	match := &c.mappings[i]

	// Fuzzy match.
	if match.genLine > genLine || match.genCol > genCol {
		if i == 0 {
			return
		}
		match = &c.mappings[i-1]
	}

	if match.sourcesInd >= 0 {
		source = c.absSource(c.smap.Sources[match.sourcesInd])
	}
	if match.namesInd >= 0 {
		v := c.smap.Names[match.namesInd]
		switch v := v.(type) {
		case string:
			name = v
		case float64:
			name = strconv.FormatFloat(v, 'f', -1, 64)
		default:
			name = fmt.Sprint(v)
		}
	}
	line = match.sourceLine
	col = match.sourceCol
	ok = true
	return
}

func (c *Consumer) absSource(source string) string {
	if path.IsAbs(source) {
		return source
	}

	if u, err := url.Parse(source); err == nil && u.IsAbs() {
		return source
	}

	if c.sourceRootURL != nil {
		u := *c.sourceRootURL
		u.Path = path.Join(c.sourceRootURL.Path, source)
		return u.String()
	}

	if c.smap.SourceRoot != "" {
		return path.Join(c.smap.SourceRoot, source)
	}

	return source
}