/*

    Minimum Profit 5.x
    A Programmer's Text Editor

    File manipulation.

    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

*/

/** editor actions **/

mp.actions['new']	= sub {
    local d = mp.find_file_by_name(L("<unnamed>"));

    if (d != -1) {
        mp.active_i = d;
        d = mp.active();
    }
    else
        d = mp.add(new(mp_doc));

    return d;
};

mp.actions['next'] = sub { mp.next(); };
mp.actions['prev'] = sub { mp.prev(); };

mp_doc.actions['save_as'] = sub (d, newname) {

    if (newname == NULL)
        newname = mp.savefile(L("Save file as:"));

    if (newname != NULL) {
        /* store new name */
        d.name = newname;

        if (d->long_op(d.save) == -1)
            mp.alert(sprintf(L("Error saving '%s': %s"),
                        mp.trim(d.name, 32), ERRNO));
        else
            d->detect_syntax();
    }

    return d;
};

mp_doc.actions['save'] = sub (d) {

    /* name is <unnamed> or something similar; ask for one */
    if (regex(d.name, "/^<.+>$/"))
        d->actions.save_as();
    else
    if (d->long_op(d.save) == -1)
        mp.alert(sprintf(L("Error saving '%s': %s"),
                        mp.trim(d.name, 32), ERRNO));

    return d;
};

mp.actions['close']	= sub (d) {
    if (d->query_close() != 0)
        mp.close();
};

mp.actions['exit']	= sub {
    if (mp.config.auto_sessions)
        mp.save_session();

    if (mp.actions.close_all())
        mp_c.exit();
};

mp.actions['open'] = sub (d, filename) {
    if (filename == NULL)
        filename = mp.openfile(L("File to open:"));

    if (filename != NULL && filename ne "") {
        if (mp.long_op(mp.open, filename) == NULL) {
            if (ERRNO == NULL) {
                /* not open but no ERRNO: it's encrypted */
                local f = mp.form(
                    [
                        {
                            type: 'password',
                            label: L("Password:")
                        }
                    ]
                );

                if (f != NULL) {
                    if (mp.long_op(mp.open, filename, f[0]) == NULL)
                        mp.alert(sprintf(L("Bad password for file '%s'"),
                            mp.trim(filename, 32)));
                }
            }
            else
                mp.alert(sprintf(L("Error opening '%s': %s"),
                            mp.trim(filename, 32), ERRNO));
        }
    }

    return filename;
};

mp_doc.actions['revert'] = sub (d) {
    /* save current name */
    local p = d.name;

    if (d.txt.mod) {
        local r;
        r = mp.confirm(sprintf(L("'%s' has changed. Are you sure?"),
                            mp.trim(p, 32)));

        /* cancel? don't close */
        if (r == 0 || r == 2)
            return d;
    }

    d->store_undo();

    mp.close();
    if (mp.long_op(mp.open, p) == NULL && ERRNO != NULL)
        mp.alert(sprintf("Error opening '%s': %s", p, ERRNO));

    local nd = mp.active();

    nd.undo_q = d.undo_q;

    nd->set_y(d.txt.y)->set_x(d.txt.x);
    nd.txt.vy = d.txt.vy;

    return nd;
};

mp.actions['open_config_file']	= sub {
    mp.open(HOMEDIR ~ ".mp.mpsl");
};

mp.actions['sync'] = sub {
    /* save all modified documents */
    foreach (d, grep(mp.docs, sub (e) { e.txt.mod; }))
        d->actions.save();

    return d;
};

mp_doc.actions['exec_command'] = sub (d, cmd) {
    if (cmd == NULL) {
        local t = mp.form(
            [
                {
                    'label' => L("System command:"),
                    'type' => 'text',
                    'history' => 'system'
                }
            ]
        );

        if (t != NULL)
            cmd = t[0];
    }
    
    if (cmd != NULL) {
        /* does it start with a pipe? */
        if (regex(cmd, '/^\|/')) {
            local p;

            /* yes; current document should be fed to it */
            cmd = sregex(cmd, '/^\|/');

            if ((p = popen(cmd, "w")) != NULL) {
                mp.busy(1);

                foreach (l, d->get_active_area())
                    write(p, l ~ mp.config.eol);

                pclose(p);
                mp.busy(0);
            }
            else
                mp.alert(sprintf(L("Error writing to command '%s'"), cmd));
        }
        else {
            /* no; execute command and insert into cursor */
            local p;

            if ((p = popen(cmd, "r")) != NULL) {
                local l;

                d->store_undo();
                mp.busy(1);

                while ((l = read(p)) != NULL) {
                    d->insert(mp.chomp(l));
                    d->insert_newline();
                }

                pclose(p);
                mp.busy(0);
            }
            else
                mp.alert(sprintf(L("Error reading from command '%s'"), cmd));
        }
    }

    return d;
};

mp_doc.actions['filter_selection'] = sub (d, cmd) {
    if (cmd == NULL) {
        local t = mp.form(
            [
                {
                    'label'     => L("System command:"),
                    'type'      => 'text',
                    'history'   => 'system2'
                }
            ]
        );

        if (t != NULL)
            cmd = t[0];
    }

    if (cmd != NULL) {
        d->store_undo();

        /* if there is no selection, take full document */
        if (d.txt.mark == NULL) {
            d->move_bof()->mark()->move_eof()->mark();
        }

        /* take it out */
        d->cut();

        /* now feed it to the command */
        local p = popen2(cmd);

        if (p != NULL) {
            write(p[1], join(mp.clipboard, "\n"));
            pclose(p[1]);

            local l;
            while ((l = read(p[0])) != NULL)
                d->insert(l);

            pclose(p[0]);
        }
    }

    return d;
};

mp.actions['close_all'] = sub {
    local s;

    while (s = size(mp.docs)) {
        local doc = mp.docs[mp.active_i];

        /* close current document */
        mp.actions.close(doc);

        /* if the size of the list hasn't changed,
            action was cancelled, so don't exit */
        if (s == size(mp.docs))
            return 0;
    }

    return 1;
};

mp_doc.actions['open_under_cursor'] = sub (d) {
    local w;

    if (d.txt.lines[d.txt.y]->regex('/^@@ /')) {
        /* it's a diff mark */

        /* pick line */
        local l = d.txt.lines[d.txt.y]->regex('/[0-9]+/');
        local f;
        local y = d.txt.y - 1;

        while (y >= 0) {
            /* pick filename (stripping mark from previous line) */
            if (d.txt.lines[y]->regex('/^\+\+\+ /')) {
                f = d.txt.lines[y]->sregex('/^\+\+\+ .\//');
                break;
            }

            y--;
        }

        if (f != NULL)
            d = mp.open(f ~ ':' ~ l ~ ':');
    }
    else
    if ((w = d->get_word("/[^ \t]+/")) != NULL)
        d = mp.open(w);

    return d;
};

mp.actions['hex_view']	= sub {
    local n;

    if ((n = mp.openfile(L("File to open:"))) != NULL && n ne "")
        local d = new(mp_hex_view, { name: n})->init();

        if (d == NULL)
            mp.alert(sprintf("Error opening '%s': %s",
                        mp.trim(n, 32), ERRNO));
        else
            mp.add(d);

    return d;
};

mp.actions['open_dropped_files'] = sub {
    while (size(mp.dropped_files))
        mp.open(shift(mp.dropped_files));
};


/** default key bindings **/

mp.keycodes['ctrl-n']           = 'next';
mp.keycodes['ctrl-o']           = 'open';
mp.keycodes['ctrl-q']           = 'exit';
mp.keycodes['ctrl-w']           = 'close';
mp.keycodes['dropped-files']    = 'open_dropped_files';
mp.keycodes['close-window']     = 'exit';
mp_doc.keycodes['ctrl-s']       = 'save';
mp_doc.keycodes['ctrl-enter']   = 'open_under_cursor';
mp_doc.keycodes['alt-enter']    = 'ctrl-enter';

/** action descriptions **/

mp.actdesc['new']                = LL("New");
mp.actdesc['save']               = LL("Save...");
mp.actdesc['save_as']            = LL("Save as...");
mp.actdesc['next']               = LL("Next");
mp.actdesc['prev']               = LL("Previous");
mp.actdesc['open']               = LL("Open...");
mp.actdesc['exit']               = LL("Exit");
mp.actdesc['close']              = LL("Close");
mp.actdesc['revert']             = LL("Revert");
mp.actdesc['close_all']          = LL("Close all");

mp.actdesc['open_config_file']   = LL("Edit configuration file");
mp.actdesc['sync']               = LL("Save modified texts");
mp.actdesc['exec_command']       = LL("Run system command...");
mp.actdesc['filter_selection']   = LL("Filter selection through system command...");
mp.actdesc['open_under_cursor']  = LL("Open file under cursor");
mp.actdesc['hex_view']           = LL("Hexadecimal viewer...");
mp.actdesc['open_dropped_files'] = LL("Open dropped files");

/** code **/

sub mp.chomp(str)
/* chomps the end of file chars from a string */
{
    sregex(str, "/\r*\n*$/");
}


sub mp_doc.pre_event(doc, k)
/* an event is to be processed */
{
    if (doc.mtime && time() > mp.mtime_test) {
        local s;

        if ((s = stat(doc.name)) != NULL && s[9] > doc.mtime) {
            if (mp.confirm(sprintf(L("'%s' was changed externally. Reload?"),
                mp.trim(doc.name, 32))) == 1) {
                doc->actions.revert();
                k = NULL;
            }
 
            doc.mtime = s[9];
        }

        mp.mtime_test = time() + 2;
    }

    return k;
}


sub mp_doc.post_event(doc, k)
/* an event has just been processed */
{
    /* if it's read only but has modifications, revert them */
    if (doc.read_only && size(doc.undo_q)) {
        while (size(doc.undo_q))
            doc->undo();

        mp.message = {
            timeout:    time() + 2,
            string:     '*' ~ L("Read-only document") ~ '*'
        };
    }

    return doc;
}


sub mp_doc.query_close(doc)
/* queries a close; if it returns 0, the closing is rejected */
{
    local r = 1;

    if (doc.txt.mod) {
        r = mp.confirm(sprintf(L("'%s' has changed. Save changes?"),
                    mp.trim(doc.name, 32)));

        /* confirm? save */
        if (r == 1)
            doc->actions.save();
    }

    return r;
}


sub mp.save_th(f, doc)
/* mp.save() helper */
{
    local nl = 0;
    local eol = mp.config.keep_eol && doc.eol || mp.config.eol;

    doc.disk_op = 1;

    /* save as a plain text file */
    foreach (l, doc.txt.lines) {
        /* write a line separator if it's not the first line */
        if (nl)
            write(f, eol);

        write(f, l);
        nl++;
    }

    doc.disk_op = 0;

    close(f);

    return nl;
}


sub mp_doc.save(doc)
/* saves a file */
{
    local f;
    local s = NULL;
    local ret = 0;

    /* if unlink before write is desired, do it */
    if (mp.config.unlink && (s = stat(doc.name)) != NULL)
        unlink(doc.name);

    /* set the encoding for this file opening */
    TEMP_ENCODING = doc.encoding;

    if ((f = open(doc.name, "wb")) == NULL) {
        /* can't write? delete name */
        doc.name = L("<unnamed>");
        ret = -1;
    }
    else {
        ret = 0;

        /* if the document has a password, save it encrypted */
        if (doc.password)
            mp.crypt1_save(f, doc.txt.lines, doc.password);
        else
            mp.save_th(f, doc);
    
        doc.txt.mod = 0;
    
        /* set back the permissions and ownership, if available */
        if (s != NULL) {
            chmod(doc.name, s[2]);
            chown(doc.name, s[4], s[5]);
        }

        s = stat(doc.name);
        doc.mtime = s[9];
    }
    
	return ret;
}


sub mp_doc.long_op(doc, func, a2, a3, a4)
{
    local r;

    doc->busy(1);
    r = doc->func(a2, a3, a4);
    doc->busy(0);

    return r;
}


sub mp.add(doc)
/* adds a doc to the list of documents */
{
    /* store in the list and set as active */
    ins(mp.docs, doc, mp.active_i);

    return doc;
}


sub mp.next()
/* rotates through the document list */
{
    if (++mp.active_i == size(mp.docs))
        mp.active_i = 0;

    return mp.active();
}


sub mp.prev()
/* rotates through the document list, backwards */
{
    if (--mp.active_i == -1)
        mp.active_i = size(mp.docs) - 1;

    return mp.active();
}


sub mp.close()
/* closes the active document */
{
    /* delete from the list */
    adel(mp.docs, mp.active_i);

    /* rotate if it was the last one */
    if (mp.active_i == size(mp.docs))
        mp.active_i = 0;

    /* cannot call mp.active() */
}


sub mp.find_file_by_name(filename)
/* finds an open file by its name */
{
    mp.docs->map(sub(d) { d.name; })->seek(filename);
}


sub mp.open(filename, password)
/* opens a new document */
{
    local d, s, f;
    local x, y;

    /* test if filename includes :y: or :y:x: positioning */
    if (regex(filename, "/.+:[0-9]+:([0-9]+:)?$/")) {
        local l = split(filename, ':');

        x           = l[2];
        y           = l[1];
        filename    = l[0];
    }

    if ((s = mp.find_file_by_name(filename)) != -1) {
        /* already open */
        mp.active_i = s;
        d = mp.active();
    }
    else
    if ((s = stat(filename)) == NULL) {
        /* non-existent file; create as new */
        mp.message = {
            timeout:    time() + 2,
            string:     sprintf(L("New file '%s'"), filename)
        };

        ERRNO = NULL;

        d = mp.add(new(mp_doc, { name: filename }));
	}
    else
    if (s[13] != NULL) {
        local r;

        /* canonicalize, if possible */
        filename = s[13];

        /* look again for this filename in the open files */
        if ((r = mp.find_file_by_name(filename)) != -1) {
            mp.active_i = r;
            d = mp.active();
        }
    }

    if (d == NULL) {
        /* still not open: load */
        f = open(filename, "rb");

        if (f != NULL) {
            if (mp.crypt1_detect(f)) {
                ERRNO = NULL;

                if (password != NULL && password ne '') {
                    /* and load the file */
                    d = mp.add(new(mp_doc, { name: filename }));

                    d.txt.lines     = mp.crypt1_load(f, password);
                    d.password      = password;
                }
            }
            else {
                /* close file (needed for rewinding AND
                   possible encoding autodetection) */
                close(f);

                /* reopen and read */
                f = open(filename, "rb");
                d = mp.add(new(mp_doc, { name: filename }));
                d.txt.lines = mp_c.plain_load(f);
            }

            close(f);
        }
        else
        if (s[2] & 0040000) {
            /* it's a directory: show it */
            ERRNO = NULL;

            d = mp.add(new(mp_doc_dir, { name: filename }));

            d->setup();
        }

        if (d) {
            /* store mtime */
            d.mtime = s[9];

            /* store the encoding */
            d.encoding = DETECTED_ENCODING || ENCODING || '';

            /* always keep original EOL */
            d.eol = mp.last_seen_eol;

            d->detect_syntax();

            /* ignore further system errors */
            ERRNO = NULL;
        }
    }

    if (d) {
        if (y)
            d->search_set_y(y - 1);
        if (x)
            d->set_x(x - 1);
    }

	return d;
}


/** hexadecimal viewer object **/

sub join_by_attr(line)
{
    local r = [ line->shift(), line->shift() ];

    while (line->size()) {
        local a = line->shift();
        local s = line->shift();

        if (r[-2] == a)
            r[-1] = r[-1] ~ s;
        else
            push(r, a, s);
    }

    return r;
}


global mp_hex_view = new(mp_base, {
        name:           '<hex view>',
        offset:         0,
        bpl:            8,
        data:           [],
        f_offset:       0,
        f_size:         0,

        get_status: sub (hex) {
            sprintf("HEX %s - 0x%06x (%d)", name, offset, offset);
        },
        init:       sub (hex) {
            local f;

            if ((f = open(name, "rb")) == NULL)
                hex = NULL;
            else
                close(f);

            return hex;
        },
        keypress:   sub (hex, keycode) {
            local f = open(name, "rb");

            if (keycode eq 'cursor-down')
                offset += bpl;
            else
            if (keycode eq 'cursor-up')
                offset -= bpl;
            else
            if (keycode eq 'page-down')
                offset += bpl * (mp.window.ty - 1);
            else
            if (keycode eq 'page-up')
                offset -= bpl * (mp.window.ty - 1);
            else
            if (keycode eq 'home')
                offset = 0;
            else
            if (keycode eq 'end')
                offset = 0x7fffffff;

            if (offset < 0)
                offset = 0;

            fseek(f, 0, 2);
            if (offset >= ftell(f))
                offset = ftell(f);

            close(f);

            return hex;
        },
        render:     sub (hex) {
            local out = [];
            local f, o;

            /* calculate bytes per line */
            bpl = 100;

            while ((12 + (bpl * 4)) > mp.window.tx)
                bpl--;

            /* round offset */
            offset = sprintf("%d", offset / bpl) * bpl;

            f = open(name, "rb");
            fseek(f, offset, 0);

            o = offset;

            while (size(out) < mp.window.ty) {
                local ov, nv, av, n, c;

                nv = [];
                av = [
                    mp.colors.word2.attr,
                    ' | '
                ];
                ov = [
                    mp.colors.word1.attr,
                    sprintf(" %06X |", ftell(f))
                ];

                for (n = 0; n < bpl; n++) {
                    local a = 0;

                    if ((c = getchar(f)) == NULL)
                        break;

                    if (o >= f_offset && o < f_offset + f_size)
                        a = mp.colors.search.attr;
                    else
                        a = mp.colors.normal.attr;

                    if (o == f_offset)
                        push(nv, mp.colors.normal.attr, ' ',
                            a, sprintf("%02X", ord(c)));
                    else
                        push(nv, a, sprintf(" %02X", ord(c)));

                    if (ord(c) == 0x0a)
                        c = "\x{00b6}";
                    else
                    if (ord(c) < 32 || ord(c) > 126)
                        c = "\x{00b7}";

                    push(av, a, c);

                    o++;
                }

                while (n < bpl) {
                    push(nv, mp.colors.normal.attr, '   ');
                    push(av, mp.colors.word2.attr,  ' ');
                    n++;
                }

                push(out, ov->join(nv)->join(av)->join_by_attr());

                if (c == NULL)
                    break;
            }

            while (size(out) < mp.window.ty)
                push(out, []);

            close(f);

            return out;
        },
        actions:    {
            seek_next:  sub (d) {
                local f = open(d.name, "rb");
                fseek(f, d.f_offset + 1);

                mp.busy(1);
                local found = mp_c.search_hex(f, d.data);
                mp.busy(0);

                if (found) {
                    d.f_offset  = ftell(f);
                    d.f_size    = d.data->size() / 2;
                    d.offset    = d.f_offset;
                }
                else
                    mp.alert(L("Not found."));

                close(f);
            },

            seek:       sub (d) {
                local t = mp.form(
                    [
                        {
                            label:   L("Hex string to seek:"),
                            type:    'text',
                            history: 'search-hex'
                        }
                    ]
                );

                if (t != NULL) {
                    d.data = t[0];
                    d.f_offset = d.offset - 1;
                    d.actions.seek_next(d);
                }
            }
        },
        keycodes:   {
            "ctrl-f"    => 'seek',
            "f3"        => 'seek_next'
        }
    }
);
