package parse

import (
	"go/ast"
	"go/parser"
	"go/token"
	"sort"
	"strings"
)

const C = "\"C\""

type GciImports struct {
	// original index of import group, include doc, name, path and comment
	Start, End int
	Name, Path string
}
type ImportList []*GciImports

func (l ImportList) Len() int {
	return len(l)
}

func (l ImportList) Less(i, j int) bool {
	if strings.Compare(l[i].Path, l[j].Path) == 0 {
		return strings.Compare(l[i].Name, l[j].Name) < 0
	}

	return strings.Compare(l[i].Path, l[j].Path) < 0
}

func (l ImportList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }

/*
 * AST considers a import block as below:
 * ```
 * Doc
 * Name Path Comment
 * ```
 * An example is like below:
 * ```
 * // test
 * test "fmt" // test
 * ```
 * getImports return a import block with name, start and end index
 */
func getImports(imp *ast.ImportSpec) (start, end int, name string) {
	if imp.Doc != nil {
		// doc poc need minus one to get the first index of comment
		start = int(imp.Doc.Pos()) - 1
	} else {
		if imp.Name != nil {
			// name pos need minus one too
			start = int(imp.Name.Pos()) - 1
		} else {
			// path pos start without quote, need minus one for it
			start = int(imp.Path.Pos()) - 1
		}
	}

	if imp.Name != nil {
		name = imp.Name.Name
	}

	if imp.Comment != nil {
		end = int(imp.Comment.End())
	} else {
		end = int(imp.Path.End())
	}
	return
}

func ParseFile(src []byte, filename string) (ImportList, int, int, int, int, error) {
	fileSet := token.NewFileSet()
	f, err := parser.ParseFile(fileSet, filename, src, parser.ParseComments)
	if err != nil {
		return nil, 0, 0, 0, 0, err
	}

	if len(f.Imports) == 0 {
		return nil, 0, 0, 0, 0, NoImportError{}
	}

	var (
		// headEnd means the start of import block
		headEnd int
		// tailStart means the end + 1 of import block
		tailStart int
		// cStart means the start of C import block
		cStart int
		// cEnd means the end of C import block
		cEnd int
		data ImportList
	)

	for index, decl := range f.Decls {
		switch decl.(type) {
		// skip BadDecl and FuncDecl
		case *ast.GenDecl:
			genDecl := decl.(*ast.GenDecl)

			if genDecl.Tok == token.IMPORT {
				// there are two cases, both end with linebreak:
				// 1.
				// import (
				//	 "xxxx"
				// )
				// 2.
				// import "xxx"
				if headEnd == 0 {
					headEnd = int(decl.Pos()) - 1
				}
				tailStart = int(decl.End())

				for _, spec := range genDecl.Specs {
					imp := spec.(*ast.ImportSpec)
					// there are only one C import block
					// ensure C import block is the first import block
					if imp.Path.Value == C {
						/*
							common case:

							// #include <png.h>
							import "C"

							notice that decl.Pos() == genDecl.Pos() > genDecl.Doc.Pos()
						*/
						if genDecl.Doc != nil {
							cStart = int(genDecl.Doc.Pos()) - 1
							// if C import block is the first, update headEnd
							if index == 0 {
								headEnd = cStart
							}
						} else {
							/*
								special case:

								import "C"
							*/
							cStart = int(decl.Pos()) - 1
						}

						cEnd = int(decl.End())

						continue
					}

					start, end, name := getImports(imp)

					data = append(data, &GciImports{
						Start: start,
						End:   end,
						Name:  name,
						Path:  strings.Trim(imp.Path.Value, `"`),
					})
				}
			}
		}
	}

	sort.Sort(data)
	return data, headEnd, tailStart, cStart, cEnd, nil
}

// IsGeneratedFileByComment reports whether the source file is generated code.
// Using a bit laxer rules than https://golang.org/s/generatedcode to
// match more generated code.
// Taken from https://github.com/golangci/golangci-lint.
func IsGeneratedFileByComment(in string) bool {
	const (
		genCodeGenerated = "code generated"
		genDoNotEdit     = "do not edit"
		genAutoFile      = "autogenerated file"      // easyjson
		genAutoGenerated = "automatically generated" // genny
	)

	markers := []string{genCodeGenerated, genDoNotEdit, genAutoFile, genAutoGenerated}
	in = strings.ToLower(in)
	for _, marker := range markers {
		if strings.Contains(in, marker) {
			return true
		}
	}

	return false
}

type NoImportError struct{}

func (n NoImportError) Error() string {
	return "No imports"
}

func (i NoImportError) Is(err error) bool {
	_, ok := err.(NoImportError)
	return ok
}
