/*

    Minimum Profit 5.x
    A Programmer's Text Editor

    Search and replace.

    Copyright (C) 1991-2012 Angel Ortega <angel@triptico.com>

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    as published by the Free Software Foundation; either version 2
    of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

    http://triptico.com

*/

/* successful searches will always be shown in this line */
mp.config.move_seek_to_line = 5;

/* grep recursivity */
mp.config.recursive_grep = 0;

/** editor actions **/

mp_doc.actions['seek'] = sub (d) {
    local t = mp.form(
        [
            {
                label:   L("Text to seek:"),
                type:    'text',
                history: 'search'
            },
            {
                label:   L("Case sensitive") ~ ':',
                type:    'checkbox',
                value:   mp.config.case_sensitive_search
            }
        ]
    );

    if (t != NULL) {
        mp.config.case_sensitive_search = t[1];

        d->long_op(mp_doc.search, mp.backslash_codes(t[0])) ||
            mp.alert(L("Text not found."));
    }

    return d;
};

mp_doc.actions['seek_next'] = sub (d) {
    d->long_op(mp_doc.search, NULL) || mp.alert(L("Text not found."));

    return d;
};

mp_doc.actions['seek_prev'] = sub (d) {
    d->long_op(mp_doc.search_back, NULL) || mp.alert(L("Text not found."));

    return d;
};

mp_doc.actions['replace'] = sub (d) {
    local r = mp.form(
        [
            {
                'label'     => L("Replace text:"),
                'type'      => 'text',
                'history'   => 'search'
            },
            {
                'label'     => L("Replace with:"),
                'type'      => 'text',
                'history'   => 'replace'
            },
            {
                'label'     => L("Case sensitive") ~ ':',
                'type'      => 'checkbox',
                'value'     => mp.config.case_sensitive_search
            },
            {
                'label'     => L("Global replace:"),
                'type'      => 'checkbox',
                'value'     => mp.config.global_replace
            }
        ]
    );

    if (r != NULL) {
        mp.config.case_sensitive_search = r[2];
        mp.config.global_replace = r[3];

        d->store_undo();
        d->long_op(mp_doc.replace, mp.backslash_codes(r[0]), mp.backslash_codes(r[1]));
    }

    return d;
};

mp_doc.actions['seek_next_char'] = sub (d) { d->seek_prev_or_next_char(mp_doc.search); };
mp_doc.actions['seek_prev_char'] = sub (d) { d->seek_prev_or_next_char(mp_doc.search_back); };

mp.actions['grep']	= sub {
    local r = mp.form(
        [
            {
                'label'     => L("Text to seek:"),
                'type'      => 'text',
                'history'   => 'search'
            },
            {
                'label'     => L("Files to grep (empty, all):"),
                'type'      => 'text',
                'history'   => 'grep'
            },
            {
                'label'     => L("Base directory (empty, current):"),
                'type'      => 'text',
                'history'   => 'grep_base'
            },
            {
                'label'     => L("Recursive?"),
                'type'      => 'checkbox',
                'value'     => mp.config.recursive_grep
            }
        ]
    );

    if (r != NULL && r[0] ne '') {
        mp.config.recursive_grep = r[3];

        local t = '<grep ' ~ r[0] ~ ' ' ~ r[1] ~ '>';

        if ((r = mp.long_op(mp.grep, '/' ~ r[0] ~ '/', r[1], r[2], r[3])) == NULL)
            mp.alert(L("File(s) not found."));
        else
        if (size(r) == 0)
            mp.alert(L("Text not found."));
        else {
            local l = mp.open(t);

            l.txt.lines = [];
            l->move_bof();

            foreach (e, r)
                l->insert(sprintf("%s:%d:%d: %s\n", e[0], e[1] + 1, e[2] + 1, e[3]));

            l->move_bof();

            l.txt.mod = 0;
            l.read_only = 1;
        }
    }

    return d;
};

/** default key bindings **/

mp_doc.keycodes['f3']               = 'seek_next';
mp_doc.keycodes['ctrl-f3']          = 'seek_prev';
mp_doc.keycodes['ctrl-f']           = 'seek';
mp_doc.keycodes['ctrl-r']           = 'replace';
mp_doc.keycodes['ctrl-page-down']   = 'seek_next_char';
mp_doc.keycodes['ctrl-page-up']     = 'seek_prev_char';

/** action descriptions **/

mp.actdesc['seek']              = LL("Search text...");
mp.actdesc['seek_next']         = LL("Search next");
mp.actdesc['seek_prev']         = LL("Search previous");
mp.actdesc['replace']           = LL("Replace...");
mp.actdesc['seek_next_char']    = LL("Move to next instance of current char");
mp.actdesc['seek_prev_char']    = LL("Move to previous instance of current char");
mp.actdesc['grep']              = LL("Grep (find inside) files...");

/** code **/

/**
 * mp_doc.search_set_y - Sets the y position after a successful search.
 * @doc: the document
 * @y: y coordinate
 *
 * Sets the y position after a successful search, setting the
 * visual line to that defined in mp.config.move_seek_to_line.
 */
sub mp_doc.search_set_y(doc, y)
{
    doc->set_y_keep_vy(y);

    /* set always to the same line */
    if (mp.config.move_seek_to_line != NULL &&
       (doc.txt.vy = doc.txt.y - mp.config.move_seek_to_line) < 0)
        doc.txt.vy = 0;

    return doc;
}


sub mp.prefix_regex(str)
/* set str to be a valid regex */
{
    if (!str)
        return NULL;

    /* surround with / for the regex */
    str = '/' ~ str ~ '/';

    /* add optional case insensitivity flag */
    if (! mp.config.case_sensitive_search)
        str = str ~ 'i';

    return str;
}


sub mp_doc.search_dir(doc, str, dir)
/* search str and put the current position there, with direction */
{
    local txt, r, l, lines;
    local bx, by, ex, ey;

    if (str == NULL)
        str = mp.last_search;
    else {
        str = mp.prefix_regex(str);
        mp.last_search = str;
    }

    if (str == NULL)
        return NULL;

    txt = doc.txt;

    if (dir == -1) {
        /* search backwards */
        str = str ~ 'l';

        ex = txt.x && txt.x - 1 || 0;
        ey = txt.y;

        if (txt.mark && !txt.mark.incomplete) {
            if (ey >= txt.mark.ey) {
                ey = txt.mark.ey;

                if (ex > txt.mark.ex)
                    ex = txt.mark.ex;
            }

            bx = txt.mark.bx;
            by = txt.mark.by;
        }
        else {
            bx = 0;
            by = 0;
        }
    }
    else {
        /* search forward */
        bx = txt.x;
        by = txt.y;

        if (txt.mark && !txt.mark.incomplete) {
            if (by <= txt.mark.by) {
                by = txt.mark.by;

                if (bx < txt.mark.bx)
                    bx = txt.mark.bx;
            }

            ex = txt.mark.ex;
            ey = txt.mark.ey;
        }
        else {
            ex = size(txt.lines[-1]);
            ey = size(txt.lines);
        }
    }

    lines = doc->get_range(bx, by, ex, ey, 0);

    /* do the search */
    local n = (dir == -1) && (size(lines) - 1) || 0;
    while ((l = lines[n]) != NULL && regex(l, str) == NULL)
        n += dir;

    r = regex();

    if (r) {
        /* if it was found in the first line, add offset */
        r[0] += (n == 0 && bx);

        doc->search_set_y(by + n)->set_x(r[0] + r[1]);
    }

    return r;
}


sub mp_doc.search(doc, str)
/* search str and put the current position there, downwards */
{
    doc->search_dir(str, 1);
}


sub mp_doc.search_back(doc, str)
/* search str and put the current position there, backwards */
{
    doc->search_dir(str, -1);
}


sub mp_doc.replace_1(doc, org, dst)
/* searches 'org' and replaces it with 'dst', once */
{
    local c;

    if ((c = doc->search(org)) != NULL) {
        local txt, l;

        txt = doc.txt;

        /* substitute */
        txt.lines[txt.y] = sregex(txt.lines[txt.y],
                                mp.prefix_regex(org),
                                dst, c[0]
                            );

        /* move to correct position */
        c = regex();
        doc->set_x(c[0] + c[1]);

        txt.mod++;
    }

    return c;
}


sub mp_doc.replace(doc, org, dst)
/* replaces 'org' with 'that', may be globally */
{
    local cnt = 0;

    while (doc->replace_1(org, dst)) {
        cnt++;

        if (!mp.config.global_replace)
            break;
    }

    mp.message = {
        timeout:    time() + 4,
        string:     sprintf(L("%d replaces"), cnt)
    };

    return doc;
}


sub mp_doc.seek_prev_or_next_char(doc, func)
/* moves to next or previous occurence of current char */
{
    local txt = doc.txt;

    /* get current char */
    local w = splice(txt.lines[txt.y], NULL, txt.x, 1);

    /* move one char right */
    doc->move_right();

    /* search for it (mp.search() or mp.search_back()) */
    local t = mp.last_search;
    doc->func('\' ~ w[1]);
    mp.last_search = t;

    /* move back */
    doc->move_left();

    return doc;
}


sub mp.grep(rx, spec, base, rec, r)
/* Greps str in the files in spec. Returns NULL if no file matched the glob()
   (or glob() is unsupported), an empty list if the string was not found or
   an array with the matches, that are four-element arrays with the file name,
   the line number, the column number and the line that matched */
{
    local all;

    /* if spec is empty, set as NULL (meaning "glob everything") */
    if (spec eq '')
        spec = NULL;

    if (base eq '')
        base = NULL;

    all = glob(spec, base);

    if (r == NULL)
        r = [];

    /* spec globs to NULL or empty; abort */
    if (size(all) == 0)
        return r;

    foreach (fn, all) {
        local f;

        if ((f = open(fn, "r")) != NULL) {
            local l, n;

            /* file open; now grep */
            while (l = read(f)) {
                l = mp.chomp(l);

                if (regex(l, rx)) {
                    local t = regex();
                    /* found; store line, filename and linenum */
                    push(r, [ fn, n, t[0], l ]);
                }

                n++;
            }

            close(f);
        }
    }

    if (rec) {
        /* glob again, trying subdirectories */
        foreach (fn, glob(NULL, base)) {
            if (regex(fn, '@/$@')) {
                r = mp.grep(rx, spec, fn, rec, r);
            }
        }
    }

    return r;
}
