/*
 * Copyright (c) 2002-2016, the original author or authors.
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * http://www.opensource.org/licenses/bsd-license.php
 */
package jline.console;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jline.internal.Log;

/**
 * @author Ståle W. Pedersen <stale.pedersen@jboss.org>
 */
public class ConsoleKeys {

    private KeyMap keys;

    private Map<String, KeyMap> keyMaps;
    private Map<String, String> variables = new HashMap<String,String>();

    public ConsoleKeys(String appName, URL inputrcUrl) {
        keyMaps = KeyMap.keyMaps();
        setVar("editing-mode", "emacs");
        loadKeys(appName, inputrcUrl);
        String editingMode = variables.get("editing-mode");
        if ("vi".equalsIgnoreCase(editingMode)) {
            keys = keyMaps.get(KeyMap.VI_INSERT);
        } else if ("emacs".equalsIgnoreCase(editingMode)) {
            keys = keyMaps.get(KeyMap.EMACS);
        }
    }

    protected boolean setKeyMap (String name) {
        KeyMap map = keyMaps.get(name);
        if (map == null) {
            return false;
        }
        this.keys = map;
        return true;
    }

    protected Map<String, KeyMap> getKeyMaps() {
        return keyMaps;
    }

    protected KeyMap getKeys() {
        return keys;
    }

    protected void setKeys(KeyMap keys) {
        this.keys = keys;
    }

    protected void loadKeys(String appName, URL inputrcUrl) {
        keys = keyMaps.get(KeyMap.EMACS);

        try {
            InputStream input = inputrcUrl.openStream();
            try {
                loadKeys(input, appName);
                Log.debug("Loaded user configuration: ", inputrcUrl);
            }
            finally {
                try {
                    input.close();
                } catch (IOException e) {
                    // Ignore
                }
            }
        }
        catch (IOException e) {
            if (inputrcUrl.getProtocol().equals("file")) {
                File file = new File(inputrcUrl.getPath());
                if (file.exists()) {
                    Log.warn("Unable to read user configuration: ", inputrcUrl, e);
                }
            } else {
                Log.warn("Unable to read user configuration: ", inputrcUrl, e);
            }
        }
    }

    private void loadKeys(InputStream input, String appName) throws IOException {
        BufferedReader reader = new BufferedReader( new java.io.InputStreamReader( input ) );
        String line;
        boolean parsing = true;
        List<Boolean> ifsStack = new ArrayList<Boolean>();
        while ( (line = reader.readLine()) != null ) {
            try {
                line = line.trim();
                if (line.length() == 0) {
                    continue;
                }
                if (line.charAt(0) == '#') {
                    continue;
                }
                int i = 0;
                if (line.charAt(i) == '$') {
                    String cmd;
                    String args;
                    for (++i; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++);
                    int s = i;
                    for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++);
                    cmd = line.substring(s, i);
                    for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++);
                    s = i;
                    for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++);
                    args = line.substring(s, i);
                    if ("if".equalsIgnoreCase(cmd)) {
                        ifsStack.add( parsing );
                        if (!parsing) {
                            continue;
                        }
                        if (args.startsWith("term=")) {
                            // TODO
                        } else if (args.startsWith("mode=")) {
                            String mode = variables.get("editing-mode");
                            parsing = args.substring("mode=".length()).equalsIgnoreCase(mode);
                        } else {
                            parsing = args.equalsIgnoreCase(appName);
                        }
                    } else if ("else".equalsIgnoreCase(cmd)) {
                        if (ifsStack.isEmpty()) {
                            throw new IllegalArgumentException("$else found without matching $if");
                        }
                        boolean invert = true;
                        for (boolean b : ifsStack) {
                            if (!b) {
                                invert = false;
                                break;
                            }
                        }
                        if (invert) {
                            parsing = !parsing;
                        }
                    } else if ("endif".equalsIgnoreCase(cmd)) {
                        if (ifsStack.isEmpty()) {
                            throw new IllegalArgumentException("endif found without matching $if");
                        }
                        parsing = ifsStack.remove( ifsStack.size() - 1 );
                    } else if ("include".equalsIgnoreCase(cmd)) {
                        // TODO
                    }
                    continue;
                }
                if (!parsing) {
                    continue;
                }
                boolean equivalency;
                String keySeq = "";
                if (line.charAt(i++) == '"') {
                    boolean esc = false;
                    for (;; i++) {
                        if (i >= line.length()) {
                            throw new IllegalArgumentException("Missing closing quote on line '" + line + "'");
                        }
                        if (esc) {
                            esc = false;
                        } else if (line.charAt(i) == '\\') {
                            esc = true;
                        } else if (line.charAt(i) == '"') {
                            break;
                        }
                    }
                }
                for (; i < line.length() && line.charAt(i) != ':'
                        && line.charAt(i) != ' ' && line.charAt(i) != '\t'
                        ; i++);
                keySeq = line.substring(0, i);
                equivalency = i + 1 < line.length() && line.charAt(i) == ':' && line.charAt(i + 1) == '=';
                i++;
                if (equivalency) {
                    i++;
                }
                if (keySeq.equalsIgnoreCase("set")) {
                    String key;
                    String val;
                    for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++);
                    int s = i;
                    for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++);
                    key = line.substring( s, i );
                    for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++);
                    s = i;
                    for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++);
                    val = line.substring( s, i );
                    setVar( key, val );
                } else {
                    for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++);
                    int start = i;
                    if (i < line.length() && (line.charAt(i) == '\'' || line.charAt(i) == '\"')) {
                        char delim = line.charAt(i++);
                        boolean esc = false;
                        for (;; i++) {
                            if (i >= line.length()) {
                                break;
                            }
                            if (esc) {
                                esc = false;
                            } else if (line.charAt(i) == '\\') {
                                esc = true;
                            } else if (line.charAt(i) == delim) {
                                break;
                            }
                        }
                    }
                    for (; i < line.length() && line.charAt(i) != ' ' && line.charAt(i) != '\t'; i++);
                    String val = line.substring(Math.min(start, line.length()), Math.min(i, line.length()));
                    if (keySeq.charAt(0) == '"') {
                        keySeq = translateQuoted(keySeq);
                    } else {
                        // Bind key name
                        String keyName = keySeq.lastIndexOf('-') > 0 ? keySeq.substring( keySeq.lastIndexOf('-') + 1 ) : keySeq;
                        char key = getKeyFromName(keyName);
                        keyName = keySeq.toLowerCase();
                        keySeq = "";
                        if (keyName.contains("meta-") || keyName.contains("m-")) {
                            keySeq += "\u001b";
                        }
                        if (keyName.contains("control-") || keyName.contains("c-") || keyName.contains("ctrl-")) {
                            key = (char)(Character.toUpperCase( key ) & 0x1f);
                        }
                        keySeq += key;
                    }
                    if (val.length() > 0 && (val.charAt(0) == '\'' || val.charAt(0) == '\"')) {
                        keys.bind( keySeq, translateQuoted(val) );
                    } else {
                        String operationName = val.replace('-', '_').toUpperCase();
                        try {
                          keys.bind(keySeq, Operation.valueOf(operationName));
                        } catch(IllegalArgumentException e) {
                          Log.info("Unable to bind key for unsupported operation: ", val);
                        }
                    }
                }
            } catch (IllegalArgumentException e) {
              Log.warn("Unable to parse user configuration: ", e);
            }
        }
    }

    private static String translateQuoted(String keySeq) {
        int i;
        String str = keySeq.substring( 1, keySeq.length() - 1 );
        keySeq = "";
        for (i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (c == '\\') {
                boolean ctrl = str.regionMatches(i, "\\C-", 0, 3)|| str.regionMatches(i, "\\M-\\C-", 0, 6);
                boolean meta = str.regionMatches(i, "\\M-", 0, 3)|| str.regionMatches(i, "\\C-\\M-", 0, 6);
                i += (meta ? 3 : 0) + (ctrl ? 3 : 0) + (!meta && !ctrl ? 1 : 0);
                if (i >= str.length()) {
                    break;
                }
                c = str.charAt(i);
                if (meta) {
                    keySeq += "\u001b";
                }
                if (ctrl) {
                    c = c == '?' ? 0x7f : (char)(Character.toUpperCase( c ) & 0x1f);
                }
                if (!meta && !ctrl) {
                    switch (c) {
                        case 'a': c = 0x07; break;
                        case 'b': c = '\b'; break;
                        case 'd': c = 0x7f; break;
                        case 'e': c = 0x1b; break;
                        case 'f': c = '\f'; break;
                        case 'n': c = '\n'; break;
                        case 'r': c = '\r'; break;
                        case 't': c = '\t'; break;
                        case 'v': c = 0x0b; break;
                        case '\\': c = '\\'; break;
                        case '0': case '1': case '2': case '3':
                        case '4': case '5': case '6': case '7':
                            c = 0;
                            for (int j = 0; j < 3; j++, i++) {
                                if (i >= str.length()) {
                                    break;
                                }
                                int k = Character.digit(str.charAt(i), 8);
                                if (k < 0) {
                                    break;
                                }
                                c = (char)(c * 8 + k);
                            }
                            c &= 0xFF;
                            break;
                        case 'x':
                            i++;
                            c = 0;
                            for (int j = 0; j < 2; j++, i++) {
                                if (i >= str.length()) {
                                    break;
                                }
                                int k = Character.digit(str.charAt(i), 16);
                                if (k < 0) {
                                    break;
                                }
                                c = (char)(c * 16 + k);
                            }
                            c &= 0xFF;
                            break;
                        case 'u':
                            i++;
                            c = 0;
                            for (int j = 0; j < 4; j++, i++) {
                                if (i >= str.length()) {
                                    break;
                                }
                                int k = Character.digit(str.charAt(i), 16);
                                if (k < 0) {
                                    break;
                                }
                                c = (char)(c * 16 + k);
                            }
                            break;
                    }
                }
                keySeq += c;
            } else {
                keySeq += c;
            }
        }
        return keySeq;
    }

    private static char getKeyFromName(String name) {
        if ("DEL".equalsIgnoreCase(name) || "Rubout".equalsIgnoreCase(name)) {
            return 0x7f;
        } else if ("ESC".equalsIgnoreCase(name) || "Escape".equalsIgnoreCase(name)) {
            return '\033';
        } else if ("LFD".equalsIgnoreCase(name) || "NewLine".equalsIgnoreCase(name)) {
            return '\n';
        } else if ("RET".equalsIgnoreCase(name) || "Return".equalsIgnoreCase(name)) {
            return '\r';
        } else if ("SPC".equalsIgnoreCase(name) || "Space".equalsIgnoreCase(name)) {
            return ' ';
        } else if ("Tab".equalsIgnoreCase(name)) {
            return '\t';
        } else {
            return name.charAt(0);
        }
    }

    private void setVar(String key, String val) {
        if ("keymap".equalsIgnoreCase(key)) {
            if (keyMaps.containsKey(val)) {
                keys = keyMaps.get(val);
            }
        } else if ("blink-matching-paren".equals(key)) {
            if ("on".equalsIgnoreCase(val)) {
              keys.setBlinkMatchingParen(true);
            } else if ("off".equalsIgnoreCase(val)) {
              keys.setBlinkMatchingParen(false);
            }
        }

        /*
         * Technically variables should be defined as a functor class
         * so that validation on the variable value can be done at parse
         * time. This is a stop-gap.
         */
        variables.put(key, val);
    }

    /**
     * Retrieves the value of a variable that was set in the .inputrc file
     * during processing
     * @param var The variable name
     * @return The variable value.
     */
    public String getVariable(String var) {
        return variables.get (var);
    }
}
