package interpreter;

import static util.Path.getPathPrefix;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.function.Function;

import environment.RuntimeEnvironment;
import error.CriticalLispException;
import error.ErrorManager;

public class LispInterpreterBuilderImpl implements LispInterpreterBuilder {

    private static LispInterpreterBuilder uniqueInstance = new LispInterpreterBuilderImpl();

    public static LispInterpreterBuilder getInstance() {
        return uniqueInstance;
    }

    private String inputName;
    private InputStream inputStream;
    private PrintStream outputStream;
    private PrintStream errorOutputStream;
    private Runnable terminationFunction;
    private Runnable errorTerminationFunction;
    private Function<String, String> promptDecorator;
    private Function<String, String> valueOutputDecorator;
    private Function<String, String> warningOutputDecorator;
    private Function<String, String> errorOutputDecorator;
    private Function<String, String> criticalOutputDecorator;
    private RuntimeEnvironment environment;
    private boolean isInteractive;
    private boolean isFileBased;
    protected boolean isBuilt;

    protected LispInterpreterBuilderImpl() {
        this.environment = RuntimeEnvironment.getInstance();
        this.inputName = "";
        this.isInteractive = true;
        this.isFileBased = false;
        this.isBuilt = false;
        this.promptDecorator = s -> s;
        this.valueOutputDecorator = s -> s;
        this.warningOutputDecorator = s -> s;
        this.errorOutputDecorator = s -> s;
        this.criticalOutputDecorator = s -> s;
    }

    @Override
    public void setInput(InputStream inputStream, String inputName) {
        this.inputStream = inputStream;
        this.inputName = inputName;
    }

    @Override
    public void setOutput(PrintStream outputStream) {
        this.outputStream = outputStream;

    }

    @Override
    public void setErrorOutput(PrintStream errorOutputStream) {
        this.errorOutputStream = errorOutputStream;
    }

    @Override
    public void setTerminationFunction(Runnable terminationFunction) {
        this.terminationFunction = terminationFunction;
    }

    @Override
    public void setErrorTerminationFunction(Runnable errorTerminationFunction) {
        this.errorTerminationFunction = errorTerminationFunction;
    }

    @Override
    public void setNotInteractive() {
        this.isInteractive = false;
    }

    @Override
    public void useFile(String fileName) {
        this.isFileBased = true;
        this.inputName = fileName;
        this.setNotInteractive();
    }

    @Override
    public void setPromptDecorator(Function<String, String> decorator) {
        this.promptDecorator = decorator;
    }

    @Override
    public void setValueOutputDecorator(Function<String, String> decorator) {
        this.valueOutputDecorator = decorator;
    }

    @Override
    public void setWarningOutputDecorator(Function<String, String> decorator) {
        this.warningOutputDecorator = decorator;

    }

    @Override
    public void setErrorOutputDecorator(Function<String, String> decorator) {
        this.errorOutputDecorator = decorator;

    }

    @Override
    public void setCriticalOutputDecorator(Function<String, String> decorator) {
        this.criticalOutputDecorator = decorator;
    }

    @Override
    public LispInterpreter build() {
        if (!isBuilt)
            return buildInterpreter();
        else
            throw new InterpreterAlreadyBuiltException();
    }

    private LispInterpreter buildInterpreter() {
        configureRuntimeEnvironment();
        LispInterpreter lispInterpreter = createInterpreter();
        isBuilt = true;

        return lispInterpreter;
    }

    private void configureRuntimeEnvironment() {
        ErrorManager errorManager = new ErrorManager();

        environment.setOutput(outputStream);
        environment.setErrorOutput(errorOutputStream);
        environment.setErrorManager(errorManager);
        environment.setTerminationFunction(terminationFunction);
        environment.setErrorTerminationFunction(errorTerminationFunction);
        environment.setPromptDecorator(promptDecorator);
        environment.setValueOutputDecorator(valueOutputDecorator);
        environment.setWarningOutputDecorator(warningOutputDecorator);
        environment.setErrorOutputDecorator(errorOutputDecorator);
        environment.setCriticalOutputDecorator(criticalOutputDecorator);
        configurePath();
        configureInput(errorManager);
    }

    private void configurePath() {
        if (isFileBased)
            environment.setPath(getPathPrefix(inputName));
        else
            environment.setPath("");
    }

    private void configureInput(ErrorManager errorManager) {
        environment.setInputName(inputName);

        try {
            environment.setInput(getInputStream());
        } catch (FileNotFoundException e) {
            errorManager.handle(new LispFileNotFoundException(e));
        }
    }

    private InputStream getInputStream() throws FileNotFoundException {
        return isFileBased ? new FileInputStream(inputName) : inputStream;
    }

    private LispInterpreter createInterpreter() {
        if (isFileBased)
            return new FileLispInterpreter();
        else if (isInteractive)
            return new InteractiveLispInterpreter();

        return new LispInterpreter();
    }

    public static class InterpreterAlreadyBuiltException extends CriticalLispException {

        private static final long serialVersionUID = 1L;

        @Override
        public String getMessage() {
            return "Refusing to build more than one interpreter.";
        }
    }

    public static class LispFileNotFoundException extends CriticalLispException {

        private static final long serialVersionUID = 1L;
        private String message;

        public LispFileNotFoundException(FileNotFoundException e) {
            this.message = e.getMessage();
        }

        @Override
        public String getMessage() {
            return message;
        }
    }

}
