/*
 * 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.utils;

import java.security.InvalidParameterException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jline.terminal.Terminal;

/**
 * Attributed string.
 * Instances of this class are immutables.
 * Substrings are created without any memory copy.
 *
 * @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
 */
public class AttributedString extends AttributedCharSequence {

    public static final AttributedString EMPTY = new AttributedString("");
    public static final AttributedString NEWLINE = new AttributedString("\n");
    final char[] buffer;
    final long[] style;
    final int start;
    final int end;

    public AttributedString(CharSequence str) {
        this(str, 0, str.length(), null);
    }

    public AttributedString(CharSequence str, int start, int end) {
        this(str, start, end, null);
    }

    public AttributedString(CharSequence str, AttributedStyle s) {
        this(str, 0, str.length(), s);
    }

    public AttributedString(CharSequence str, int start, int end, AttributedStyle s) {
        if (end < start) {
            throw new InvalidParameterException();
        }
        if (str instanceof AttributedString as) {
            this.buffer = as.buffer;
            if (s != null) {
                this.style = as.style.clone();
                for (int i = 0; i < style.length; i++) {
                    this.style[i] = (this.style[i] & ~s.getMask()) | s.getStyle();
                }
            } else {
                this.style = as.style;
            }
            this.start = as.start + start;
            this.end = as.start + end;
        } else if (str instanceof AttributedStringBuilder asb) {
            AttributedString as = asb.subSequence(start, end);
            this.buffer = as.buffer;
            this.style = as.style;
            if (s != null) {
                for (int i = 0; i < style.length; i++) {
                    this.style[i] = (this.style[i] & ~s.getMask()) | s.getStyle();
                }
            }
            this.start = as.start;
            this.end = as.end;
        } else {
            int l = end - start;
            buffer = new char[l];
            for (int i = 0; i < l; i++) {
                buffer[i] = str.charAt(start + i);
            }
            style = new long[l];
            if (s != null) {
                Arrays.fill(style, s.getStyle());
            }
            this.start = 0;
            this.end = l;
        }
    }

    AttributedString(char[] buffer, long[] style, int start, int end) {
        this.buffer = buffer;
        this.style = style;
        this.start = start;
        this.end = end;
    }

    public static AttributedString fromAnsi(String ansi) {
        return fromAnsi(ansi, 0);
    }

    public static AttributedString fromAnsi(String ansi, int tabs) {
        return fromAnsi(ansi, List.of(tabs));
    }

    public static AttributedString fromAnsi(String ansi, List<Integer> tabs) {
        return fromAnsi(ansi, tabs, null, null);
    }

    public static AttributedString fromAnsi(String ansi, Terminal terminal) {
        String alternateIn, alternateOut;
        if (!DISABLE_ALTERNATE_CHARSET) {
            alternateIn = Curses.tputs(terminal.getStringCapability(InfoCmp.Capability.enter_alt_charset_mode));
            alternateOut = Curses.tputs(terminal.getStringCapability(InfoCmp.Capability.exit_alt_charset_mode));
        } else {
            alternateIn = null;
            alternateOut = null;
        }
        return fromAnsi(ansi, List.of(0), alternateIn, alternateOut);
    }

    public static AttributedString fromAnsi(String ansi, List<Integer> tabs, String altIn, String altOut) {
        if (ansi == null) {
            return null;
        }
        return new AttributedStringBuilder(ansi.length())
                .tabs(tabs)
                .altCharset(altIn, altOut)
                .ansiAppend(ansi)
                .toAttributedString();
    }

    public static String stripAnsi(String ansi) {
        if (ansi == null) {
            return null;
        }
        return new AttributedStringBuilder(ansi.length()).ansiAppend(ansi).toString();
    }

    public static AttributedString join(AttributedString delimiter, AttributedString... elements) {
        Objects.requireNonNull(delimiter);
        Objects.requireNonNull(elements);
        return join(delimiter, Arrays.asList(elements));
    }

    public static AttributedString join(AttributedString delimiter, Iterable<AttributedString> elements) {
        Objects.requireNonNull(elements);
        AttributedStringBuilder sb = new AttributedStringBuilder();
        int i = 0;
        for (AttributedString str : elements) {
            if (i++ > 0 && delimiter != null) {
                sb.append(delimiter);
            }
            sb.append(str);
        }
        return sb.toAttributedString();
    }

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

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

    @Override
    public int length() {
        return end - start;
    }

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

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

    @Override
    public AttributedString subSequence(int start, int end) {
        return new AttributedString(this, start, end);
    }

    public AttributedString styleMatches(Pattern pattern, AttributedStyle style) {
        Matcher matcher = pattern.matcher(this);
        boolean result = matcher.find();
        if (result) {
            long[] newstyle = this.style.clone();
            do {
                for (int i = matcher.start(); i < matcher.end(); i++) {
                    newstyle[this.start + i] = (newstyle[this.start + i] & ~style.getMask()) | style.getStyle();
                }
                result = matcher.find();
            } while (result);
            return new AttributedString(buffer, newstyle, start, end);
        }
        return this;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        AttributedString that = (AttributedString) o;
        return end - start == that.end - that.start
                && arrEq(buffer, that.buffer, start, that.start, end - start)
                && arrEq(style, that.style, start, that.start, end - start);
    }

    private boolean arrEq(char[] a1, char[] a2, int s1, int s2, int l) {
        for (int i = 0; i < l; i++) {
            if (a1[s1 + i] != a2[s2 + i]) {
                return false;
            }
        }
        return true;
    }

    private boolean arrEq(long[] a1, long[] a2, int s1, int s2, int l) {
        for (int i = 0; i < l; i++) {
            if (a1[s1 + i] != a2[s2 + i]) {
                return false;
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        int result = Arrays.hashCode(buffer);
        result = 31 * result + Arrays.hashCode(style);
        result = 31 * result + start;
        result = 31 * result + end;
        return result;
    }
}
