/*
 * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.jshell.execution.impl;

import java.io.BufferedOutputStream;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Arrays;
import jdk.internal.io.JdkConsole;
import jdk.internal.io.JdkConsoleProvider;
import jdk.jshell.JShellConsole;

/**
 *
 */
public class ConsoleImpl {

    public static void ensureOutputAreWritten() {
        var console = ConsoleProviderImpl.console;

        if (console != null) {
            console.ensureOutputAreWritten();
        }
    }

    public static class ConsoleProviderImpl implements JdkConsoleProvider {

        public ConsoleProviderImpl() {
        }

        private static InputStream remoteOutput;
        private static OutputStream remoteInput;
        private static RemoteConsole console;

        @Override
        public JdkConsole console(boolean isTTY, Charset charset) {
            synchronized (ConsoleProviderImpl.class) {
                if (remoteOutput != null && remoteInput != null) {
                    return console = new RemoteConsole(remoteOutput, remoteInput);
                }
                return null;
            }
        }

        public static synchronized void setRemoteOutput(InputStream remoteOutput) {
            ConsoleProviderImpl.remoteOutput = remoteOutput;
        }

        public static synchronized void setRemoteInput(OutputStream remoteInput) {
            ConsoleProviderImpl.remoteInput =
                    new BufferedOutputStream(remoteInput);
        }

    }

    private static final class RemoteConsole implements JdkConsole {
        private final InputStream remoteOutput;
        private final OutputStream remoteInput;
        private PrintWriter writer;
        private Reader reader;

        public RemoteConsole(InputStream remoteOutput, OutputStream remoteInput) {
            this.remoteInput = new BufferedOutputStream(remoteInput);
            this.remoteOutput = new InputStream() {
                @Override
                public int read() throws IOException {
                    RemoteConsole.this.remoteInput.flush();
                    return remoteOutput.read();
                }
            };
        }

        private void sendChars(char[] data, int off, int len) throws IOException {
            ConsoleImpl.sendChars(remoteInput, data, off, len);
        }

        private int readChars(char[] data, int off, int len) throws IOException {
            sendInt(len);
            int actualLen = readInt();
            for (int i = 0; i < actualLen; i++) {
                data[off + i] = (char) ((remoteOutput.read() <<  8) |
                                        (remoteOutput.read() <<  0));
            }
            return actualLen;
        }

        private char[] readChars() throws IOException {
            int actualLen = readInt();
            char[] result = new char[actualLen];
            for (int i = 0; i < actualLen; i++) {
                result[i] = (char) ((remoteOutput.read() <<  8) |
                                    (remoteOutput.read() <<  0));
            }
            return result;
        }

        private void sendInt(int data) throws IOException {
            ConsoleImpl.sendInt(remoteInput, data);
        }

        private int readInt() throws IOException {
            return (remoteOutput.read() << 24) |
                   (remoteOutput.read() << 16) |
                   (remoteOutput.read() <<  8) |
                   (remoteOutput.read() <<  0);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized PrintWriter writer() {
            if (writer == null) {
                writer = new PrintWriter(new Writer() {
                    int i;
                    @Override
                    public void write(char[] cbuf, int off, int len) throws IOException {
                        sendAndReceive(() -> {
                            remoteInput.write(Task.WRITE_CHARS.ordinal());
                            sendChars(cbuf, off, len);
                            return null;
                        });
                    }

                    @Override
                    public void flush() throws IOException {
                        sendAndReceive(() -> {
                            remoteInput.write(Task.FLUSH_OUTPUT.ordinal());
                            remoteOutput.read();
                            return null;
                        });
                    }

                    @Override
                    public void close() throws IOException {
                    }
                });
            }
            return writer;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized Reader reader() {
            if (reader == null) {
                reader = new Reader() {
                    @Override
                    public int read(char[] cbuf, int off, int len) throws IOException {
                        return sendAndReceive(() -> {
                            remoteInput.write(Task.READ_CHARS.ordinal());
                            return readChars(cbuf, off, len);
                        });
                    }

                    @Override
                    public void close() throws IOException {
                    }
                };
            } return reader;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public JdkConsole format(String fmt, Object... args) {
            writer().format(fmt, args).flush();
            return this;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public JdkConsole printf(String format, Object... args) {
            return format(format, args);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String readLine(String fmt, Object... args) {
            try {
                return sendAndReceive(() -> {
                    remoteInput.write(Task.READ_LINE.ordinal());
                    String prompt = fmt.formatted(args);
                    char[] chars = prompt.toCharArray();
                    sendChars(chars, 0, chars.length);
                    char[] line = readChars();
                    return new String(line);
                });
            } catch (IOException ex) {
                throw new IOError(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String readLine() {
            return readLine("");
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public char[] readPassword(String fmt, Object... args) {
            try {
                return sendAndReceive(() -> {
                    remoteInput.write(Task.READ_PASSWORD.ordinal());
                    String prompt = fmt.formatted(args);
                    char[] chars = prompt.toCharArray();
                    sendChars(chars, 0, chars.length);
                    return readChars();
                });
            } catch (IOException ex) {
                throw new IOError(ex);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public char[] readPassword() {
            return readPassword("");
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void flush() {
            try {
                sendAndReceive(() -> {
                    remoteInput.write(Task.FLUSH_CONSOLE.ordinal());
                    remoteOutput.read();
                    return null;
                });
            } catch (IOException ex) {
                throw new IOError(ex);
            }
        }

        @Override
        public Charset charset() {
            try {
                return sendAndReceive(() -> {
                    remoteInput.write(Task.CHARSET.ordinal());
                    return Charset.forName(new String(readChars()));
                });
            } catch (IOException ex) {
                throw new IOError(ex);
            }
        }

        void ensureOutputAreWritten() {
            try {
                sendAndReceive(() -> {
                    remoteInput.write(Task.ENSURE_OUTPUTS_ARE_WRITTEN.ordinal());
                    return remoteOutput.read();
                });
            } catch (IOException ex) {
                throw new IOError(ex);
            }
        }

        private synchronized <R, E extends Exception> R sendAndReceive(SendAndReceive<R, E> task) throws IOException, E {
            return task.run();
        }

        interface SendAndReceive<R, E extends Exception> {
            R run() throws E;
        }

    }

    public static final class ConsoleOutputStream extends OutputStream {

        int[] buffer = new int[1024];
        int bp;
        final JShellConsole console;
        public final InputStream sinkInput;
        final OutputStream sinkOutput;

        public ConsoleOutputStream(JShellConsole console) {
            this.console = console;
            PipeInputStream sinkInput = new PipeInputStream();
            this.sinkInput = sinkInput;
            this.sinkOutput = sinkInput.createOutput();
        }

        @Override
        public synchronized void write(int b) throws IOException {
            if (bp + 1 >= buffer.length) {
                buffer = Arrays.copyOf(buffer, 2 * buffer.length);
            }

            buffer[bp++] = b;

            switch (Task.values()[buffer[0]]) {
                case WRITE_CHARS -> {
                    char[] data = readCharsOrNull(1);
                    if (data != null) {
                        console.writer().write(data);
                        bp = 0;
                    }
                }
                case FLUSH_OUTPUT -> {
                    console.writer().flush();
                    sinkOutput.write(0);
                    bp = 0;
                }
                case READ_CHARS -> {
                    if (bp >= 5) {
                        int len = readInt(b);
                        int c = console.reader().read();
                        //XXX: EOF handling!
                        sendChars(sinkOutput, new char[] {(char) c}, 0, 1);
                        bp = 0;
                    }
                }
                case READ_LINE -> {
                    char[] data = readCharsOrNull(1);
                    if (data != null) {
                        String line = console.readLine(new String(data));
                        char[] chars = line.toCharArray();
                        sendChars(sinkOutput, chars, 0, chars.length);
                        bp = 0;
                    }
                }
                case READ_PASSWORD -> {
                    char[] data = readCharsOrNull(1);
                    if (data != null) {
                        char[] chars = console.readPassword(new String(data));
                        sendChars(sinkOutput, chars, 0, chars.length);
                        bp = 0;
                    }
                }
                case FLUSH_CONSOLE -> {
                    console.flush();
                    sinkOutput.write(0);
                    bp = 0;
                }
                case CHARSET -> {
                    char[] name = console.charset().name().toCharArray();
                    sendChars(sinkOutput, name, 0, name.length);
                    bp = 0;
                }
                case ENSURE_OUTPUTS_ARE_WRITTEN -> {
                    sinkOutput.write(0);
                    bp = 0;
                }
            }
        }

        private int readInt(int pos) throws IOException {
            return (buffer[pos + 0] << 24) |
                   (buffer[pos + 1] << 16) |
                   (buffer[pos + 2] <<  8) |
                   (buffer[pos + 3] <<  0);
        }

        private char readChar(int pos) throws IOException {
            return (char) ((buffer[pos] << 8) |
                    (buffer[pos + 1] << 0));
        }

        private char[] readCharsOrNull(int pos) throws IOException {
            if (bp >= pos + 4) {
                int len = readInt(pos);
                if (bp >= pos + 4 + 2 * len) {
                    char[] result = new char[len];
                    for (int i = 0; i < len; i++) {
                        result[i] = readChar(pos + 4 + 2 * i);
                    }
                    return result;
                }
            }
            return null;
        }
    }

    private static void sendChars(OutputStream remoteInput, char[] data, int off, int len) throws IOException {
        sendInt(remoteInput, len);
        for (int i = 0; i < len; i++) {
            char c = data[off + i];

            remoteInput.write((c >> 8) & 0xFF);
            remoteInput.write((c >> 0) & 0xFF);
        }
    }

    private static void sendInt(OutputStream remoteInput, int data) throws IOException {
        remoteInput.write((data >> 24) & 0xFF);
        remoteInput.write((data >> 16) & 0xFF);
        remoteInput.write((data >>  8) & 0xFF);
        remoteInput.write((data >>  0) & 0xFF);
    }

    private enum Task {
        WRITE_CHARS,
        FLUSH_OUTPUT,
        READ_CHARS,
        READ_LINE,
        READ_PASSWORD,
        FLUSH_CONSOLE,
        CHARSET,
        ENSURE_OUTPUTS_ARE_WRITTEN,
        ;
    }
}
