1const npa = require('npm-package-arg')
2const semver = require('semver')
3
4class OverrideSet {
5  constructor ({ overrides, key, parent }) {
6    this.parent = parent
7    this.children = new Map()
8
9    if (typeof overrides === 'string') {
10      overrides = { '.': overrides }
11    }
12
13    // change a literal empty string to * so we can use truthiness checks on
14    // the value property later
15    if (overrides['.'] === '') {
16      overrides['.'] = '*'
17    }
18
19    if (parent) {
20      const spec = npa(key)
21      if (!spec.name) {
22        throw new Error(`Override without name: ${key}`)
23      }
24
25      this.name = spec.name
26      spec.name = ''
27      this.key = key
28      this.keySpec = spec.toString()
29      this.value = overrides['.'] || this.keySpec
30    }
31
32    for (const [key, childOverrides] of Object.entries(overrides)) {
33      if (key === '.') {
34        continue
35      }
36
37      const child = new OverrideSet({
38        parent: this,
39        key,
40        overrides: childOverrides,
41      })
42
43      this.children.set(child.key, child)
44    }
45  }
46
47  getEdgeRule (edge) {
48    for (const rule of this.ruleset.values()) {
49      if (rule.name !== edge.name) {
50        continue
51      }
52
53      // if keySpec is * we found our override
54      if (rule.keySpec === '*') {
55        return rule
56      }
57
58      let spec = npa(`${edge.name}@${edge.spec}`)
59      if (spec.type === 'alias') {
60        spec = spec.subSpec
61      }
62
63      if (spec.type === 'git') {
64        if (spec.gitRange && semver.intersects(spec.gitRange, rule.keySpec)) {
65          return rule
66        }
67
68        continue
69      }
70
71      if (spec.type === 'range' || spec.type === 'version') {
72        if (semver.intersects(spec.fetchSpec, rule.keySpec)) {
73          return rule
74        }
75
76        continue
77      }
78
79      // if we got this far, the spec type is one of tag, directory or file
80      // which means we have no real way to make version comparisons, so we
81      // just accept the override
82      return rule
83    }
84
85    return this
86  }
87
88  getNodeRule (node) {
89    for (const rule of this.ruleset.values()) {
90      if (rule.name !== node.name) {
91        continue
92      }
93
94      if (semver.satisfies(node.version, rule.keySpec) ||
95        semver.satisfies(node.version, rule.value)) {
96        return rule
97      }
98    }
99
100    return this
101  }
102
103  getMatchingRule (node) {
104    for (const rule of this.ruleset.values()) {
105      if (rule.name !== node.name) {
106        continue
107      }
108
109      if (semver.satisfies(node.version, rule.keySpec) ||
110        semver.satisfies(node.version, rule.value)) {
111        return rule
112      }
113    }
114
115    return null
116  }
117
118  * ancestry () {
119    for (let ancestor = this; ancestor; ancestor = ancestor.parent) {
120      yield ancestor
121    }
122  }
123
124  get isRoot () {
125    return !this.parent
126  }
127
128  get ruleset () {
129    const ruleset = new Map()
130
131    for (const override of this.ancestry()) {
132      for (const kid of override.children.values()) {
133        if (!ruleset.has(kid.key)) {
134          ruleset.set(kid.key, kid)
135        }
136      }
137
138      if (!override.isRoot && !ruleset.has(override.key)) {
139        ruleset.set(override.key, override)
140      }
141    }
142
143    return ruleset
144  }
145}
146
147module.exports = OverrideSet
148