// This file is part of the adequate Debian-native package, and is available
// under the Expat license. For the full terms please see debian/copyright.

package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"regexp"
	"strings"
)

type licenseType int
type licenseID int

const (
	unknownLicense licenseType = iota
	agplV3
	agplV3Plus
	eitherLGPLv3PlusOrGPLv2Plus
	gplV2
	gplV2Plus
	gplV3
	gplV3Plus
	lgplV2point1
	lgplV2point1Plus
	lgplV3
	lgplV3Plus
	openSSL
)

func newLicense(s string) licenseType {
	// Strip minor version if it's zero. This doesn't work for or'ed
	// versions (see below).
	var ok bool
	if s, ok = strings.CutSuffix(s, ".0"); !ok {
		if strings.HasSuffix(s, ".0+") {
			s = s[:len(s)-3] + "+"
		}
	}

	switch s {
	case "GPLv2", "GPL-2":
		return gplV2
	case "GPLv3", "GPL-3":
		return gplV3
	case "AGPLv3":
		return agplV3
	case "GPLv2+", "GPL-2+":
		return gplV2Plus
	case "GPLv3+", "GPL-3+":
		return gplV3Plus
	case "AGPLv3+", "AGPL-3+":
		return agplV3Plus
	case "LGPLv2.1", "LGPL-2.1":
		return lgplV2point1
	case "LGPLv3", "LGPL-3":
		return lgplV3
	case "LGPLv2.1+", "LGPL-2.1+":
		return lgplV2point1Plus
	case "LGPLv3+", "LGPL-3+":
		return lgplV3Plus
	// Need to enumerate or'ed licenses with and without minor zero.
	case "LGPLv3+ | GPLv2+", "LGPLv3.0+ | GPLv2.0+", "LGPLv3+ | GPLv2.0+", "LGPLv3.0+ | GPLv2+":
		fallthrough
	case "LGPL-3+ | GPL-2+", "LGPL-3.0+ | GPL-2.0+", "LGPL-3+ | GPL-2.0+", "LGPL-3.0+ | GPL-2+":
		return eitherLGPLv3PlusOrGPLv2Plus
	case "OpenSSL":
		return openSSL
	}
	// TODO: many of these are deprecated (still valid but a newer name is
	// preferred); incorporate newer versions from
	// https://spdx.org/licenses; die once we incorporate a more thorough
	// list.
	return unknownLicense
}

func (li licenseType) String() string {
	switch li {
	case gplV2:
		return "GPLv2"
	case gplV3:
		return "GPLv3"
	case agplV3:
		return "AGPLv3"
	case gplV2Plus:
		return "GPLv2+"
	case gplV3Plus:
		return "GPLv3+"
	case agplV3Plus:
		return "AGPLv3+"
	case lgplV2point1:
		return "LGPLv2.1"
	case lgplV3:
		return "LGPLv3"
	case lgplV2point1Plus:
		return "LGPLv2.1+"
	case lgplV3Plus:
		return "LGPLv3+"
	case eitherLGPLv3PlusOrGPLv2Plus:
		return "LGPLv3+ | GPLv2+"
	case openSSL:
		return "OpenSSL"
	}
	return "unknown license"
}

func (li licenseType) id() licenseID {
	var id licenseID
	switch li {
	case gplV2:
		id = 0x04
	case gplV3:
		id = 0x08
	case agplV3:
		id = 0x08
	case gplV2Plus:
		id = 0x0c
	case gplV3Plus:
		id = 0x08
	case agplV3Plus:
		id = 0x08
	case lgplV2point1:
		id = 0x14c
	case lgplV3:
		id = 0x188
	case lgplV2point1Plus:
		id = 0x1cc
	case lgplV3Plus:
		id = 0x188
	case eitherLGPLv3PlusOrGPLv2Plus:
		id = 0x18c
	case openSSL:
		id = 0x100
	default:
		log.Fatalf("Invalid license %q", li)
	}
	return licenseID(id)
}

func (li licenseType) undefined() bool {
	return li == unknownLicense
}

// TODO: update versions
var soname2license = map[string]licenseType{
	"libcrypto.so.0.9.8":      openSSL,
	"libcrypto.so.1.0.0":      openSSL,
	"libgmp.so.10":            eitherLGPLv3PlusOrGPLv2Plus, // 'LGPLv3+ | GPLv2+'
	"libgnutls-extra.so.26":   gplV3Plus,
	"libgnutls-openssl.so.27": gplV3Plus,
	// FIXME: libgs9 and libjbig2dec in jessie are gplV2Plus
	"libgs.so.9":       agplV3Plus,
	"libjbig2dec.so.0": agplV3Plus,
	"libltdl.so.7":     gplV2Plus,
	"libpoppler.so.19": gplV2,
	"libpoppler.so.28": gplV2,
	"libpoppler.so.37": gplV2,
	"libpoppler.so.43": gplV2,
	"libpoppler.so.44": gplV2,
	"libpoppler.so.46": gplV2,
	"libpoppler.so.47": gplV2,
	"libpoppler.so.5":  gplV2,
	"libpoppler.so.57": gplV2,
	"libpoppler.so.60": gplV2,
	"libpoppler.so.61": gplV2,
	"libreadline.so.5": gplV2Plus,
	"libreadline.so.6": gplV3Plus,
	"libreadline.so.7": gplV3Plus,
	"libssl.so.0.9.8":  openSSL,
	"libssl.so.1.0.0":  openSSL,
	"libssl.so.1.0.2":  openSSL,
	"libssl.so.1.1":    openSSL,
}

func loadPkgLicenses(pkgs []string) map[string]licenseType {
	pkg2license := make(map[string]licenseType)
	emptyLineRE := regexp.MustCompile("^[\t ]*$")
	formatLineRE := regexp.MustCompile("(?i:^Format:[\t ]+(" + nonSpaceToken + ")[\t ]*)$")
	licenseLineRE := regexp.MustCompile("(?i:^License:[\t ]+(" + nonSpaceToken + ")[\t ]*)$")
	for _, pkg := range pkgs {
		path, ok := copyrightFileExists(pkg)
		if !ok {
			continue
		}
		buf, err := os.ReadFile(path)
		if err != nil {
			log.Fatalf("Failed to access %q: %v", path, err)
		}

		firstPara := true
		machineReadable := false
		var pkgLicenses []licenseType
	licenseLoop:
		for {
			advance, line, err := bufio.ScanLines(buf, true)
			switch {
			case err != nil:
				log.Fatalf("Failed to scan %q: %v\n", path, err)
			case advance == 0:
				break licenseLoop // we got to EoF
			case advance <= len(buf):
				buf = buf[advance:]
			}

			switch {
			case emptyLineRE.Match(line):
				firstPara = false
			case firstPara:
				if m := formatLineRE.FindSubmatch(line); m != nil {
					machineReadable = isSupportedCopyrightFormat(string(m[1]))
				}
			default:
				if !machineReadable {
					continue
				}
				m := licenseLineRE.FindSubmatch(line)
				if len(m) != 2 {
					continue
				}
				lic := newLicense(string(m[1]))
				if lic != unknownLicense {
					pkgLicenses = append(pkgLicenses, lic)
				}
			}
		}
		// The perl version of adequate only keeps dep5 data only if a
		// package has exactly one license. In theory we could try
		// harder and store licenses at {package, file glob}
		// granularity.
		if len(pkgLicenses) == 1 {
			pkg2license[pkg] = pkgLicenses[0]
		}
	}

	return pkg2license
}

type soNameLicense struct {
	soName  string
	license licenseType
}

func (s soNameLicense) String() string {
	if s.soName == "" {
		return s.license.String()
	}
	return fmt.Sprintf("%s (%s)", s.license.String(), s.soName)
}

func isSupportedCopyrightFormat(s string) bool {
	ss := strings.TrimRight(s, "/")
	return (ss == "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0" ||
		ss == "http://www.debian.org/doc/packaging-manuals/copyright-format/1.0")
}
