/*
 * Copyright (c) 2002-2018, 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.utils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Attributed string builder
 *
 * @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
 */
public class AttributedStringBuilder extends AttributedCharSequence implements Appendable {

    private char[] buffer;
    private long[] style;
    private int length;
    private TabStops tabs = new TabStops(0);
    private char[] altIn;
    private char[] altOut;
    private boolean inAltCharset;
    private int lastLineLength = 0;
    private AttributedStyle current = AttributedStyle.DEFAULT;

    public AttributedStringBuilder() {
        this(64);
    }

    public AttributedStringBuilder(int capacity) {
        buffer = new char[capacity];
        style = new long[capacity];
        length = 0;
    }

    public static AttributedString append(CharSequence... strings) {
        AttributedStringBuilder sb = new AttributedStringBuilder();
        for (CharSequence s : strings) {
            sb.append(s);
        }
        return sb.toAttributedString();
    }

    private static boolean equals(char[] a, int aFromIndex, char[] b, int bFromIndex, int length) {
        if (aFromIndex < 0 || bFromIndex < 0 || aFromIndex + length > a.length || bFromIndex + length > b.length) {
            return false;
        }
        for (int i = 0; i < length; i++) {
            if (a[aFromIndex + i] != b[bFromIndex + i]) {
                return false;
            }
        }
        return true;
    }

    @Override
    public int length() {
        return length;
    }

    @Override
    public char charAt(int index) {
        return buffer[index];
    }

    @Override
    public AttributedStyle styleAt(int index) {
        return new AttributedStyle(style[index], style[index]);
    }

    @Override
    long styleCodeAt(int index) {
        return style[index];
    }

    @Override
    protected char[] buffer() {
        return buffer;
    }

    @Override
    protected int offset() {
        return 0;
    }

    @Override
    public AttributedString subSequence(int start, int end) {
        return new AttributedString(
                Arrays.copyOfRange(buffer, start, end), Arrays.copyOfRange(style, start, end), 0, end - start);
    }

    @Override
    public AttributedStringBuilder append(CharSequence csq) {
        if (csq == null) {
            csq = "null"; // Required by Appendable.append
        }
        return append(new AttributedString(csq, current));
    }

    @Override
    public AttributedStringBuilder append(CharSequence csq, int start, int end) {
        if (csq == null) {
            csq = "null"; // Required by Appendable.append
        }
        return append(csq.subSequence(start, end));
    }

    @Override
    public AttributedStringBuilder append(char c) {
        return append(Character.toString(c));
    }

    public AttributedStringBuilder append(char c, int repeat) {
        AttributedString s = new AttributedString(Character.toString(c), current);
        while (repeat-- > 0) {
            append(s);
        }
        return this;
    }

    public AttributedStringBuilder append(CharSequence csq, AttributedStyle style) {
        return append(new AttributedString(csq, style));
    }

    public AttributedStringBuilder style(AttributedStyle style) {
        current = style;
        return this;
    }

    public AttributedStringBuilder style(Function<AttributedStyle, AttributedStyle> style) {
        current = style.apply(current);
        return this;
    }

    public AttributedStringBuilder styled(Function<AttributedStyle, AttributedStyle> style, CharSequence cs) {
        return styled(style, sb -> sb.append(cs));
    }

    public AttributedStringBuilder styled(AttributedStyle style, CharSequence cs) {
        return styled(s -> style, sb -> sb.append(cs));
    }

    public AttributedStringBuilder styled(
            Function<AttributedStyle, AttributedStyle> style, Consumer<AttributedStringBuilder> consumer) {
        AttributedStyle prev = current;
        current = style.apply(prev);
        consumer.accept(this);
        current = prev;
        return this;
    }

    public AttributedStyle style() {
        return current;
    }

    public AttributedStringBuilder append(AttributedString str) {
        return append((AttributedCharSequence) str, 0, str.length());
    }

    public AttributedStringBuilder append(AttributedString str, int start, int end) {
        return append((AttributedCharSequence) str, start, end);
    }

    public AttributedStringBuilder append(AttributedCharSequence str) {
        return append(str, 0, str.length());
    }

    public AttributedStringBuilder append(AttributedCharSequence str, int start, int end) {
        ensureCapacity(length + end - start);
        for (int i = start; i < end; i++) {
            char c = str.charAt(i);
            long s = str.styleCodeAt(i) & ~current.getMask() | current.getStyle();
            if (tabs.defined() && c == '\t') {
                insertTab(new AttributedStyle(s, 0));
            } else {
                ensureCapacity(length + 1);
                buffer[length] = c;
                style[length] = s;
                if (c == '\n') {
                    lastLineLength = 0;
                } else {
                    lastLineLength++;
                }
                length++;
            }
        }
        return this;
    }

    protected void ensureCapacity(int nl) {
        if (nl > buffer.length) {
            int s = Math.max(buffer.length, 1);
            while (s <= nl) {
                s *= 2;
            }
            buffer = Arrays.copyOf(buffer, s);
            style = Arrays.copyOf(style, s);
        }
    }

    public void appendAnsi(String ansi) {
        ansiAppend(ansi);
    }

    public AttributedStringBuilder ansiAppend(String ansi) {
        int ansiStart = 0;
        int ansiState = 0;
        ensureCapacity(length + ansi.length());
        for (int i = 0; i < ansi.length(); i++) {
            char c = ansi.charAt(i);
            if (ansiState == 0 && c == 27) {
                ansiState++;
            } else if (ansiState == 1 && c == '[') {
                ansiState++;
                ansiStart = i + 1;
            } else if (ansiState == 2) {
                if (c == 'm') {
                    String[] params = ansi.substring(ansiStart, i).split(";");
                    int j = 0;
                    while (j < params.length) {
                        int ansiParam = params[j].isEmpty() ? 0 : Integer.parseInt(params[j]);
                        switch (ansiParam) {
                            case 0:
                                current = AttributedStyle.DEFAULT;
                                break;
                            case 1:
                                current = current.bold();
                                break;
                            case 2:
                                current = current.faint();
                                break;
                            case 3:
                                current = current.italic();
                                break;
                            case 4:
                                current = current.underline();
                                break;
                            case 5:
                                current = current.blink();
                                break;
                            case 7:
                                current = current.inverse();
                                break;
                            case 8:
                                current = current.conceal();
                                break;
                            case 9:
                                current = current.crossedOut();
                                break;
                            case 22:
                                current = current.boldOff().faintOff();
                                break;
                            case 23:
                                current = current.italicOff();
                                break;
                            case 24:
                                current = current.underlineOff();
                                break;
                            case 25:
                                current = current.blinkOff();
                                break;
                            case 27:
                                current = current.inverseOff();
                                break;
                            case 28:
                                current = current.concealOff();
                                break;
                            case 29:
                                current = current.crossedOutOff();
                                break;
                            case 30:
                            case 31:
                            case 32:
                            case 33:
                            case 34:
                            case 35:
                            case 36:
                            case 37:
                                current = current.foreground(ansiParam - 30);
                                break;
                            case 39:
                                current = current.foregroundOff();
                                break;
                            case 40:
                            case 41:
                            case 42:
                            case 43:
                            case 44:
                            case 45:
                            case 46:
                            case 47:
                                current = current.background(ansiParam - 40);
                                break;
                            case 49:
                                current = current.backgroundOff();
                                break;
                            case 38:
                            case 48:
                                if (j + 1 < params.length) {
                                    int ansiParam2 = Integer.parseInt(params[++j]);
                                    if (ansiParam2 == 2) {
                                        if (j + 3 < params.length) {
                                            int r = Integer.parseInt(params[++j]);
                                            int g = Integer.parseInt(params[++j]);
                                            int b = Integer.parseInt(params[++j]);
                                            if (ansiParam == 38) {
                                                current = current.foreground(r, g, b);
                                            } else {
                                                current = current.background(r, g, b);
                                            }
                                        }
                                    } else if (ansiParam2 == 5) {
                                        if (j + 1 < params.length) {
                                            int col = Integer.parseInt(params[++j]);
                                            if (ansiParam == 38) {
                                                current = current.foreground(col);
                                            } else {
                                                current = current.background(col);
                                            }
                                        }
                                    }
                                }
                                break;
                            case 90:
                            case 91:
                            case 92:
                            case 93:
                            case 94:
                            case 95:
                            case 96:
                            case 97:
                                current = current.foreground(ansiParam - 90 + 8);
                                break;
                            case 100:
                            case 101:
                            case 102:
                            case 103:
                            case 104:
                            case 105:
                            case 106:
                            case 107:
                                current = current.background(ansiParam - 100 + 8);
                                break;
                        }
                        j++;
                    }
                    ansiState = 0;
                } else if (!(c >= '0' && c <= '9' || c == ';')) {
                    // This is not a SGR code, so ignore
                    ansiState = 0;
                }
            } else {
                if (ansiState >= 1) {
                    ensureCapacity(length + 1);
                    buffer[length++] = 27;
                    if (ansiState >= 2) {
                        ensureCapacity(length + 1);
                        buffer[length++] = '[';
                    }
                    ansiState = 0;
                }
                if (c == '\t' && tabs.defined()) {
                    insertTab(current);
                } else {
                    ensureCapacity(length + 1);
                    if (inAltCharset) {
                        switch (c) {
                            case 'j':
                                c = '┘';
                                break;
                            case 'k':
                                c = '┐';
                                break;
                            case 'l':
                                c = '┌';
                                break;
                            case 'm':
                                c = '└';
                                break;
                            case 'n':
                                c = '┼';
                                break;
                            case 'q':
                                c = '─';
                                break;
                            case 't':
                                c = '├';
                                break;
                            case 'u':
                                c = '┤';
                                break;
                            case 'v':
                                c = '┴';
                                break;
                            case 'w':
                                c = '┬';
                                break;
                            case 'x':
                                c = '│';
                                break;
                        }
                    }
                    buffer[length] = c;
                    style[length] = this.current.getStyle();
                    if (c == '\n') {
                        lastLineLength = 0;
                    } else {
                        lastLineLength++;
                    }
                    length++;
                    if (altIn != null && altOut != null) {
                        char[] alt = inAltCharset ? altOut : altIn;
                        if (equals(buffer, length - alt.length, alt, 0, alt.length)) {
                            inAltCharset = !inAltCharset;
                            length -= alt.length;
                        }
                    }
                }
            }
        }
        return this;
    }

    protected void insertTab(AttributedStyle s) {
        int nb = tabs.spaces(lastLineLength);
        ensureCapacity(length + nb);
        for (int i = 0; i < nb; i++) {
            buffer[length] = ' ';
            style[length] = s.getStyle();
            length++;
        }
        lastLineLength += nb;
    }

    public void setLength(int l) {
        length = l;
    }

    /**
     * Set the number of spaces a tab is expanded to. Tab size cannot be changed
     * after text has been added to prevent inconsistent indentation.
     * <p>
     * If tab size is set to 0, tabs are not expanded (the default).
     *
     * @param tabsize Spaces per tab or 0 for no tab expansion. Must be non-negative
     * @return this
     */
    public AttributedStringBuilder tabs(int tabsize) {
        if (tabsize < 0) {
            throw new IllegalArgumentException("Tab size must be non negative");
        }
        return tabs(List.of(tabsize));
    }

    public AttributedStringBuilder tabs(List<Integer> tabs) {
        if (length > 0) {
            throw new IllegalStateException("Cannot change tab size after appending text");
        }
        this.tabs = new TabStops(tabs);
        return this;
    }

    public AttributedStringBuilder altCharset(String altIn, String altOut) {
        if (length > 0) {
            throw new IllegalStateException("Cannot change alternative charset after appending text");
        }
        this.altIn = altIn != null ? altIn.toCharArray() : null;
        this.altOut = altOut != null ? altOut.toCharArray() : null;
        return this;
    }

    public AttributedStringBuilder styleMatches(Pattern pattern, AttributedStyle s) {
        Matcher matcher = pattern.matcher(this);
        while (matcher.find()) {
            for (int i = matcher.start(); i < matcher.end(); i++) {
                style[i] = (style[i] & ~s.getMask()) | s.getStyle();
            }
        }
        return this;
    }

    public AttributedStringBuilder styleMatches(Pattern pattern, List<AttributedStyle> styles) {
        Matcher matcher = pattern.matcher(this);
        while (matcher.find()) {
            for (int group = 0; group < matcher.groupCount(); group++) {
                AttributedStyle s = styles.get(group);
                for (int i = matcher.start(group + 1); i < matcher.end(group + 1); i++) {
                    style[i] = (style[i] & ~s.getMask()) | s.getStyle();
                }
            }
        }
        return this;
    }

    private static class TabStops {
        private List<Integer> tabs = new ArrayList<>();
        private int lastStop = 0;
        private int lastSize = 0;

        public TabStops(int tabs) {
            this.lastSize = tabs;
        }

        public TabStops(List<Integer> tabs) {
            this.tabs = tabs;
            int p = 0;
            for (int s : tabs) {
                if (s <= p) {
                    continue;
                }
                lastStop = s;
                lastSize = s - p;
                p = s;
            }
        }

        boolean defined() {
            return lastSize > 0;
        }

        int spaces(int lastLineLength) {
            int out = 0;
            if (lastLineLength >= lastStop) {
                out = lastSize - (lastLineLength - lastStop) % lastSize;
            } else {
                for (int s : tabs) {
                    if (s > lastLineLength) {
                        out = s - lastLineLength;
                        break;
                    }
                }
            }
            return out;
        }
    }
}
