new object $string: $libraries;

var $root inited = 1;
var $string alphabet = "abcdefghijklmnopqrstuvwxyz";
var $string non_alphanumeric = "!@#$%^&*()_+-=~`'{}[]|/?\",.<>;: ";
var $string numbers = "1234567890";

public method .a_or_an() {
    arg string;
    
    if (string[1].lowercase() in "aeiou")
        return "an";
    return "a";
};

public method .alphabet() {
    return alphabet;
};

public method .regexp() {
  arg string, regexp;
  var match, i, out;

  out = [];
  match = match_regexp(string, regexp);
  if (!match) {
    return [];
  }
  for i in (sublist(match, 2)) {
    if (i[1] > 0) {
      out += [substr(string, @i)];
    } else {
      out += [""];
    }
  }
  return out;
};

public method .center() {
    arg text, len, [args];
    var lfill, rfill, textlen, padlen;
    
    // args[1] == string to center
    // args[2] == integer of width to center in
    // args[3] <op> == what to fill the left|right side with.
    // args[4] <op> == what to fill the right side with.
    lfill = args.length() >= 1 && args[1] || " ";
    rfill = args.length() >= 2 ? args[2] : lfill == " " ? "" : lfill;
    textlen = text.length();
    padlen = (len - textlen) / 2;
    if (textlen < len)
        return .fill(padlen, lfill) + text + (rfill ? .fill(len - textlen - padlen, rfill) : "");
    else
        return len > 0 ? text : text.pad(len);
};

public method .chop() {
    arg str, len, [end];
    
    // chops string off end.length() characters before len and appends len
    end = [@end, "..."][1];
    if (str.length() < len)
        return str;
    if (str.length() < end.length())
        return str;
    return str.pad(len - end.length()) + end;
};

public method .echo() {
    arg str;
    
    return str;
};

public method .explode_delimited() {
    arg str, left, right;
    var pattern, parsed, matched, match_num, match_result;
    
    // parse str looking for anything surrounded by left and right
    // ;$string.explode_delimited("foo%[bar]baz", "%[", "]")
    // => [["foo", 1, "baz"], ["bar"]]
    pattern = "*" + left + "*" + right + "*";
    parsed = [];
    matched = [];
    match_num = 0;
    while (str) {
        match_result = str.match_pattern(pattern);
        if (match_result) {
            match_num = match_num + 1;
            parsed = [@parsed, match_result[1], match_num];
            matched = [@matched, match_result[2]];
            str = match_result[3];
        } else {
            parsed = [@parsed, str];
            str = "";
        }
    }
    return [parsed, matched];
};

public method .explode_english_list() {
    arg line, [opts];
    var x, output, tmp;
    
    if (!line)
        return [];
    
    // explodes an english list ("foo, bar and zoo").
    line = line.explode(",");
    output = [];
    for x in (line) {
        x = .trim(x);
        if ((| x.subrange(1, 4) |) == "and ")
            output = [@output, .trim(x.subrange(4))];
        else
            output = [@output, x];
    }
    
    // check the last element, if they didn't specify  'noand
    if (!('noand in opts)) {
        line = output[output.length()].explode();
        tmp = "";
        for x in [1 .. line.length()] {
            if (line[x] == "and") {
                output = output.delete(output.length());
                if (tmp)
                    output = [@output, tmp];
                tmp = $list.join(line.subrange(x + 1));
                if (tmp)
                    output = [@output, tmp];
    
                // only bother with the first "and"
                break;
            }
            tmp = tmp + (tmp ? " " : "") + line[x];
        }
    }
    return output;
};

public method .explode_list() {
    arg str;
    
    if ("," in str)
        return str.explode_english_list();
    else
        return str.explode();
};

public method .explode_quoted() {
    arg str;
    var out, result;
    
    out = [];
    while (str) {
        result = match_pattern(str, "*\"*\"*");
        if (result) {
            out = [@out, @result[1].explode(), result[2].trim()];
            str = result[3];
        } else {
            out = [@out, @str.explode()];
            str = "";
        }
    }
    return out;
};

public method .explode_template_word() {
    arg template;
    var t, x, idx, out, new;
    
    // this only explodes single word templates
    template = template.explode("|");
    out = [];
    for t in (template) {
        idx = "?" in t;
        if (idx) {
            t = t.strip("?");
            new = t.subrange(1, idx - 1);
            out = [@out, new];
            for x in [idx .. t.length()] {
                new = new + t[x];
                out = [@out, new];
            }
        } else {
            out = [@out, t];
        }
    }
    return out;
};

public method .fill() {
    arg n, [args];
    var fill, x;
    
    // same as pad("", n, [args]);
    fill = [@args, " "][1];
    return "".pad(n, fill);
};

public method .find_escaped() {
    arg str, char;
    var good, start, pos, p;
    
    good = 0;
    start = 0;
    while (!good && start < str.length()) {
        pos = (char in str.subrange(start + 1)) + start;
        good = 1;
        if (pos > start) {
            p = pos - 1;
            while (p > 0 && str[p] == "\\") {
                good = good ? 0 : 1;
                p = p - 1;
            }
        }
        if (good)
            return pos;
        else
            start = pos;
    }
};

public method .find_next() {
    arg str, choices;
    var t, first, pos;
    
    //Returns the index of the first string in choices to appear.
    //Returns str.length() if none are in str.
    first = str.length() + 1;
    for t in (choices) {
        pos = t in str;
        if (pos && pos < first)
            first = pos;
    }
    return first;
};

public method .find_next_escaped() {
    arg str, choices;
    var t, first, pos, good, p, start;
    
    //Returns the index of the first string in choices to appear.
    //If 
    //Returns str.length() if none are in str.
    first = str.length() + 1;
    for t in (choices) {
        pos = str.find_escaped(t);
        if (pos < first)
            first = pos;
    }
    return first;
};

public method .is_boolean() {
    arg str;
    
    if ("yes".match_begin(str) || "true".match_begin(str) || str == "1")
        return 1;
    else if ("no".match_begin(str) || "false".match_begin(str) || str == "0")
        return 0;
    return -1;
};

public method .is_numeric() {
    arg string;
    
    return toint(string) || string == "0";
};

public method .last() {
    arg str;
    
    return str[str.length()];
};

public method .left() {
    arg str, width, [fchar];
    
    // will NOT chop off 'str' if it is longer than width, use pad() for that.
    if (fchar)
        return str + (str.length() < width ? "".pad(width - str.length(), fchar[1]) : "");
    else
        return str + (str.length() < width ? "".pad(width - str.length()) : "");
};

public method .match_sub_tag() {
    arg string, tag;
    var x, expl, output, match, matches;
    
    // matches a string between 'tag' and " " in a larger string against
    // the sender's environment.  If a match is found it subs the match.name
    // with the string, otherwize it lets it pass through with the tag, ie:
    // .match_sub_tag("this test #of something #note or other");
    // => "this test #of something Note of sorts or other"
    // where the note is in the sender's environment.
    expl = .explode_delimited(string + " ", tag, " ");
    matches = expl[2];
    expl = expl[1];
    output = "";
    for x in (expl) {
        if (type(x) == 'integer) {
            match = (| sender().match_environment(matches[x]) |);
            if (match)
                output = output + match.name() + " ";
            else
                output = output + tag + matches[x] + " ";
        } else {
            output = output + x;
        }
    }
    return output.subrange(1, output.length() - 1);
};

public method .non_alphanumeric() {
    return non_alphanumeric;
};

public method .numbers() {
    return numbers;
};

public method .onespace() {
    arg str;
    var sub;
    
    if ((sub = "  +".sed(str, " ", "g")))
        str = sub;
    return str;
};

public method .parse_template() {
    arg str;
    var index, out;
    
    out = (str.explode(" *"))[1];
    
    // index = "?" in str;
    // if (index) {
    //     out = uppercase(str.subrange(1, index - 1));
    //     out = out + "?" + str.subrange(index + 1);
    // } else {
    //     out = uppercase(out);
    // }
    return out;
};

public method .pat_sub() {
    arg pat, subs;
    var wc_idx;
    
    // wc_idx == wildcard index
    while (subs) {
        wc_idx = "*" in pat;
        if (wc_idx == 1)
            pat = subs[1] + pat.subrange(2);
        else if (wc_idx == pat.length())
            pat = pat.subrange(1, wc_idx - 1) + subs[1];
        else
            pat = pat.subrange(1, wc_idx - 1) + subs[1] + pat.subrange(wc_idx + 1);
        subs = subs.delete(1);
    }
    return pat;
};

public method .repeat() {
    arg string, times;
    var t, out;
    
    // repeats <string> <times> times
    if (type(string) != 'string)
        throw(~type, "The first agrument must be a string.");
    if (type(times) != 'integer || times < 0)
        throw(~type, "The second agrument must be a non-negatiive integer.");
    out = "";
    for t in [1 .. times]
        out = out + string;
    return out;
};

public method .right() {
    arg str, width, [fchar];
    
    // will not chop off 'str' if it is longer than width (unlike pad())
    if (fchar)
        return "".pad(width - str.length(), fchar[1]) + str;
    else
        return "".pad(width - str.length()) + str;
};

public method .rindex() {
    arg string, index;
    var loc, rest;
    
    // returns the first occurance of index starting from the end of the string,
    // and moving to the beginning.
    loc = index in string;
    rest = loc && string.subrange(loc + 1);
    while (loc && index in rest) {
        loc = loc + (index in rest);
        rest = loc && string.subrange(loc + 1);
    }
    return loc;
};

public method .rindexc() {
    arg str, c;
    var i;
    
    // same as rindex, but only with a single character, faster.
    i = str.length();
    while (i) {
        if (str[i] == c)
            return i;
        i = i - 1;
    }
    return 0;
};

public method .search_pat() {
    arg pat, text, [start_at];
    var line, match_result, type;
    
    line = 1;
    type = [@start_at, 'pattern, 'pattern][2] == 'pattern ? 'match_pattern : 'match_regexp;
    if (start_at) {
        line = start_at[1];
        start_at = [@start_at, 1, 1][2];
        match_result = pat.(type)(text[line].subrange(line));
        if (match_result != 0) {
            if (type == 'match_pattern) {
                pat = $string.pat_sub(pat, match_result);
                return [line, start_at + pat in text[line].subrange(start_at)];
            } else {
                return [line, start_at + match_result[1][1]];
            }
        }
        line = line + 1;
    }
    while (line <= text.length()) {
        match_result = pat.(type)(text[line]);
        if (match_result != 0) {
            if (type == 'pattern) {
                pat = $string.pat_sub(pat, match_result);
                return [line, pat in text[line]];
            } else {
                return [line, match_result[1][1]];
            }
        }
        line = line + 1;
    }
    throw(~strnf, "String not found in text.");
};

public method .split_on_next() {
    arg str, choices;
    var pos, pre, post;
    
    // splits str around whichever choice appears first.
    pos = $string.find_next(str, choices);
    pre = (| str.subrange(1, pos - 1) |) || "";
    post = (| str.subrange(pos + 1) |) || "";
    return [pre, (| str[pos] |) || "", post];
};

public method .split_on_next_escaped() {
    arg str, choices;
    var pos, pre, post;
    
    // splits str around whichever choice appears first.
    pos = str.find_next_escaped(choices);
    pre = (| str.subrange(1, pos - 1) |) || "";
    post = (| str.subrange(pos + 1) |) || "";
    return [pre, (| str[pos] |) || "", post];
};

public method .strip() {
    arg string, [strip];
    var new_str, char;
    
    new_str = "";
    if (!strip)
        strip = "!@#$%^&*()_+-=~`'{}[]|/?\"\,.<>;: ";
    else
        strip = strip[1];
    for char in [1 .. strlen(string)] {
        if (!(string[char] in strip))
            new_str = new_str + string[char];
    }
    return new_str;
};

public method .strip_article() {
    arg str;
    
    return strsed(str, "^(an|a|the)  *", "", "g");
};

public method .strip_others() {
    arg string, valid;
    var new_str, char;
    
    // strips all but "strip" characters from the string
    new_str = "";
    for char in [1 .. string.length()] {
        if (string[char] in valid)
            new_str = new_str + string[char];
    }
    return new_str;
};

public method .sub() {
    arg regexp, string;
    var match, m, complete, out;
    
    match = string.match_regexp(regexp);
    if (!match)
        return 0;
    complete = match[1];
    out = [];
    for m in (match.delete(1)) {
        if (!m[1])
            break;
        out = out + [string.subrange(@m)];
    }
    return out || string.subrange(@complete);
};

public method .to_buffer() {
    arg string;
    
    return (> $buffer.from_string(string) <);
};

public method .to_list() {
    arg str, [sep];
    var result, list;
    
    // separate a string into a list of strings, breaking wherever 'sep' appears.
    // if not provided, sep defaults to a comma.
    // One word of warning.  sep should not contain an asterisk.  If it does,
    // this routine will separate the string oddly, most likely losing bits.
    if (!str)
        return [];
    sep = "*" + (sep ? sep[1] : ",") + "*";
    list = [];
    while (1) {
        result = str.match_pattern(sep);
        if (result) {
            list = list + [result[1]];
            str = result[2];
        } else {
            return list + [str];
        }
    }
};

public method .to_number() {
    arg str;
    
    if (str.is_numeric())
        return toint(str);
    throw(~nonum, "\"" + str + "\" is not a number.");
};

public method .to_symbol() {
    arg str;
    
    str = str.strip();
    return (> tosym(str) <);
};

public method .toliteral() {
    arg [args];
    
    return (> toliteral(@args) <);
};

public method .unquote() {
    arg str;
    
    if (str && str[1] == "\"" && str[str.length()] == "\"")
        return str.subrange(2, str.length() - 2);
    return str;
};

public method .valid_ident() {
    arg str;
    
    return (| tosym(str) |) || 0;
};

public method .valid_method_name() {
    arg str;
    
    return .strip_others(str, alphabet + numbers + "_").length() == str.length();
};

public method .wrap_line() {
    arg string, length, [stuff];
    var output, cutoff, firstline, prefix;
    
    // takes string and wraps it by words, compared to length, breaks with \n
    prefix = [@stuff, ""][1];
    firstline = [@stuff, 0, 0][2];
    output = "";
    if (firstline)
        string = prefix + string;
    while (string.length() > length) {
        cutoff = .rindex(string.subrange(1, length), " ");
        if (cutoff <= prefix.length()) {
            output += "\n" + string.subrange(1, length);
            string = prefix + string.subrange(length + 1);
        } else {
            output += "\n" + string.subrange(1, cutoff - 1);
            string = prefix + string.subrange(cutoff + 1);
        }
    }
    return (output ? output.subrange(3) + "\n" : "") + string;
};

public method .wrap_lines() {
    arg string, length, [stuff];
    var output, cutoff, firstline, prefix;
    
    // takes string and wraps it by words, compared to length, returns a list. 
    prefix = [@stuff, ""][1];
    firstline = [@stuff, 0, 0][2];
    output = [];
    if (firstline)
        string = prefix + string;
    while (string.length() > length) {
        cutoff = .rindex(string.subrange(1, length), " ");
        if (cutoff <= prefix.length()) {
            output = [@output, string.subrange(1, length)];
            string = prefix + string.subrange(length + 1);
        } else {
            output = [@output, string.subrange(1, cutoff - 1)];
            string = prefix + string.subrange(cutoff + 1);
        }
    }
    return [@output, string];
};