/*
 * Copyright (c) 2002-2016, the original author(s).
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * https://opensource.org/licenses/BSD-3-Clause
 */
package org.jline.reader.impl;

import java.util.ListIterator;
import org.jline.reader.Expander;
import org.jline.reader.History;
import org.jline.reader.History.Entry;

public class DefaultExpander implements Expander {

    /**
     * Expand event designator such as !!, !#, !3, etc...
     * See http://www.gnu.org/software/bash/manual/html_node/Event-Designators.html
     */
    @SuppressWarnings("fallthrough")
    @Override
    public String expandHistory(History history, String line) {
        boolean inQuote = false;
        StringBuilder sb = new StringBuilder();
        boolean escaped = false;
        int unicode = 0;
        for (int i = 0; i < line.length(); i++) {
            char c = line.charAt(i);
            if (unicode > 0) {
                escaped = (--unicode >= 0);
                sb.append(c);
            } else if (escaped) {
                if (c == 'u') {
                    unicode = 4;
                } else {
                    escaped = false;
                }
                sb.append(c);
            } else if (c == '\'') {
                inQuote = !inQuote;
                sb.append(c);
            } else if (inQuote) {
                sb.append(c);
            } else {
                switch (c) {
                    case '\\':
                        // any '\!' should be considered an expansion escape, so skip expansion and strip the escape
                        // character
                        // a leading '\^' should be considered an expansion escape, so skip expansion and strip the
                        // escape character
                        // otherwise, add the escape
                        escaped = true;
                        sb.append(c);
                        break;
                    case '!':
                        if (i + 1 < line.length()) {
                            c = line.charAt(++i);
                            boolean neg = false;
                            String rep = null;
                            int i1, idx;
                            switch (c) {
                                case '!':
                                    if (history.size() == 0) {
                                        throw new IllegalArgumentException("!!: event not found");
                                    }
                                    rep = history.get(history.index() - 1);
                                    break;
                                case '#':
                                    sb.append(sb);
                                    break;
                                case '?':
                                    i1 = line.indexOf('?', i + 1);
                                    if (i1 < 0) {
                                        i1 = line.length();
                                    }
                                    String sc = line.substring(i + 1, i1);
                                    i = i1;
                                    idx = searchBackwards(history, sc, history.index(), false);
                                    if (idx < 0) {
                                        throw new IllegalArgumentException("!?" + sc + ": event not found");
                                    } else {
                                        rep = history.get(idx);
                                    }
                                    break;
                                case '$':
                                    if (history.size() == 0) {
                                        throw new IllegalArgumentException("!$: event not found");
                                    }
                                    String previous =
                                            history.get(history.index() - 1).trim();
                                    int lastSpace = previous.lastIndexOf(' ');
                                    if (lastSpace != -1) {
                                        rep = previous.substring(lastSpace + 1);
                                    } else {
                                        rep = previous;
                                    }
                                    break;
                                case ' ':
                                case '\t':
                                    sb.append('!');
                                    sb.append(c);
                                    break;
                                case '-':
                                    neg = true;
                                    i++;
                                    // fall through
                                case '0':
                                case '1':
                                case '2':
                                case '3':
                                case '4':
                                case '5':
                                case '6':
                                case '7':
                                case '8':
                                case '9':
                                    i1 = i;
                                    for (; i < line.length(); i++) {
                                        c = line.charAt(i);
                                        if (c < '0' || c > '9') {
                                            break;
                                        }
                                    }
                                    try {
                                        idx = Integer.parseInt(line.substring(i1, i));
                                    } catch (NumberFormatException e) {
                                        throw new IllegalArgumentException(
                                                (neg ? "!-" : "!") + line.substring(i1, i) + ": event not found");
                                    }
                                    if (neg && idx > 0 && idx <= history.size()) {
                                        rep = history.get(history.index() - idx);
                                    } else if (!neg
                                            && idx > history.index() - history.size()
                                            && idx <= history.index()) {
                                        rep = history.get(idx - 1);
                                    } else {
                                        throw new IllegalArgumentException(
                                                (neg ? "!-" : "!") + line.substring(i1, i) + ": event not found");
                                    }
                                    break;
                                default:
                                    String ss = line.substring(i);
                                    i = line.length();
                                    idx = searchBackwards(history, ss, history.index(), true);
                                    if (idx < 0) {
                                        throw new IllegalArgumentException("!" + ss + ": event not found");
                                    } else {
                                        rep = history.get(idx);
                                    }
                                    break;
                            }
                            if (rep != null) {
                                sb.append(rep);
                            }
                        } else {
                            sb.append(c);
                        }
                        break;
                    case '^':
                        if (i == 0) {
                            int i1 = line.indexOf('^', i + 1);
                            int i2 = line.indexOf('^', i1 + 1);
                            if (i2 < 0) {
                                i2 = line.length();
                            }
                            if (i1 > 0 && i2 > 0) {
                                String s1 = line.substring(i + 1, i1);
                                String s2 = line.substring(i1 + 1, i2);
                                String s = history.get(history.index() - 1).replace(s1, s2);
                                sb.append(s);
                                i = i2 + 1;
                                break;
                            }
                        }
                        sb.append(c);
                        break;
                    default:
                        sb.append(c);
                        break;
                }
            }
        }
        return sb.toString();
    }

    @Override
    public String expandVar(String word) {
        return word;
    }

    protected int searchBackwards(History history, String searchTerm, int startIndex, boolean startsWith) {
        ListIterator<Entry> it = history.iterator(startIndex);
        while (it.hasPrevious()) {
            Entry e = it.previous();
            if (startsWith) {
                if (e.line().startsWith(searchTerm)) {
                    return e.index();
                }
            } else {
                if (e.line().contains(searchTerm)) {
                    return e.index();
                }
            }
        }
        return -1;
    }
}
