package main

// Much of the core of this is copied from go's cover tool itself.

// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// The rest is written by Dustin Sallings

import (
	"fmt"
	"go/build"
	"io/ioutil"
	"os"
	"path/filepath"
	"strings"

	"golang.org/x/mod/modfile"
	"golang.org/x/tools/cover"
)

func findFile(rootPackage string, rootDir string, file string) (string, error) {
	// If we find a file that is inside the root package, we already know
	// where it should be!
	if rootPackage != "" {
		if relPath, _ := filepath.Rel(rootPackage, file); !strings.HasPrefix(relPath, "..") {
			// The file is inside the root package...
			return filepath.Join(rootDir, relPath), nil
		}
	}

	dir, file := filepath.Split(file)
	pkg, err := build.Import(dir, ".", build.FindOnly)
	if err != nil {
		return "", fmt.Errorf("can't find %q: %v", file, err)
	}
	return filepath.Join(pkg.Dir, file), nil
}

// mergeProfs merges profiles for same target packages.
// It assumes each profiles have same sorted FileName and Blocks.
func mergeProfs(pfss [][]*cover.Profile) []*cover.Profile {
	if len(pfss) == 0 {
		return nil
	}
	for len(pfss) > 1 {
		i := 0
		for ; 2*i+1 < len(pfss); i++ {
			pfss[i] = mergeTwoProfs(pfss[2*i], pfss[2*i+1])
		}
		if 2*i < len(pfss) {
			pfss[i] = pfss[2*i]
			i++
		}
		pfss = pfss[:i]
	}
	return pfss[0]
}

func mergeTwoProfs(left, right []*cover.Profile) []*cover.Profile {
	ret := make([]*cover.Profile, 0, len(left)+len(right))
	for len(left) > 0 && len(right) > 0 {
		if left[0].FileName == right[0].FileName {
			profile := &cover.Profile{
				FileName: left[0].FileName,
				Mode:     left[0].Mode,
				Blocks:   mergeTwoProfBlock(left[0].Blocks, right[0].Blocks),
			}
			ret = append(ret, profile)
			left = left[1:]
			right = right[1:]
		} else if left[0].FileName < right[0].FileName {
			ret = append(ret, left[0])
			left = left[1:]
		} else {
			ret = append(ret, right[0])
			right = right[1:]
		}
	}
	ret = append(ret, left...)
	ret = append(ret, right...)
	return ret
}

func mergeTwoProfBlock(left, right []cover.ProfileBlock) []cover.ProfileBlock {
	ret := make([]cover.ProfileBlock, 0, len(left)+len(right))
	for len(left) > 0 && len(right) > 0 {
		a, b := left[0], right[0]
		if a.StartLine == b.StartLine && a.StartCol == b.StartCol && a.EndLine == b.EndLine && a.EndCol == b.EndCol {
			ret = append(ret, cover.ProfileBlock{
				StartLine: a.StartLine,
				StartCol:  a.StartCol,
				EndLine:   a.EndLine,
				EndCol:    a.EndCol,
				NumStmt:   a.NumStmt,
				Count:     a.Count + b.Count,
			})
			left = left[1:]
			right = right[1:]
		} else if a.StartLine < b.StartLine || (a.StartLine == b.StartLine && a.StartCol < b.StartCol) {
			ret = append(ret, a)
			left = left[1:]
		} else {
			ret = append(ret, b)
			right = right[1:]
		}
	}
	ret = append(ret, left...)
	ret = append(ret, right...)
	return ret
}

// toSF converts profiles to sourcefiles for coveralls.
func toSF(profs []*cover.Profile) ([]*SourceFile, error) {
	// find root package to reduce build.Import calls when importing files from relative root
	// https://github.com/mattn/goveralls/pull/195
	rootDirectory, err := os.Getwd()
	if err != nil {
		return nil, fmt.Errorf("get working dir: %v", err)
	}
	rootPackage := findRootPackage(rootDirectory)

	var rv []*SourceFile
	for _, prof := range profs {
		path, err := findFile(rootPackage, rootDirectory, prof.FileName)
		if err != nil {
			return nil, fmt.Errorf("cannot find file %q: %v", prof.FileName, err)
		}
		sf := &SourceFile{
			Name: getCoverallsSourceFileName(path),
		}
		lineLookup := map[int]int{}
		maxLineNo := 0
		for _, block := range prof.Blocks {
			for i := block.StartLine; i <= block.EndLine; i++ {
				lineLookup[i] += block.Count
			}
			if block.EndLine > maxLineNo {
				maxLineNo = block.EndLine
			}
		}
		sf.Coverage = make([]interface{}, maxLineNo)
		for i := 1; i <= maxLineNo; i++ {
			if c, ok := lineLookup[i]; ok {
				sf.Coverage[i-1] = c
			}
		}
		if *uploadSource {
			fb, err := ioutil.ReadFile(path)
			if err != nil {
				return nil, fmt.Errorf("cannot read source of file %q: %v", path, err)
			}
			sf.Source = string(fb)
		}

		rv = append(rv, sf)
	}

	return rv, nil
}

func parseCover(fn string) ([]*SourceFile, error) {
	var pfss [][]*cover.Profile
	for _, p := range strings.Split(fn, ",") {
		profs, err := cover.ParseProfiles(p)
		if err != nil {
			return nil, fmt.Errorf("error parsing coverage: %v", err)
		}
		pfss = append(pfss, profs)
	}

	sourceFiles, err := toSF(mergeProfs(pfss))
	if err != nil {
		return nil, err
	}

	return sourceFiles, nil
}

func findRootPackage(rootDirectory string) string {
	modPath := filepath.Join(rootDirectory, "go.mod")
	content, err := ioutil.ReadFile(modPath)
	if err != nil {
		return ""
	}
	return modfile.ModulePath(content)
}
