/*
 * $Id$
 *
 * Copyright (C) 2003-2015 JNode.org
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library 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 Lesser General Public 
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; If not, write to the Free Software Foundation, Inc., 
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
 
package org.jnode.apps.editor;

import java.awt.event.KeyEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.List;
import org.jnode.driver.console.ConsoleManager;
import org.jnode.driver.console.TextConsole;
import org.jnode.driver.console.textscreen.TextScreenConsoleManager;
import org.jnode.driver.input.KeyboardEvent;
import org.jnode.driver.input.KeyboardListener;
import org.jnode.naming.InitialNaming;
import org.jnode.shell.ShellManager;

/**
 * @author Levente S\u00e1ntha
 */
public class TextEditor implements KeyboardListener {
    private int cx, cy, sh, sw, cxm, cym, fx, fy;
    private List<StringBuilder> ls;
    private TextConsole console;
    private File file;
    private boolean ro = false;

    public static void main(String[] argv) throws Exception {
        if (argv.length == 0) {
            System.out.println("No file specified");
            return;
        }

        ShellManager sm = InitialNaming.lookup(ShellManager.NAME);
        TextScreenConsoleManager manager = (TextScreenConsoleManager) sm.getCurrentShell().getConsole().getManager();
        TextConsole console = manager.createConsole(
            "editor",
            (ConsoleManager.CreateOptions.TEXT |
                ConsoleManager.CreateOptions.STACKED |
                ConsoleManager.CreateOptions.NO_LINE_EDITTING |
                ConsoleManager.CreateOptions.NO_SYSTEM_OUT_ERR));
        try {
            manager.focus(console);

            TextEditor te = new TextEditor(console);
            File f = new File(argv[0]);
            te.ro = argv.length == 2 && "ro".equals(argv[1]);
            te.loadFile(f);
        } catch (Exception e) {
            manager.unregisterConsole(console);
            throw e;
        }
    }

    public TextEditor(TextConsole console) {
        this.console = console;
        console.addKeyboardListener(this);
        this.console.setCursorVisible(true);
        cx = 0;
        cy = 0;
        sh = console.getHeight();
        sw = console.getWidth();
        cxm = sw - 1;
        cym = sh - 1 - 1;
        fx = 0;
        fy = 0;
        ls = new ArrayList<StringBuilder>();
    }

    public void loadFile(File f) throws IOException {
        ls.clear();
        if (f.exists()) {
            BufferedReader br = new BufferedReader(new FileReader(f));
            int c;
            StringBuilder sb = new StringBuilder();
            while ((c = br.read()) != -1) {
                if (c == '\n') {
                    sb.append((char) c);
                    ls.add(sb);
                    sb = new StringBuilder();
                } else {
                    sb.append((char) c);
                }
            }
            if (sb.length() > 0)
                ls.add(sb);
        }

        file = f;
        updateScreen();
    }

    private String message;

    private void saveFile() {
        if (file == null) {
            System.out.println("No file.");
            return;
        }
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                try {
                    StringBuilder buf = new StringBuilder();
                    FileWriter fw = new FileWriter(file);

                    for (StringBuilder sb : ls)
                        buf.append(sb);

                    fw.write(buf.toString());
                    fw.flush();
                    fw.close();
                    message = "Saved " + file;
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }
        });
    }


    private void updateScreen() {
        char[] data = new char[sw * (cym + 1 + 1)];
        int k = 0;
        StringBuilder e = new StringBuilder();
        List<StringBuilder> rl = new ArrayList<StringBuilder>();
        for (int i = 0; i < cym + 1; i++) {
            StringBuilder l = (fy + i < ls.size()) ? ls.get(fy + i) : e;
            StringBuilder sb = new StringBuilder();
            for (int j = 0; j < l.length(); j++) {
                char c = l.charAt(j);
                if (c == '\t') {
                    sb.append(' ');
                    sb.append(' ');
                    sb.append(' ');
                    sb.append(' ');
                } else if (c == '\n') {
                    sb.append(' ');
                } else {
                    sb.append(c);
                }
            }
            rl.add(sb);
        }

        int tab_count = 0;
        if (ls.size() > 0) {
            CharSequence l = ls.get(oy()).subSequence(0, fx + cx);
            for (int i = 0; i < l.length(); i++) {
                if (l.charAt(i) == '\t') tab_count++;
            }
        }

        int tx = fx + cx + 3 * tab_count;
        int cx2 = cx + 3 * tab_count;
        int fx2 = fx;
        if (tx >= sw) {
            cx2 = sw - 1;
            fx2 = tx - cx2;
        }

        for (int i = 0; i < cym + 1; i++) {
            StringBuilder l = rl.get(i);
            for (int j = 0; j < sw; j++) {
                char c = (fx2 + j < l.length()) ? l.charAt(fx2 + j) : ' ';
                data[k++] = c;
            }
        }

        if (message != null) {
            e.append(message);
            message = null;
        } else {
            e.append(ro ? "VIEW: " : "EDIT: ").append(file.getName()).
                append(" [").append(fx2 + cx2 + 1).append(",").append(oy() + 1).append("]");
        }
        for (int j = 0; j < sw; j++) {
            data[k++] = (j < e.length()) ? e.charAt(j) : ' ';
        }
        console.setChar(0, 0, data, 7);
        console.setCursor(cx2, cy);
    }

    public void keyPressed(KeyboardEvent e) {
        int k = e.getKeyCode();
        if (ro ^ e.isControlDown()) {
            switch (k) {
                case KeyEvent.VK_UP: {
                    if (fy > 0) fy--;
                    break;
                }
                case KeyEvent.VK_DOWN: {
                    if (fy < my() - cym) fy++;
                    break;
                }
                case KeyEvent.VK_LEFT: {
                    if (fx > 0) fx--;
                    break;
                }
                case KeyEvent.VK_RIGHT: {
                    if (fx < mx()) fx++;
                    break;
                }
                case KeyEvent.VK_HOME: {
                    cx = fx = cy = fy = 0;
                    break;
                }
                case KeyEvent.VK_END: {
                    if (my() < cym) cy = my();
                    else {
                        cy = cym;
                        fy = my() - cy;
                    }
                    end();
                    break;
                }
                case KeyEvent.VK_Y: {
                    if (ro) break;
                    if (oy() >= 0 && oy() <= my()) {
                        ls.remove(oy());
                        if (cy > 0 && oy() == my() + 1) cy--;
                        else if (fy > 0) fy--;
                    }
                    break;
                }
                case KeyEvent.VK_S:
                case KeyEvent.VK_F2: {
                    if (ro) break;
                    saveFile();
                    break;
                }
                case KeyEvent.VK_Q:
                case KeyEvent.VK_F10: {
                    console.getManager().unregisterConsole(console);
                    break;
                }

                //same as without control key down
                case KeyEvent.VK_PAGE_UP: {
                    if (fy > cym) fy -= cym;
                    else fy = 0;
                    break;
                }
                case KeyEvent.VK_PAGE_DOWN: {
                    if (my() - fy > cym) fy += cym;
                    break;
                }

            }
        } else {
        out:
            switch (k) {
                case KeyEvent.VK_UP: {
                    cursorUp();
                    break;
                }
                case KeyEvent.VK_DOWN: {
                    cursorDown();
                    break;
                }
                case KeyEvent.VK_LEFT: {
                    if (cx > 0) cx--;
                    else if (fx > 0) fx--;
                    else if (cursorUp()) end();
                    break;
                }
                case KeyEvent.VK_RIGHT: {
                    if (ox() < mx()) {
                        if (cx < cxm) cx++;
                        else fx++;
                    } else if (cursorDown()) {
                        cx = 0;
                        fx = 0;
                    }
                    break;
                }
                case KeyEvent.VK_HOME: {
                    cx = fx = 0;
                    break;
                }
                case KeyEvent.VK_END: {
                    end();
                    break;
                }

                //same as with control key down
                case KeyEvent.VK_PAGE_UP: {
                    if (fy > cym) fy -= cym;
                    else fy = 0;
                    break;
                }
                case KeyEvent.VK_PAGE_DOWN: {
                    if (my() - fy > cym) fy += cym;
                    break;
                }
                default:
                    if (ro) break;

                    char c = e.getKeyChar();
                    if (c == KeyEvent.CHAR_UNDEFINED && k != KeyEvent.VK_DELETE) return;
                    if (cy == ls.size()) ls.add(new StringBuilder());
                    StringBuilder l = ls.get(oy());
                    switch (k) {
                        case KeyEvent.VK_ENTER: {
                            c = '\n';
                            ls.add(oy() + 1, new StringBuilder());
                            break;
                        }
                        case KeyEvent.VK_DELETE: {
                            if (cx == mx()) {
                                if (cy < my()) {
                                    l.deleteCharAt(l.length() - 1).append(ls.remove(oy() + 1));
                                }
                            } else if (cx < mx())
                                l.deleteCharAt(ox());
                            break out;
                        }
                        case KeyEvent.VK_BACK_SPACE: {
                            if (ox() == 0) {
                                if (cy > 0) {
                                    StringBuilder l2 = ls.get(oy() - 1);
                                    cx = l2.length() - 1;
                                    l2.deleteCharAt(l2.length() - 1).append(ls.remove(oy()));
                                    cy--;
                                } else if (fy > 0) {
                                    StringBuilder l2 = ls.get(oy() - 1);
                                    cx = l2.length() - 1;
                                    l2.deleteCharAt(l2.length() - 1).append(ls.remove(oy()));
                                    fy--;
                                }
                            } else if (cx > 0) {
                                cx--;
                                l.deleteCharAt(ox());
                            } else {
                                fx--;
                                l.deleteCharAt(ox());
                            }
                            break out;
                        }

                    }
                    if (cx == l.length()) l.append(c);
                    else l.insert(ox(), c);
                    if (c == '\n') {
                        if (cy < cym) cy++;
                        else fy++;
                        if (cx < l.length() + 1) {
                            StringBuilder l2 = ls.get(oy());
                            l2.append(l, ox() + 1, l.length());
                            l.delete(ox() + 1, l.length());
                        }
                        cx = fx = 0;
                    } else {
                        if (cx == cxm) fx++;
                        else cx++;
                    }
            }
        }
        if (oy() > cym && cy < 0) {
            cy = cym;
            fy = my() - cy;
        }
        if (oy() > my()) {
            fy -= oy() - my();
            fy = fy < 0 ? 0 : fy;
        }
        if (ox() > mx()) {
            if (mx() > cxm) {
                cx = cxm;
                fx = mx() - cx;
            } else {
                cx = mx();
                fx = 0;
            }
        }
        e.consume();
        updateScreen();
    }

    private void end() {
        if (mx() < cxm)
            cx = mx();
        else {
            cx = cxm;
            fx = mx() - cx;
        }
    }

    private boolean cursorUp() {
        boolean ret;
        if (ret = (cy > 0))
            cy--;
        else if (ret = (fy > 0))
            fy--;
        return ret;
    }

    private boolean cursorDown() {
        boolean ret;
        if (ret = (oy() < my()))
            if (cy < cym) cy++;
            else fy++;
        return ret;
    }

    private int oy() {
        return fy + cy;
    }

    private int ox() {
        return fx + cx;
    }

    private int my() {
        return ls.size() - 1;
    }

    private int mx() {
        return ls.size() == 0 ? 0 : ls.get(oy()).length() - (oy() == my() ? 0 : 1);
    }

    public void keyReleased(KeyboardEvent event) {
    }
}
