1let path = require('path');
2let fs = require('fs');
3let Task = require('./task/task').Task;
4
5// Split a task to two parts, name space and task name.
6// For example, given 'foo:bin/a%.c', return an object with
7// - 'ns'     : foo
8// - 'name'   : bin/a%.c
9function splitNs(task) {
10  let parts = task.split(':');
11  let name = parts.pop();
12  let ns = resolveNs(parts);
13  return {
14    'name' : name,
15    'ns'   : ns
16  };
17}
18
19// Return the namespace based on an array of names.
20// For example, given ['foo', 'baz' ], return the namespace
21//
22//   default -> foo -> baz
23//
24// where default is the global root namespace
25// and -> means child namespace.
26function resolveNs(parts) {
27  let  ns = jake.defaultNamespace;
28  for(let i = 0, l = parts.length; ns && i < l; i++) {
29    ns = ns.childNamespaces[parts[i]];
30  }
31  return ns;
32}
33
34// Given a pattern p, say 'foo:bin/a%.c'
35// Return an object with
36// - 'ns'     : foo
37// - 'dir'    : bin
38// - 'prefix' : a
39// - 'suffix' : .c
40function resolve(p) {
41  let task = splitNs(p);
42  let name  = task.name;
43  let ns    = task.ns;
44  let split = path.basename(name).split('%');
45  return {
46    ns: ns,
47    dir: path.dirname(name),
48    prefix: split[0],
49    suffix: split[1]
50  };
51}
52
53// Test whether string a is a suffix of string b
54function stringEndWith(a, b) {
55  let l;
56  return (l = b.lastIndexOf(a)) == -1 ? false : l + a.length == b.length;
57}
58
59// Replace the suffix a of the string s with b.
60// Note that, it is assumed a is a suffix of s.
61function stringReplaceSuffix(s, a, b) {
62  return s.slice(0, s.lastIndexOf(a)) + b;
63}
64
65class Rule {
66  constructor(opts) {
67    this.pattern = opts.pattern;
68    this.source = opts.source;
69    this.prereqs = opts.prereqs;
70    this.action = opts.action;
71    this.opts = opts.opts;
72    this.desc =  opts.desc;
73    this.ns = opts.ns;
74  }
75
76  // Create a file task based on this rule for the specified
77  // task-name
78  // ======
79  // FIXME: Right now this just throws away any passed-in args
80  // for the synthsized task (taskArgs param)
81  // ======
82  createTask(fullName, level) {
83    let self = this;
84    let pattern;
85    let source;
86    let action;
87    let opts;
88    let prereqs;
89    let valid;
90    let src;
91    let tNs;
92    let createdTask;
93    let name = Task.getBaseTaskName(fullName);
94    let nsPath = Task.getBaseNamespacePath(fullName);
95    let ns = this.ns.resolveNamespace(nsPath);
96
97    pattern = this.pattern;
98    source = this.source;
99
100    if (typeof source == 'string') {
101      src = Rule.getSource(name, pattern, source);
102    }
103    else {
104      src = source(name);
105    }
106
107    // TODO: Write a utility function that appends a
108    // taskname to a namespace path
109    src = nsPath.split(':').filter(function (item) {
110      return !!item;
111    }).concat(src).join(':');
112
113    // Generate the prerequisite for the matching task.
114    //    It is the original prerequisites plus the prerequisite
115    //    representing source file, i.e.,
116    //
117    //      rule( '%.o', '%.c', ['some.h'] ...
118    //
119    //    If the objective is main.o, then new task should be
120    //
121    //      file( 'main.o', ['main.c', 'some.h' ] ...
122    prereqs = this.prereqs.slice(); // Get a copy to work with
123    prereqs.unshift(src);
124
125    // Prereq should be:
126    // 1. an existing task
127    // 2. an existing file on disk
128    // 3. a valid rule (i.e., not at too deep a level)
129    valid = prereqs.some(function (p) {
130      let ns = self.ns;
131      return ns.resolveTask(p) ||
132        fs.existsSync(Task.getBaseTaskName(p)) ||
133        jake.attemptRule(p, ns, level + 1);
134    });
135
136    // If any of the prereqs aren't valid, the rule isn't valid
137    if (!valid) {
138      return null;
139    }
140    // Otherwise, hunky-dory, finish creating the task for the rule
141    else {
142      // Create the action for the task
143      action = function () {
144        let task = this;
145        self.action.apply(task);
146      };
147
148      opts = this.opts;
149
150      // Insert the file task into Jake
151      //
152      // Since createTask function stores the task as a child task
153      // of currentNamespace. Here we temporariliy switch the namespace.
154      // FIXME: Should allow optional ns passed in instead of this hack
155      tNs = jake.currentNamespace;
156      jake.currentNamespace = ns;
157      createdTask = jake.createTask('file', name, prereqs, action, opts);
158      createdTask.source = src.split(':').pop();
159      jake.currentNamespace = tNs;
160
161      return createdTask;
162    }
163  }
164
165  match(name) {
166    return Rule.match(this.pattern, name);
167  }
168
169  // Test wether the a prerequisite matchs the pattern.
170  // The arg 'pattern' does not have namespace as prefix.
171  // For example, the following tests are true
172  //
173  //   pattern      |    name
174  //   bin/%.o      |    bin/main.o
175  //   bin/%.o      |    foo:bin/main.o
176  //
177  // The following tests are false (trivally)
178  //
179  //   pattern      |    name
180  //   bin/%.o      |    foobin/main.o
181  //   bin/%.o      |    bin/main.oo
182  static match(pattern, name) {
183    let p;
184    let task;
185    let obj;
186    let filename;
187
188    if (pattern instanceof RegExp) {
189      return pattern.test(name);
190    }
191    else if (pattern.indexOf('%') == -1) {
192      // No Pattern. No Folder. No Namespace.
193      // A Simple Suffix Rule. Just test suffix
194      return stringEndWith(pattern, name);
195    }
196    else {
197      // Resolve the dir, prefix and suffix of pattern
198      p = resolve(pattern);
199
200      // Resolve the namespace and task-name
201      task = splitNs(name);
202      name = task.name;
203
204      // Set the objective as the task-name
205      obj = name;
206
207      // Namespace is already matched.
208
209      // Check dir
210      if (path.dirname(obj) != p.dir) {
211        return false;
212      }
213
214      filename = path.basename(obj);
215
216      // Check file name length
217      if ((p.prefix.length + p.suffix.length + 1) > filename.length) {
218        // Length does not match.
219        return false;
220      }
221
222      // Check prefix
223      if (filename.indexOf(p.prefix) !== 0) {
224        return false;
225      }
226
227      // Check suffix
228      if (!stringEndWith(p.suffix, filename)) {
229        return false;
230      }
231
232      // OK. Find a match.
233      return true;
234    }
235  }
236
237  // Generate the source based on
238  //  - name    name for the synthesized task
239  //  - pattern    pattern for the objective
240  //  - source    pattern for the source
241  //
242  // Return the source with properties
243  //  - dep      the prerequisite of source
244  //             (with the namespace)
245  //
246  //  - file     the file name of source
247  //             (without the namespace)
248  //
249  // For example, given
250  //
251  //  - name   foo:bin/main.o
252  //  - pattern    bin/%.o
253  //  - source    src/%.c
254  //
255  //    return 'foo:src/main.c',
256  //
257  static getSource(name, pattern, source) {
258    let dep;
259    let pat;
260    let match;
261    let file;
262    let src;
263
264    // Regex pattern -- use to look up the extension
265    if (pattern instanceof RegExp) {
266      match = pattern.exec(name);
267      if (match) {
268        if (typeof source == 'function') {
269          src = source(name);
270        }
271        else {
272          src = stringReplaceSuffix(name, match[0], source);
273        }
274      }
275    }
276    // Assume string
277    else {
278      // Simple string suffix replacement
279      if (pattern.indexOf('%') == -1) {
280        if (typeof source == 'function') {
281          src = source(name);
282        }
283        else {
284          src = stringReplaceSuffix(name, pattern, source);
285        }
286      }
287      // Percent-based substitution
288      else {
289        pat = pattern.replace('%', '(.*?)');
290        pat = new RegExp(pat);
291        match = pat.exec(name);
292        if (match) {
293          if (typeof source == 'function') {
294            src = source(name);
295          }
296          else {
297            file = match[1];
298            file = source.replace('%', file);
299            dep = match[0];
300            src = name.replace(dep, file);
301          }
302        }
303      }
304    }
305
306    return src;
307  }
308}
309
310
311exports.Rule = Rule;
312