/*

    Minimum Profit 5.x
    A Programmer's Text Editor

    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

*/


/** startup: language first **/

/* L(x) is the same as gettext(x) */
L = gettext;

/* LL(x) is the same as x */
sub LL(x) { x; }

/* set gettext() domain */
gettext_domain('minimum-profit', APPDIR ~ 'locale');

/* test if gettext() can do a basic translation */
if (gettext('&File') eq '&File' && ENV.LANG) {
    /* no; try alternatives using the LANG variable */
    local v = [ sregex(ENV.LANG, '!/!g') ]; /* es_ES.UTF-8 */
    push(v, shift(split(v[-1], '.')));      /* es_ES */
    push(v, shift(split(v[-1], '_')));      /* es */

    foreach (l, v) {
        eval('load("lang/' ~ l ~ '.mpsl");');

        if (ERROR == NULL)
            break;
    }

    ERROR = NULL;
}

/** main namespace **/

global mp = {
    VERSION:    mp_c.VERSION,

    /* document list */
    docs:       [],
    active_i:   0,

    /* configuration */
    config:     {
        undo_levels:            100,
        word_wrap:              0,
        auto_indent:            0,
        tab_size:               4,
        tabs_as_spaces:         1,
        dynamic_tabs:           0,
        unlink:                 1,
        case_sensitive_search:  1,
        global_replace:         0,
        preread_lines:          60,
        mark_eol:               0,
        maximize:               0,
        keep_eol:               1,
        smart_bol:              1,
        emacs_scrolling:        0,
        no_text_mouse:          0,
        show_line_numbers:      0,
        eol:                    (mp_drv.id eq 'win32' && "\r\n" || "\n"),
        status_format:          "%m%n %x,%y [%l] %R%O %s %e %t",
        time_stamp_format:      "%a, %d %b %Y %H:%M:%S %z"
    },

    /* regular expression matching a word (test fallback) */
    word_regex:     "/[[:alnum:]_]+/i",

    /* viewport size */
    window:         {},

    /* allowed color names (order matters, match the Unix curses one) */
    color_names:    [
        "default",
        "black", "red", "green", "yellow",
        "blue", "magenta", "cyan", "white"
    ],

    /* color definitions */
    colors:         {
        normal: {
            text:   [ 'default', 'default' ],
            gui:    [ 0x000000, 0xffffff ]
        },
        cursor: {
            text:   [ 'default', 'default' ],
            gui:    [ 0x000000, 0xffffff ],
            flags:  [ 'reverse' ]
        },
        selection: {
            text:   [ 'red', 'default' ],
            gui:    [ 0xff0000, 0xffffff ],
            flags:  [ 'reverse']
        },
        comments: {
            text:   [ 'green', 'default' ],
            gui:    [ 0x00cc77, 0xffffff ]
        },
        documentation: {
            text:   [ 'cyan', 'default' ],
            gui:    [ 0x8888ff, 0xffffff ]
        },
        quotes: {
            text:   [ 'blue', 'default' ],
            gui:    [ 0x0000ff, 0xffffff ],
            flags:  [ 'bright' ]
        },
        matching: {
            text:   [ 'black', 'cyan' ],
            gui:    [ 0x000000, 0xffff00 ]
        },
        word1: {
            text:   [ 'green', 'default' ],
            gui:    [ 0x00aa00, 0xffffff ],
            flags:  [ 'bright' ]
        },
        word2: {
            text:   [ 'red', 'default' ],
            gui:    [ 0xff6666, 0xffffff ],
            flags:  [ 'bright' ]
        },
        word3: {
            text:   [ 'default', 'default' ],
            gui:    [ 0x000088, 0xffffff ],
            flags:  [ 'bright' ]
        },
        tag: {
            text:   [ 'cyan', 'default' ],
            gui:    [ 0x8888ff, 0xffffff ],
            flags:  [ 'underline' ]
        },
        spell: {
            text:   [ 'red', 'default' ],
            gui:    [ 0xff8888, 0xffffff ],
            flags:  [ 'bright', 'underline' ]
        },
        search: {
            text:   [ 'black', 'green' ],
            gui:    [ 0x000000, 0x00cc77 ]
        }
    },

    /* hash of specially coloured words */
    word_color: {},

    /* generic keycodes */
    keycodes:   {
        ansi: {}
    },

    /* generic actions */
    actions:    {},

    /* action descriptions (generic and document specific) */
    actdesc:    {},

    /* form history */
    history:    {},

    /* the menu */
    menu:       [
        [
            LL("&File"), [
                'new', 'open', 'save', 'save_as', 'close', 'revert',
                'open_under_cursor',
                '-', 'open_folder',
                '-', 'hex_view',
                '-', 'set_password',
                '-', 'open_config_file', 'open_templates_file',
                '-', 'sync',
                '-', 'exit'
            ]
        ],
        [
            LL("&Edit"), [
                'undo', 'redo', '-',
                'cut_mark', 'copy_mark', 'paste_mark', 'delete_mark',
                'delete_line', 'cut_lines_with_string', '-',
                'mark',	'mark_vertical', 'unmark', 'mark_all', '-',
                'insert_template', '-',
                'exec_command', 'filter_selection', '-',
                'exec_action', 'eval', 'eval_doc'
            ]
        ],
        [
            LL("&Search"), [
                'seek', 'seek_next', 'seek_prev', 'replace', '-',
                'complete', '-',
                'grep'
            ]
        ],
        [
            LL("&Go to"), [
                'next', 'prev',
                'move_bof', 'move_eof', 'move_bol', 'move_eol',
                'goto', 'move_word_right', 'move_word_left', '-',
                'section_list', 'move_section_up', 'move_section_down',
                '-', 'document_list'
            ]
        ],
        [
            LL("&Project"), [
                'build', '-',
                'vcs', '-',
                'find_tag', 'complete_symbol', '-',
                'save_session', 'load_session'
            ]
        ],
        [
            LL("&Writing") , [
                'toggle_spellcheck',
                'seek_misspelled', 'ignore_last_misspell', '-',
                'seek_repeated_word', '-',
                'word_count', '-',
                'word_wrap_paragraph', 'join_paragraph', '-',
                'insert_next_item', 'insert_time_stamp'
            ]
        ],
        [
            LL("&Options"), [
                'record_macro', 'play_macro', '-',
                'encoding', 'tab_options', 'line_options',
                'repeated_words_options', '-',
                'zoom_in', 'zoom_out', '-',
                'pipes', '-', 'check_for_updates', 'about'
            ]
        ]
    ]
};


/** new color schemes **/

/* don't remember where I took this one */
/*
mp.colors.normal.gui        = [ 0x000000, 0xeeeeee ];
mp.colors.cursor.gui        = [ 0x000000, 0xeeeeee ];
mp.colors.quotes.gui        = [ 0x006666, 0xeeeeee ];
mp.colors.comments.gui      = [ 0x880000, 0xeeeeee ];
mp.colors.word1.gui         = [ 0x000088, 0xeeeeee ];
mp.colors.word2.gui         = [ 0x660066, 0xeeeeee ];
mp.colors.word3.gui         = [ 0x666600, 0xeeeeee ];
mp.colors.documentation.gui = [ 0x8888ff, 0xeeeeee ];
*/

/* Tomorrow: https://github.com/chriskempson/tomorrow-theme */
/*
mp.colors.normal.gui        = [ 0x4d4d4c, 0xffffff ];
mp.colors.cursor.gui        = [ 0x4d4d4c, 0xffffff ];
mp.colors.quotes.gui        = [ 0x3e999f, 0xffffff ];
mp.colors.comments.gui      = [ 0x8e908c, 0xffffff ];
mp.colors.word1.gui         = [ 0x4271ae, 0xffffff ];
mp.colors.word2.gui         = [ 0xeab700, 0xffffff ];
mp.colors.word3.gui         = [ 0xf5871f, 0xffffff ];
mp.colors.documentation.gui = [ 0x8959a8, 0xffffff ];
mp.colors.selection.gui     = [ 0xd6d6d6, 0x000000 ];
*/

/* Tomorrow Night Eighties: https://github.com/chriskempson/tomorrow-theme */
/*
mp.colors.normal.gui        = [ 0xcccccc, 0x2d2d2d ];
mp.colors.cursor.gui        = [ 0xcccccc, 0x2d2d2d ];
mp.colors.quotes.gui        = [ 0x66cccc, 0x2d2d2d ];
mp.colors.comments.gui      = [ 0x999999, 0x2d2d2d ];
mp.colors.word1.gui         = [ 0x6699cc, 0x2d2d2d ];
mp.colors.word2.gui         = [ 0xffcc66, 0x2d2d2d ];
mp.colors.word3.gui         = [ 0xf99157, 0x2d2d2d ];
mp.colors.documentation.gui = [ 0xcc99cc, 0x2d2d2d ];
mp.colors.selection.gui     = [ 0xf2777a, 0x000000 ];
*/

/* Tomorrow Night: https://github.com/chriskempson/tomorrow-theme */
/*
mp.colors.normal.gui        = [ 0xc5c8c6, 0x1d1f21 ];
mp.colors.cursor.gui        = [ 0xc5c8c6, 0x1d1f21 ];
mp.colors.quotes.gui        = [ 0x8abeb7, 0x1d1f21 ];
mp.colors.comments.gui      = [ 0x969896, 0x1d1f21 ];
mp.colors.word1.gui         = [ 0x81a2be, 0x1d1f21 ];
mp.colors.word2.gui         = [ 0xf0c674, 0x1d1f21 ];
mp.colors.word3.gui         = [ 0xde935f, 0x1d1f21 ];
mp.colors.documentation.gui = [ 0xb294bb, 0x1d1f21 ];
mp.colors.selection.gui     = [ 0xcc6666, 0x000000 ];
*/

/* Tomorrow Night Bright: https://github.com/chriskempson/tomorrow-theme */
/*
mp.colors.normal.gui        = [ 0xeaeaea, 0x000000 ];
mp.colors.cursor.gui        = [ 0xeaeaea, 0x000000 ];
mp.colors.quotes.gui        = [ 0x70c0b1, 0x000000 ];
mp.colors.comments.gui      = [ 0x969896, 0x000000 ];
mp.colors.word1.gui         = [ 0x7aa6da, 0x000000 ];
mp.colors.word2.gui         = [ 0xe7c547, 0x000000 ];
mp.colors.word3.gui         = [ 0xe7c547, 0x000000 ];
mp.colors.documentation.gui = [ 0xc397d8, 0x000000 ];
mp.colors.selection.gui     = [ 0xd54e53, 0x000000 ];
*/

/** MP base class **/

global mp_base = {
    title:          NULL,
    name:           'MP',
    keycodes:       {},
    actions:        {},
    keypress:       sub (doc, key)  { doc; },
    render:         sub (doc, opt)  { return []; },
    pre_event:      sub (doc, key)  { key; },
    post_event:     sub (doc, key)  { doc; },
    query_close:    sub (doc)       { 1; },
    get_name:       sub (doc)       { name; },
    get_status:     sub (doc)       { name; },
    busy:           sub (doc, b)    { mp.busy(b); return doc; }
};


/** MP document class **/

global mp_doc = new(mp_base, {
        name:       L("<unnamed>"),
        txt: {
            x:      0,
            y:      0,
            vx:     0,
            vy:     0,
            mod:    0,
            lines:  [ '' ]
        },
        undo_q:     [],
        redo_q:     [],
        syntax:     NULL,

        /* rendering function for MP docs in mp_core.c */
        render:     mp_c.render
    }
);



/** code **/

/**
 * mp.redraw - Triggers a redraw on the next cycle.
 *
 * Triggers a full document redraw in the next cycle.
 */
sub mp.redraw()
{
	/* just increment the redraw trigger */
	mp.redraw_counter++;
}


sub mp.active()
/* returns the active document */
{
    /* if there is a list of pending files to open, do it */
    while (size(mp.active_pending))
        mp.actions.open(NULL, pop(mp.active_pending));

    /* empty document list? create a new, empty one */
    if (size(mp.docs) == 0)
        mp.actions.new();

    /* get active document */
    return mp.docs[mp.active_i];
}


sub mp.process_action(action)
/* processes an action */
{
    local f, doc;

    doc = mp.active();

    f = doc.actions[action] || mp.actions[action];

    if (f != NULL)
        f(doc);
    else {
        mp.message = {
            timeout:    time() + 2,
            string:     sprintf(L("Unknown action '%s'"), action)
        };
    }

    return doc;
}


sub mp.process_event(k)
/* processes a key event */
{
    local doc = mp.active();

    /* notify the document that an event is to be processed */
    k = doc->pre_event(k);

    if (k != NULL) {
        /* get the action or next keycode from the possible sources */
        local a = mp.keycodes_t[k] || doc.keycodes[k] || mp.keycodes[k];

        mp.keycodes_t = NULL;
    
        /* get the action asociated to the keycode */
        if (a != NULL) {
            /* if it's a hash, store for further testing */
            if (is_hash(a))
                mp.keycodes_t = a;
            else {
                /* if it's executable, run it */
                if (is_exec(a))
                    a(doc);
                else
                /* if it's an array, process it sequentially */
                if (is_array(a))
                    foreach(l, a)
                        mp.process_action(l);
                else
                    mp.process_action(doc.keycodes[a] || mp.keycodes[a] || a);
            }
        }
        else
            doc->keypress(k);
    
        mp.shift_pressed = NULL;

        doc->post_event(k);
    }

    return NULL;
}


sub mp.build_status_line()
/* returns the string to be drawn in the status line */
{
    local r;

    if (mp.message) {
        r = mp.message.string;

        /* is the message still active? */
        if (mp.message.timeout <= time())
            mp.message = NULL;
    }
    else
        r = mp.active()->get_status();

    return r;
}


sub mp.backslash_codes(s, d)
/* encodes (d == 0) or decodes (d == 1) backslash codes
   (like \n, \r, etc.) */
{
    local r;

    if (d) {
        r = s->sregex("/[\r\n\t]/g", { "\r" => '\r', "\n" => '\n', "\t" => '\t'});
    }
    else {
        r = s->
            sregex("/\\\\[rnt]/g", { '\r' => "\r", '\n' => "\n", '\t' => "\t"})->
            sregex("/\\\\x\\{[0-9A-Za-z]{4}\\}/g", sub (e) {
                    /* expand \x{HHHH} to character */
                    e = ("0x" ~ e->regex("/[0-9A-Za-z]{4}/")) + 0;
                    chr(e);
                }
            );
    }

    return r;
}


sub mp.long_op(func, a1, a2, a3, a4)
/* executes a potentially long function */
{
	local r;

	mp.busy(1);
	r = func(a1, a2, a3, a4);
	mp.busy(0);

	return r;
}


sub mp.get_history(key)
/* returns a history for the specified key */
{
    return mp.history[key] ||= [];
}


sub mp.menu_label(action)
/* returns a label for the menu for an action */
{
    local l = NULL;

	/* if action is '-', it's a menu separator */
    if (action ne '-') {
        l = L(mp.actdesc[action]) || action;

        local ks = [];

        /* look for this action in mp_doc keycodes */
        foreach (i, mp_doc.keycodes->grep(sub(k, v) {v eq action;})->keys())
            push(ks, i);

        /* look now in global keycodes */
        foreach (i, mp.keycodes->grep(sub(k, v) {v eq action;})->keys())
            push(ks, i);

        if (size(ks))
            l = l ~ ' [' ~ join(ks, ', ') ~ ']';
    }

	return l;
}


sub mp.trim(str, max)
/* trims the string to the last max characters, adding ellipsis if done */
{
	local v = regex(str, '/.{' ~ max ~ '}$/');
	return v && '...' ~ v || str;
}


sub mp.get_doc_names(max)
/* returns an array with the trimmed names of the documents */
{
    mp.docs->map(sub(e) {mp.trim(e->get_name(), (max || 24));});
}


sub mp.usage()
/* set mp.exit_message with an usage message (--help) */
{
	mp.exit_message = 
	sprintf(L(
		"Minimum Profit %s - Programmer Text Editor\n"\
		"Copyright (C) Angel Ortega <angel@triptico.com>\n"\
		"This software is covered by the GPL license. NO WARRANTY.\n"\
		"\n"\
		"Usage: mp-5 [options] [files...]\n"\
		"\n"\
		"Options:\n"\
		"\n"\
		" -t {tag}           Edits the file where tag is defined\n"\
		" -e {mpsl_code}     Executes MPSL code\n"\
		" -f {mpsl_script}   Executes MPSL script file\n"\
		" -d {directory}     Set current directory\n"\
		" -x {file}          Open file in the hexadecimal viewer\n"\
		" -txt               Use text mode instead of GUI\n"\
		" +NNN               Moves to line number NNN of last file\n"\
		"\n"\
		"Homepage: http://triptico.com/software/mp.html\n"
	), mp.VERSION);
}


sub mp.process_cmdline()
/* process the command line arguments (ARGV) */
{
    local o, nl;

    mp.load_tags();

    /* skip ARGV[0] */
    mp.binary_name = shift(ARGV);

    /* files requested to be opened */
    mp.active_pending = [];

    while (o = shift(ARGV)) {
        if (o eq '-h' || o eq '--help') {
            mp.usage();
            mp_c.exit();
            return;
        }
        else
        if (o eq '-e') {
            /* execute code */
            local c = shift(ARGV);

            if (! regex(c, '/;\s*$/'))
                c = c ~ ';';

            eval(c);
        }
        else
        if (o eq '-f') {
            /* execute script */
            local s = shift(ARGV);

            if (stat(s) == NULL)
                ERROR = sprintf(L("Cannot open '%s'"), s);
            else {
                mp.open(s);
                eval(join(mp.active.txt.lines, "\n"));
                mp.close();
            }
        }
        else
        if (o eq '-d')
            chdir(shift(ARGV));
        else
        if (o eq '-t')
            mp.open_tag(shift(ARGV));
        else
        if (o eq '-x') {
            local s = shift(ARGV);

            local d = new(mp_hex_view, { name: s })->init();

            if (d == NULL)
                ERROR = sprintf(L("Cannot open '%s'"), s);
            else
                mp.add(d);
        }
        else
        if (o eq '-txt')
            mp.config.text_mode = 1;
        else
        if (regex(o, '/^\+[0-9]+$/')) {
            nl = o + 0;
        }
        else {
            if (nl) {
                push(mp.active_pending, o ~ ':' ~ nl ~ ':');
                nl = NULL;
            }
            else
                push(mp.active_pending, o);
        }
    }

    if (ERROR) {
        mp.exit_message = ERROR ~ "\n";
        ERROR = NULL;
        mp_c.exit();
        return;
    }

    /* if no files are to be loaded, try a session */
    if (size(mp.active_pending) == 0 && mp.config.auto_sessions) {
        mp.load_session();
    }
}


sub mp.load_profile()
/* loads ~/.mp.mpsl */
{
	/* if /etc/mp.mpsl exists, execute it */
	if (stat('/etc/mp.mpsl') != NULL) {
		eval( sub {
			local INC = [ '/etc' ];
			load('mp.mpsl');
		});
	}

	/* if ~/.mp.mpsl exists, execute it */
	if (ERROR == NULL && stat(HOMEDIR ~ '.mp.mpsl') != NULL) {
		eval( sub {
			local INC = [ HOMEDIR ];
			load(".mp.mpsl");
		});
	}

	/* errors? show in a message */
	if (ERROR != NULL) {
		mp.message = {
			'timeout'	=> time() + 20,
			'string'	=> ERROR
		};

		ERROR = NULL;
	}
}


sub mp.normalize_version(vs)
/* converts a version string to something usable with cmp() */
{
    vs->sregex('/-.+$/')->split('.')->map(sub(e) { sprintf("%03d", e); });
}


sub mp.assert_version(found, minimal, package)
/* asserts that 'found' version of 'package' is at least 'minimal',
   or generate a warning otherwise */
{
	if (cmp(mp.normalize_version(found),
		mp.normalize_version(minimal)) < 0) {
		mp.alert(sprintf(L("WARNING: %s version found is %s, but %s is needed"),
				package, found, minimal));
	}
}


sub mp.test_versions()
/* tests component versions */
{
    local mpdm = MPDM();

    mp.assert_version(mpdm.version, '2.1.4', 'MPDM');
    mp.assert_version(MPSL.VERSION, '2.2.2', 'MPSL');
}


sub mp.exit()
{
    mp_c.exit();
}


/** modules **/

local mp_modules = [
    'drv',
    'move',
    'edit',
    'file',
    'clipboard',
    'search',
    'tags',
    'syntax',
    'macro',
    'templates',
    'spell',
    'misc',
    'crypt',
    'session',
    'build',
    'writing',
    'toys',
    'vcs',
    'dir'
];

foreach (m, mp_modules) {
    eval('load("mp_' ~ m ~ '.mpsl");');

    if (ERROR != NULL)
        mp.exit_message = mp.exit_message ~ ERROR ~ "\n";
}

ERROR = NULL;

/** main **/

mp.load_profile();
mp_drv.startup();
mp.process_cmdline();
mp.test_versions();
mp_drv.main_loop();
mp_drv.shutdown();
