// 
// Decompiled by Procyon v0.6.0
// 

package org.jline.reader.impl.history;

import java.util.Spliterator;
import java.util.Objects;
import java.util.ListIterator;
import java.util.ArrayList;
import java.util.Collection;
import java.nio.file.StandardCopyOption;
import java.nio.file.CopyOption;
import java.io.BufferedWriter;
import org.jline.reader.impl.ReaderUtils;
import java.nio.file.StandardOpenOption;
import java.nio.file.OpenOption;
import java.nio.file.attribute.FileAttribute;
import java.time.DateTimeException;
import java.time.Instant;
import java.util.Iterator;
import java.io.BufferedReader;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.List;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.io.IOException;
import org.jline.utils.Log;
import java.nio.file.Paths;
import java.io.File;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import org.jline.reader.LineReader;
import java.util.LinkedList;
import org.jline.reader.History;

public class DefaultHistory implements History
{
    public static final int DEFAULT_HISTORY_SIZE = 500;
    public static final int DEFAULT_HISTORY_FILE_SIZE = 10000;
    private final LinkedList<Entry> items;
    private LineReader reader;
    private Map<String, HistoryFileData> historyFiles;
    private int offset;
    private int index;
    
    public DefaultHistory() {
        this.items = new LinkedList<Entry>();
        this.historyFiles = new HashMap<String, HistoryFileData>();
        this.offset = 0;
        this.index = 0;
    }
    
    public DefaultHistory(final LineReader reader) {
        this.items = new LinkedList<Entry>();
        this.historyFiles = new HashMap<String, HistoryFileData>();
        this.offset = 0;
        this.index = 0;
        this.attach(reader);
    }
    
    private Path getPath() {
        final Object obj = (this.reader != null) ? this.reader.getVariables().get("history-file") : null;
        if (obj instanceof Path) {
            return (Path)obj;
        }
        if (obj instanceof File) {
            return ((File)obj).toPath();
        }
        if (obj != null) {
            return Paths.get(obj.toString(), new String[0]);
        }
        return null;
    }
    
    @Override
    public void attach(final LineReader reader) {
        if (this.reader != reader) {
            this.reader = reader;
            try {
                this.load();
            }
            catch (final IllegalArgumentException | IOException e) {
                Log.warn("Failed to load history", e);
            }
        }
    }
    
    @Override
    public void load() throws IOException {
        final Path path = this.getPath();
        if (path != null) {
            try {
                if (Files.exists(path, new LinkOption[0])) {
                    Log.trace("Loading history from: ", path);
                    this.internalClear();
                    boolean hasErrors = false;
                    try (final BufferedReader reader = Files.newBufferedReader(path)) {
                        final List<String> lines = reader.lines().collect((Collector<? super String, ?, List<String>>)Collectors.toList());
                        for (final String line : lines) {
                            try {
                                this.addHistoryLine(path, line);
                            }
                            catch (final IllegalArgumentException e) {
                                Log.debug("Skipping invalid history line: " + line, e);
                                hasErrors = true;
                            }
                        }
                    }
                    this.setHistoryFileData(path, new HistoryFileData(this.items.size(), this.offset + this.items.size()));
                    this.maybeResize();
                    if (hasErrors) {
                        Log.info("History file contained errors, rewriting with valid entries");
                        this.write(path, false);
                    }
                }
            }
            catch (final IOException e2) {
                Log.debug("Failed to load history; clearing", e2);
                this.internalClear();
                throw e2;
            }
        }
    }
    
    @Override
    public void read(final Path file, final boolean checkDuplicates) throws IOException {
        final Path path = (file != null) ? file : this.getPath();
        if (path != null) {
            try {
                if (Files.exists(path, new LinkOption[0])) {
                    Log.trace("Reading history from: ", path);
                    boolean hasErrors = false;
                    try (final BufferedReader reader = Files.newBufferedReader(path)) {
                        final List<String> lines = reader.lines().collect((Collector<? super String, ?, List<String>>)Collectors.toList());
                        for (final String line : lines) {
                            try {
                                this.addHistoryLine(path, line, checkDuplicates);
                            }
                            catch (final IllegalArgumentException e) {
                                Log.debug("Skipping invalid history line: " + line, e);
                                hasErrors = true;
                            }
                        }
                    }
                    this.setHistoryFileData(path, new HistoryFileData(this.items.size(), this.offset + this.items.size()));
                    this.maybeResize();
                    if (hasErrors) {
                        Log.info("History file contained errors, rewriting with valid entries");
                        this.write(path, false);
                    }
                }
            }
            catch (final IOException e2) {
                Log.debug("Failed to read history; clearing", e2);
                this.internalClear();
                throw e2;
            }
        }
    }
    
    private String doHistoryFileDataKey(final Path path) {
        return (path != null) ? path.toAbsolutePath().toString() : null;
    }
    
    private HistoryFileData getHistoryFileData(final Path path) {
        final String key = this.doHistoryFileDataKey(path);
        if (!this.historyFiles.containsKey(key)) {
            this.historyFiles.put(key, new HistoryFileData());
        }
        return this.historyFiles.get(key);
    }
    
    private void setHistoryFileData(final Path path, final HistoryFileData historyFileData) {
        this.historyFiles.put(this.doHistoryFileDataKey(path), historyFileData);
    }
    
    private boolean isLineReaderHistory(final Path path) throws IOException {
        final Path lrp = this.getPath();
        if (lrp == null) {
            return path == null;
        }
        return Files.isSameFile(lrp, path);
    }
    
    private void setLastLoaded(final Path path, final int lastloaded) {
        this.getHistoryFileData(path).setLastLoaded(lastloaded);
    }
    
    private void setEntriesInFile(final Path path, final int entriesInFile) {
        this.getHistoryFileData(path).setEntriesInFile(entriesInFile);
    }
    
    private void incEntriesInFile(final Path path, final int amount) {
        this.getHistoryFileData(path).incEntriesInFile(amount);
    }
    
    private int getLastLoaded(final Path path) {
        return this.getHistoryFileData(path).getLastLoaded();
    }
    
    private int getEntriesInFile(final Path path) {
        return this.getHistoryFileData(path).getEntriesInFile();
    }
    
    protected void addHistoryLine(final Path path, final String line) {
        this.addHistoryLine(path, line, false);
    }
    
    protected void addHistoryLine(final Path path, final String line, final boolean checkDuplicates) {
        if (this.reader.isSet(LineReader.Option.HISTORY_TIMESTAMPED)) {
            final int idx = line.indexOf(58);
            final String badHistoryFileSyntax = "Bad history file syntax! The history file `" + path + "` may be an older history: please remove it or use a different history file.";
            if (idx < 0) {
                throw new IllegalArgumentException(badHistoryFileSyntax);
            }
            Instant time;
            try {
                time = Instant.ofEpochMilli(Long.parseLong(line.substring(0, idx)));
            }
            catch (final DateTimeException | NumberFormatException e) {
                throw new IllegalArgumentException(badHistoryFileSyntax);
            }
            final String unescaped = unescape(line.substring(idx + 1));
            this.internalAdd(time, unescaped, checkDuplicates);
        }
        else {
            this.internalAdd(Instant.now(), unescape(line), checkDuplicates);
        }
    }
    
    @Override
    public void purge() throws IOException {
        this.internalClear();
        final Path path = this.getPath();
        if (path != null) {
            Log.trace("Purging history from: ", path);
            Files.deleteIfExists(path);
        }
    }
    
    @Override
    public void write(final Path file, final boolean incremental) throws IOException {
        final Path path = (file != null) ? file : this.getPath();
        if (path != null && Files.exists(path, new LinkOption[0])) {
            Files.deleteIfExists(path);
        }
        this.internalWrite(path, incremental ? this.getLastLoaded(path) : 0);
    }
    
    @Override
    public void append(final Path file, final boolean incremental) throws IOException {
        this.internalWrite((file != null) ? file : this.getPath(), incremental ? this.getLastLoaded(file) : 0);
    }
    
    @Override
    public void save() throws IOException {
        this.internalWrite(this.getPath(), this.getLastLoaded(this.getPath()));
    }
    
    private void internalWrite(final Path path, final int from) throws IOException {
        if (path != null) {
            Log.trace("Saving history to: ", path);
            final Path parent = path.toAbsolutePath().getParent();
            if (!Files.exists(parent, new LinkOption[0])) {
                Files.createDirectories(parent, (FileAttribute<?>[])new FileAttribute[0]);
            }
            try (final BufferedWriter writer = Files.newBufferedWriter(path.toAbsolutePath(), StandardOpenOption.WRITE, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) {
                for (final Entry entry : this.items.subList(from, this.items.size())) {
                    if (this.isPersistable(entry)) {
                        writer.append((CharSequence)this.format(entry));
                    }
                }
            }
            this.incEntriesInFile(path, this.items.size() - from);
            final int max = ReaderUtils.getInt(this.reader, "history-file-size", 10000);
            if (this.getEntriesInFile(path) > max + max / 4) {
                this.trimHistory(path, max);
            }
        }
        this.setLastLoaded(path, this.items.size());
    }
    
    protected void trimHistory(final Path path, final int max) throws IOException {
        Log.trace("Trimming history path: ", path);
        final LinkedList<Entry> allItems = new LinkedList<Entry>();
        try (final BufferedReader historyFileReader = Files.newBufferedReader(path)) {
            final List<String> lines = historyFileReader.lines().collect((Collector<? super String, ?, List<String>>)Collectors.toList());
            for (final String l : lines) {
                try {
                    if (this.reader.isSet(LineReader.Option.HISTORY_TIMESTAMPED)) {
                        final int idx = l.indexOf(58);
                        if (idx < 0) {
                            Log.debug("Skipping invalid history line: " + l);
                        }
                        else {
                            try {
                                final Instant time = Instant.ofEpochMilli(Long.parseLong(l.substring(0, idx)));
                                final String line = unescape(l.substring(idx + 1));
                                allItems.add(this.createEntry(allItems.size(), time, line));
                            }
                            catch (final DateTimeException | NumberFormatException e) {
                                Log.debug("Skipping invalid history timestamp: " + l);
                            }
                        }
                    }
                    else {
                        allItems.add(this.createEntry(allItems.size(), Instant.now(), unescape(l)));
                    }
                }
                catch (final Exception e2) {
                    Log.debug("Skipping invalid history line: " + l, e2);
                }
            }
        }
        final List<Entry> trimmedItems = doTrimHistory(allItems, max);
        final Path temp = Files.createTempFile(path.toAbsolutePath().getParent(), path.getFileName().toString(), ".tmp", (FileAttribute<?>[])new FileAttribute[0]);
        try (final BufferedWriter writer = Files.newBufferedWriter(temp, StandardOpenOption.WRITE)) {
            for (final Entry entry : trimmedItems) {
                writer.append((CharSequence)this.format(entry));
            }
        }
        Files.move(temp, path, StandardCopyOption.REPLACE_EXISTING);
        if (this.isLineReaderHistory(path)) {
            this.internalClear();
            this.offset = trimmedItems.get(0).index();
            this.items.addAll(trimmedItems);
            this.setHistoryFileData(path, new HistoryFileData(this.items.size(), this.items.size()));
        }
        else {
            this.setEntriesInFile(path, allItems.size());
        }
        this.maybeResize();
    }
    
    protected EntryImpl createEntry(final int index, final Instant time, final String line) {
        return new EntryImpl(index, time, line);
    }
    
    private void internalClear() {
        this.offset = 0;
        this.index = 0;
        this.historyFiles = new HashMap<String, HistoryFileData>();
        this.items.clear();
    }
    
    static List<Entry> doTrimHistory(final List<Entry> allItems, final int max) {
        for (int idx = 0; idx < allItems.size(); ++idx) {
            final int ridx = allItems.size() - idx - 1;
            final String line = allItems.get(ridx).line().trim();
            final ListIterator<Entry> iterator = allItems.listIterator(ridx);
            while (iterator.hasPrevious()) {
                final String l = iterator.previous().line();
                if (line.equals(l.trim())) {
                    iterator.remove();
                }
            }
        }
        while (allItems.size() > max) {
            allItems.remove(0);
        }
        int index = allItems.get(allItems.size() - 1).index() - allItems.size() + 1;
        final List<Entry> out = new ArrayList<Entry>();
        for (final Entry e : allItems) {
            out.add(new EntryImpl(index++, e.time(), e.line()));
        }
        return out;
    }
    
    @Override
    public int size() {
        return this.items.size();
    }
    
    @Override
    public boolean isEmpty() {
        return this.items.isEmpty();
    }
    
    @Override
    public int index() {
        return this.offset + this.index;
    }
    
    @Override
    public int first() {
        return this.offset;
    }
    
    @Override
    public int last() {
        return this.offset + this.items.size() - 1;
    }
    
    private String format(final Entry entry) {
        if (this.reader.isSet(LineReader.Option.HISTORY_TIMESTAMPED)) {
            return entry.time().toEpochMilli() + ":" + escape(entry.line()) + "\n";
        }
        return escape(entry.line()) + "\n";
    }
    
    @Override
    public String get(final int index) {
        final int idx = index - this.offset;
        if (idx >= this.items.size() || idx < 0) {
            throw new IllegalArgumentException("IndexOutOfBounds: Index:" + idx + ", Size:" + this.items.size());
        }
        return this.items.get(idx).line();
    }
    
    @Override
    public void add(final Instant time, String line) {
        Objects.requireNonNull(time);
        Objects.requireNonNull(line);
        if (ReaderUtils.getBoolean(this.reader, "disable-history", false)) {
            return;
        }
        if (ReaderUtils.isSet(this.reader, LineReader.Option.HISTORY_IGNORE_SPACE) && line.startsWith(" ")) {
            return;
        }
        if (ReaderUtils.isSet(this.reader, LineReader.Option.HISTORY_REDUCE_BLANKS)) {
            line = line.trim();
        }
        if (ReaderUtils.isSet(this.reader, LineReader.Option.HISTORY_IGNORE_DUPS) && !this.items.isEmpty() && line.equals(this.items.getLast().line())) {
            return;
        }
        if (this.matchPatterns(ReaderUtils.getString(this.reader, "history-ignore", ""), line)) {
            return;
        }
        this.internalAdd(time, line);
        if (ReaderUtils.isSet(this.reader, LineReader.Option.HISTORY_INCREMENTAL)) {
            try {
                this.save();
            }
            catch (final IOException e) {
                Log.warn("Failed to save history", e);
            }
        }
    }
    
    protected boolean matchPatterns(final String patterns, final String line) {
        if (patterns == null || patterns.isEmpty()) {
            return false;
        }
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < patterns.length(); ++i) {
            char ch = patterns.charAt(i);
            if (ch == '\\') {
                ch = patterns.charAt(++i);
                sb.append(ch);
            }
            else if (ch == ':') {
                sb.append('|');
            }
            else if (ch == '*') {
                sb.append('.').append('*');
            }
            else {
                sb.append(ch);
            }
        }
        return line.matches(sb.toString());
    }
    
    protected void internalAdd(final Instant time, final String line) {
        this.internalAdd(time, line, false);
    }
    
    protected void internalAdd(final Instant time, final String line, final boolean checkDuplicates) {
        final Entry entry = new EntryImpl(this.offset + this.items.size(), time, line);
        if (checkDuplicates) {
            for (final Entry e : this.items) {
                if (e.line().trim().equals(line.trim())) {
                    return;
                }
            }
        }
        this.items.add(entry);
        this.maybeResize();
    }
    
    private void maybeResize() {
        while (this.size() > ReaderUtils.getInt(this.reader, "history-size", 500)) {
            this.items.removeFirst();
            for (final HistoryFileData hfd : this.historyFiles.values()) {
                hfd.decLastLoaded();
            }
            ++this.offset;
        }
        this.index = this.size();
    }
    
    @Override
    public ListIterator<Entry> iterator(final int index) {
        return this.items.listIterator(index - this.offset);
    }
    
    @Override
    public Spliterator<Entry> spliterator() {
        return this.items.spliterator();
    }
    
    @Override
    public void resetIndex() {
        this.index = Math.min(this.index, this.items.size());
    }
    
    @Override
    public boolean moveToLast() {
        final int lastEntry = this.size() - 1;
        if (lastEntry >= 0 && lastEntry != this.index) {
            this.index = this.size() - 1;
            return true;
        }
        return false;
    }
    
    @Override
    public boolean moveTo(int index) {
        index -= this.offset;
        if (index >= 0 && index < this.size()) {
            this.index = index;
            return true;
        }
        return false;
    }
    
    @Override
    public boolean moveToFirst() {
        if (this.size() > 0 && this.index != 0) {
            this.index = 0;
            return true;
        }
        return false;
    }
    
    @Override
    public void moveToEnd() {
        this.index = this.size();
    }
    
    @Override
    public String current() {
        if (this.index >= this.size()) {
            return "";
        }
        return this.items.get(this.index).line();
    }
    
    @Override
    public boolean previous() {
        if (this.index <= 0) {
            return false;
        }
        --this.index;
        return true;
    }
    
    @Override
    public boolean next() {
        if (this.index >= this.size()) {
            return false;
        }
        ++this.index;
        return true;
    }
    
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        for (final Entry e : this) {
            sb.append(e.toString()).append("\n");
        }
        return sb.toString();
    }
    
    private static String escape(final String s) {
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); ++i) {
            final char ch = s.charAt(i);
            switch (ch) {
                case '\n': {
                    sb.append('\\');
                    sb.append('n');
                    break;
                }
                case '\r': {
                    sb.append('\\');
                    sb.append('r');
                    break;
                }
                case '\\': {
                    sb.append('\\');
                    sb.append('\\');
                    break;
                }
                default: {
                    sb.append(ch);
                    break;
                }
            }
        }
        return sb.toString();
    }
    
    static String unescape(final String s) {
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); ++i) {
            char ch = s.charAt(i);
            switch (ch) {
                case '\\': {
                    ch = s.charAt(++i);
                    if (ch == 'n') {
                        sb.append('\n');
                        break;
                    }
                    if (ch == 'r') {
                        sb.append('\r');
                        break;
                    }
                    sb.append(ch);
                    break;
                }
                default: {
                    sb.append(ch);
                    break;
                }
            }
        }
        return sb.toString();
    }
    
    protected static class EntryImpl implements Entry
    {
        private final int index;
        private final Instant time;
        private final String line;
        
        public EntryImpl(final int index, final Instant time, final String line) {
            this.index = index;
            this.time = time;
            this.line = line;
        }
        
        @Override
        public int index() {
            return this.index;
        }
        
        @Override
        public Instant time() {
            return this.time;
        }
        
        @Override
        public String line() {
            return this.line;
        }
        
        @Override
        public String toString() {
            return String.format("%d: %s", this.index, this.line);
        }
    }
    
    private static class HistoryFileData
    {
        private int lastLoaded;
        private int entriesInFile;
        
        public HistoryFileData() {
            this.lastLoaded = 0;
            this.entriesInFile = 0;
        }
        
        public HistoryFileData(final int lastLoaded, final int entriesInFile) {
            this.lastLoaded = 0;
            this.entriesInFile = 0;
            this.lastLoaded = lastLoaded;
            this.entriesInFile = entriesInFile;
        }
        
        public int getLastLoaded() {
            return this.lastLoaded;
        }
        
        public void setLastLoaded(final int lastLoaded) {
            this.lastLoaded = lastLoaded;
        }
        
        public void decLastLoaded() {
            --this.lastLoaded;
            if (this.lastLoaded < 0) {
                this.lastLoaded = 0;
            }
        }
        
        public int getEntriesInFile() {
            return this.entriesInFile;
        }
        
        public void setEntriesInFile(final int entriesInFile) {
            this.entriesInFile = entriesInFile;
        }
        
        public void incEntriesInFile(final int amount) {
            this.entriesInFile += amount;
        }
    }
}
