1package mkcompare
2
3import (
4	"fmt"
5	"io"
6	"regexp"
7	"sort"
8	"strings"
9)
10
11// Classify takes two maps with string keys and return the lists of left-only, common, and right-only keys
12func Classify[V interface{}](mLeft map[string]V, mRight map[string]V, varFilter func(_ string) bool) (left []string, common []string, right []string) {
13	for k := range mLeft {
14		if !varFilter(k) {
15			break
16		}
17		if _, ok := mRight[k]; ok {
18			common = append(common, k)
19		} else {
20			left = append(left, k)
21		}
22	}
23	for k := range mRight {
24		if !varFilter(k) {
25			break
26		}
27		if _, ok := mLeft[k]; !ok {
28			right = append(right, k)
29		}
30	}
31
32	return left, common, right
33}
34
35var normalizer = map[string]func(ref, our string) (string, string){
36	"LOCAL_SOONG_INSTALL_PAIRS":         normalizeInstallPairs,
37	"LOCAL_COMPATIBILITY_SUPPORT_FILES": normalizeInstallPairs,
38	"LOCAL_PREBUILT_MODULE_FILE":        normalizePrebuiltModuleFile,
39	"LOCAL_SOONG_CLASSES_JAR":           normalizePrebuiltModuleFile,
40	"LOCAL_SOONG_HEADER_JAR":            normalizePrebuiltModuleFile,
41}
42
43func normalizePrebuiltModuleFile(ref string, our string) (string, string) {
44	return strings.ReplaceAll(ref, "/bazelCombined/", "/combined/"), strings.ReplaceAll(our, "/bazelCombined/", "/combined/")
45}
46
47var rexRemoveInstallSource = regexp.MustCompile("([^ ]+:)")
48
49func normalizeInstallPairs(ref string, our string) (string, string) {
50	return rexRemoveInstallSource.ReplaceAllString(ref, ""), rexRemoveInstallSource.ReplaceAllString(our, "")
51}
52
53type MkVarDiff struct {
54	Name         string
55	MissingItems []string `json:",omitempty"`
56	ExtraItems   []string `json:",omitempty"`
57}
58
59// MkModuleDiff holds module difference between reference and our mkfile.
60type MkModuleDiff struct {
61	Ref          *MkModule   `json:"-"`
62	Our          *MkModule   `json:"-"`
63	MissingVars  []string    `json:",omitempty"`
64	ExtraVars    []string    `json:",omitempty"`
65	DiffVars     []MkVarDiff `json:",omitempty"`
66	TypeDiffers  bool        `json:",omitempty"`
67	ExtrasDiffer bool        `json:",omitempty"`
68}
69
70// Empty returns true if there is no difference
71func (d *MkModuleDiff) Empty() bool {
72	return !d.TypeDiffers && !d.ExtrasDiffer && len(d.MissingVars) == 0 && len(d.ExtraVars) == 0 && len(d.DiffVars) == 0
73}
74
75// Print prints the difference
76func (d *MkModuleDiff) Print(sink io.Writer, name string) {
77	if d.Empty() {
78		return
79	}
80	fmt.Fprintf(sink, "%s (ref line %d, our line %d):\n", name, d.Ref.Location, d.Our.Location)
81	if d.TypeDiffers {
82		fmt.Fprintf(sink, "  type %s <-> %s\n", d.Ref.Type, d.Our.Type)
83	}
84
85	if !d.ExtrasDiffer {
86		fmt.Fprintf(sink, "  extras %d <-> %d\n", d.Ref.Extras, d.Our.Extras)
87	}
88
89	if len(d.MissingVars)+len(d.DiffVars) > 0 {
90		fmt.Fprintf(sink, "  variables:\n")
91		if len(d.MissingVars) > 0 {
92			fmt.Fprintf(sink, "    -%v\n", d.MissingVars)
93		}
94		if len(d.ExtraVars) > 0 {
95			fmt.Fprintf(sink, "    +%v\n", d.ExtraVars)
96		}
97	}
98	for _, vdiff := range d.DiffVars {
99		fmt.Printf("   %s value:\n", vdiff.Name)
100		if len(vdiff.MissingItems) > 0 {
101			fmt.Printf("    -%v\n", vdiff.MissingItems)
102		}
103		if len(vdiff.ExtraItems) > 0 {
104			fmt.Printf("    +%v\n", vdiff.ExtraItems)
105		}
106	}
107}
108
109// Compare returns the difference for a module. Only the variables filtered by the given
110// function are considered.
111func Compare(refMod *MkModule, ourMod *MkModule, varFilter func(string) bool) MkModuleDiff {
112	d := MkModuleDiff{
113		Ref:          refMod,
114		Our:          ourMod,
115		TypeDiffers:  refMod.Type != ourMod.Type,
116		ExtrasDiffer: refMod.Extras != ourMod.Extras,
117	}
118	var common []string
119	d.MissingVars, common, d.ExtraVars = Classify(d.Ref.Variables, d.Our.Variables, varFilter)
120
121	if len(common) > 0 {
122		for _, v := range common {
123			doSort := true // TODO(asmundak): find if for some variables the value should not be sorted
124			refValue := d.Ref.Variables[v]
125			ourValue := d.Our.Variables[v]
126			if f, ok := normalizer[v]; ok {
127				refValue, ourValue = f(refValue, ourValue)
128			}
129			missingItems, extraItems := compareVariableValues(refValue, ourValue, doSort)
130			if len(missingItems)+len(extraItems) > 0 {
131				d.DiffVars = append(d.DiffVars, MkVarDiff{
132					Name:         v,
133					MissingItems: missingItems,
134					ExtraItems:   extraItems,
135				})
136			}
137		}
138	}
139	return d
140}
141
142func compareVariableValues(ref string, our string, sortItems bool) ([]string, []string) {
143	refTokens := strings.Split(ref, " ")
144	ourTokens := strings.Split(our, " ")
145	if sortItems {
146		sort.Strings(refTokens)
147		sort.Strings(ourTokens)
148	}
149	var missing []string
150	var extra []string
151	refStream := &tokenStream{refTokens, 0}
152	ourStream := &tokenStream{ourTokens, 0}
153	refToken := refStream.next()
154	ourToken := ourStream.next()
155	compare := 0
156	for refToken != tsEOF || ourToken != tsEOF {
157		if refToken == tsEOF {
158			compare = 1
159		} else if ourToken == tsEOF {
160			compare = -1
161		} else {
162			compare = 0
163			if refToken <= ourToken {
164				compare = -1
165			}
166			if refToken >= ourToken {
167				compare = compare + 1
168			}
169		}
170		switch compare {
171		case -1:
172			missing = append(missing, refToken)
173			refToken = refStream.next()
174		case 0:
175			refToken = refStream.next()
176			ourToken = ourStream.next()
177		case 1:
178			extra = append(extra, ourToken)
179			ourToken = ourStream.next()
180		}
181	}
182	return missing, extra
183}
184
185// Auxiliary stuff used to find the difference
186const tsEOF = " "
187
188type tokenStream struct {
189	tokens  []string
190	current int
191}
192
193func (ts *tokenStream) next() string {
194	if ts.current >= len(ts.tokens) {
195		return tsEOF
196	}
197	ret := ts.tokens[ts.current]
198	ts.current = ts.current + 1
199	return ret
200}
201