package com.jscape.util.cli;

import com.jscape.util.IOTools;

import java.io.*;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;

import static com.jscape.util.Parameters.assertNotNull;

/**
 * Provides basic interface to system CLI.
 *
 * @author Alex Usun
 */
public class Console {

    private static final String DEFAULT_ECHO = "";

    private static volatile Console systemConsole;

    private final BufferedReader reader;
    private final PrintStream writer;

    /**
     * Creates a new console instance for system in/out streams.
     *
     * @return a console instance
     */
    public static synchronized Console systemConsole() {
        if (systemConsole == null) {
            systemConsole = new Console(new BufferedReader(new InputStreamReader(System.in, Charset.defaultCharset())), System.out);
        }

        return systemConsole;
    }

    /**
     * Creates a new console instance.
     *
     * @param reader a reader
     * @param writer a writer
     */
    public Console(Reader reader, PrintStream writer) {
        assertNotNull(reader);
        this.reader = new BufferedReader(reader);

        assertNotNull(writer);
        this.writer = writer;
    }

    /**
     * Prints the specified message.
     *
     * @param message the message to print
     */
    public void print(String message) {
        this.writer.print(message);
    }

    /**
     * Prints the specified message and terminates the line.
     *
     * @param message the message to print
     */
    public void println(String message) {
        this.writer.println(message);
    }

    /**
     * Terminates the line.
     */
    public void println() {
        this.writer.println();
    }

    /**
     * Prints the specified message.
     *
     * @param template  the message template to print
     * @param arguments template arguments
     */
    public void printf(String template, Object... arguments) {
        this.writer.printf(template, arguments);
    }

    /**
     * Prints the specified error stack trace.
     *
     * @param error target error instance
     */
    public void print(Throwable error) {
        error.printStackTrace(this.writer);
    }

    /**
     * Prints a prompt and reads an user response from the same line.
     *
     * @param prompt a prompt
     *
     * @return an user response
     *
     * @throws IOException if an error occurs
     */
    public String prompt(String prompt)
            throws IOException {
        return prompt(prompt, false);
    }

    /**
     * Prints a prompt and reads an user response.
     *
     * @param prompt  a prompts
     * @param newLine if <code>true</code> the user response will be read from the new line
     *
     * @return an user response
     *
     * @throws IOException if an error occurs
     */
    public String prompt(String prompt, boolean newLine)
            throws IOException {
        print(prompt);

        if (newLine) {
            println();
        }

        return readLine();
    }

    /**
     * Prints a prompt and reads an user response masking user input.
     * May be used for password-related interactions.
     *
     * @param prompt a prompt
     *
     * @return an user response
     *
     * @throws IOException if an error occurs
     */
    public String promptMasked(String prompt)
            throws IOException {
        return promptPassword(prompt, DEFAULT_ECHO, false);
    }

    /**
     * Prints a prompt and reads an user response masking user input.
     * May be used for password-related interactions.
     *
     * @param prompt  a prompt
     * @param echo    a char that replaces entered symbols
     * @param newLine if <code>true</code> the user response will be read from the new line
     *
     * @return an user response
     *
     * @throws IOException if an error occurs
     */
    public String promptPassword(String prompt, String echo, boolean newLine)
            throws IOException {
        InputMasker masker = new InputMasker(this.writer, echo);

        try {
            masker.start();
            return prompt(prompt, newLine);
        } finally {
            masker.stop();
        }
    }

    /**
     * Prints a prompt and awaits one of the specified responses.
     *
     * @param prompt         a prompt
     * @param validResponses a valid responses
     *
     * @return an user response
     *
     * @throws IOException if an error occurs
     */
    public String prompt(String prompt, String... validResponses)
            throws IOException {
        List<String> responsesList = adjustResponses(validResponses);
        String response;

        do {
            response = prompt(prompt);
        } while (!responsesList.contains(response.toUpperCase()));

        return response;
    }

    /**
     * Reads and returns an user response.
     *
     * @return an user response
     *
     * @throws IOException if an error occurs
     */
    public String readLine()
            throws IOException {
        return this.reader.readLine();
    }

    public void awaitInput(char target)
            throws IOException {
        int ch;

        while ((ch = System.in.read()) != -1) {
            if (ch == target) {
                return;
            }
        }
    }

    /**
     * Closes console and underlaid streams if they aren't default system in/out.
     */
    public void close() {
        if (this != systemConsole) {
            IOTools.close(this.reader);
            IOTools.close(this.writer);
        }
    }

    private List<String> adjustResponses(String[] responses) {
        List<String> ret = new LinkedList<String>();
        for (String response : responses) {
            ret.add(response.toUpperCase());
        }

        return ret;
    }

    /**
     * Masks user input.
     */
    public static class InputMasker
            implements Runnable {

        private static final String BACKSPACE = "\010";
        private static final String THREAD_NAME = "Console Input Masker";

        private final PrintStream writer;
        private final String echo;
        private Thread thread;
        private int priority;
        private volatile boolean stopped;

        /**
         * Creates a new instance.
         *
         * @param echo a char that replaces entered symbols
         */
        public InputMasker(PrintStream writer, String echo) {
            assertNotNull(writer);
            this.writer = writer;

            this.echo = BACKSPACE + echo;
        }

        /**
         * Starts masker in separated thread.
         */
        public void start() {
            this.thread = new Thread(this, THREAD_NAME);
            this.thread.start();
        }

        /**
         * Stops masker.
         */
        public void stop() {
            this.stopped = true;
        }

        public void run() {
            this.stopped = false;
            raiseThreadPriority();

            try {
                while (!this.stopped) {
                    replaceCurrentSymbol();
                    awaitNextSymbol();
                }
            } finally {
                restoreThreadPriority();
                cleanup();
            }
        }

        private void raiseThreadPriority() {
            this.priority = Thread.currentThread().getPriority();
            Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
        }

        private void restoreThreadPriority() {
            Thread.currentThread().setPriority(this.priority);
        }

        private void replaceCurrentSymbol() {
            this.writer.print(this.echo);
        }

        private void awaitNextSymbol() {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                this.stopped = true;
            }
        }

        private void cleanup() {
            this.thread = null;
        }
    }

}