package eu.jonahbauer.wizard.client.cli;

import eu.jonahbauer.wizard.client.cli.state.ClientState;
import eu.jonahbauer.wizard.client.cli.state.Menu;
import eu.jonahbauer.wizard.client.cli.util.DelegateCompleter;
import eu.jonahbauer.wizard.client.cli.util.StateAwareFactory;
import eu.jonahbauer.wizard.common.machine.TimeoutContext;
import eu.jonahbauer.wizard.common.messages.client.ClientMessage;
import eu.jonahbauer.wizard.common.messages.server.ServerMessage;
import lombok.Getter;
import org.java_websocket.framing.CloseFrame;
import org.jline.reader.*;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import picocli.CommandLine;
import picocli.CommandLine.Model.CommandSpec;
import picocli.shell.jline3.PicocliJLineCompleter;

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static picocli.CommandLine.*;

@Command
public class Client extends TimeoutContext<ClientState, Client> implements Runnable {
    @Option(names = "-v")
    @Getter
    private boolean verbose;

    private LineReader reader;

    @Getter
    private ClientSocket socket;

    private final ReentrantLock lock = new ReentrantLock();
    private final Condition ready = lock.newCondition();
    private boolean isReady = true;

    private final DelegateCompleter completer = new DelegateCompleter();
    private CommandSpec spec;

    public static void main(String[] args) {
        new CommandLine(new Client()).execute(args);
    }

    public Client() {
        super(new Menu());
    }

    @Override
    public void run() {
        try {
            update();

            Parser parser = new DefaultParser();
            try (Terminal terminal = TerminalBuilder.builder().build()) {
                this.reader = LineReaderBuilder.builder()
                        .terminal(terminal)
                        .completer(completer)
                        .parser(parser)
                        .build();

                String prompt = "> ";

                String line;
                while (true) {
                    lock.lock();
                    try {
                        while (!isReady) {
                            ready.await();
                        }
                    } finally {
                        lock.unlock();
                    }

                    try {
                        line = reader.readLine(prompt).trim();
                        terminal.flush();

                        if (line.isEmpty() || line.startsWith("#")) continue;
                        if (line.equals("exit") || line.equals("quit")) break;

                        var parsedLine = parser.parse(line, 0);
                        execute(() -> execute(spec, parsedLine));
                        terminal.flush();
                    } catch (UserInterruptException e) {
                        // ignore
                    } catch (EndOfFileException e) {
                        break;
                    } catch (Exception e) {
                        e.printStackTrace(reader.getTerminal().writer());
                        terminal.flush();
                    }
                }
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }

        if (socket != null && socket.isOpen()) {
            socket.close(CloseFrame.GOING_AWAY);
        }
        shutdownNow();
    }

    @Override
    protected void handleError(Throwable t) {
        t.printStackTrace();
        System.exit(1);
    }

    @Override
    protected void onTransition(ClientState from, ClientState to) {
        update(to);
    }

    //<editor-fold desc="Socket" defaultstate="collapsed">
    public void setSocket(ClientSocket socket) {
        this.socket = socket;
        if (socket != null) {
            this.socket.setAttachment(this);
        }
    }

    public void onOpen() {
        execute(s -> s.onOpen(this));
    }

    public void onMessage(ServerMessage message) {
        if (verbose) {
            println(message.toString());
        }
        execute(s -> s.onMessage(this, message));
    }

    public void onClose(int code, String reason, boolean remote) {
        execute(s -> s.onClose(this, code, reason, remote));
    }

    public void send(ClientMessage message) {
        getSocket().send(message.toString());
    }
    //</editor-fold>

    //<editor-fold desc="Output" defaultstate="collapsed">
    public void println(String str) {
        reader.printAbove(str);
    }

    public void println(Object object) {
        println(Objects.toString(object));
    }

    public void printfln(String format, Object...args) {
        reader.printAbove(format.formatted(args));
    }

    public void print(String[]...columns) {
        print(null, null, columns);
    }

    public void print(String prefix, String suffix, String[]...columns) {
        int[] length = new int[columns.length];

        for (int i = 0; i < columns.length; i++) {
            length[i] = 0;
            for (int j = 0; j < columns[i].length; j++) {
                if (columns[i][j] == null) columns[i][j] = "null";
                var str = columns[i][j].length();
                if (str > length[i]) {
                    length[i] = str;
                }
            }
        }

        String format = IntStream.range(0, length.length)
                                 .mapToObj(i -> "%" + (i + 1) + "$" + (length[i]) + "s")
                                 .collect(Collectors.joining(" "));

        StringBuilder builder = new StringBuilder();
        if (prefix != null) {
            builder.append(prefix).append('\n');
        }
        String[] row = new String[columns.length];
        for (int j = 0; j < columns[0].length; j++) {
            for (int i = 0; i < columns.length; i++) {
                row[i] = columns[i][j];
            }
            builder.append(format.formatted((Object[]) row)).append('\n');
        }
        if (suffix != null) {
            builder.append(suffix);
        }

        println(builder.toString());
    }
    //</editor-fold>

    public void ready() {
        lock.lock();
        try {
            isReady = true;
            ready.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void waitForReady() {
        lock.lock();
        try {
            isReady = false;
        } finally {
            lock.unlock();
        }
    }

    private void update() {
        update(getState());
    }

    private void update(ClientState state) {
        synchronized (completer) {
            var commandLine = new CommandLine(state.getCommand(), new StateAwareFactory(this, state));
            var spec = commandLine.getCommandSpec();
            if (this.spec != spec) {
                this.spec = spec;
                completer.setDelegate(new PicocliJLineCompleter(spec));
            }
        }
    }

    private Optional<ClientState> execute(CommandSpec spec, ParsedLine line) {
        var commandLine = spec.commandLine();

        AtomicReference<Exception> exceptionRef = new AtomicReference<>();
        commandLine.setExecutionExceptionHandler((ex, cl, parseResult) -> {
            exceptionRef.set(ex);
            return -1;
        });
        commandLine.execute(line.words().toArray(String[]::new));

        Exception exception = exceptionRef.get();
        if (exception == null) {
            Object result;
            ParseResult parseResult = commandLine.getParseResult();
            if (parseResult.subcommand() != null) {
                CommandLine sub = parseResult.subcommand().commandSpec().commandLine();
                result = sub.getExecutionResult();
            } else {
                result = commandLine.getExecutionResult();
            }

            if (result instanceof ClientState state) {
                return Optional.of(state);
            } else if (result instanceof Optional) {
                //noinspection unchecked
                return (Optional<ClientState>) result;
            } else {
                return Optional.empty();
            }
        } else {
            handleError(exception);
            return Optional.empty();
        }
    }
}
