/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.samza.sql.client.cli;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import org.apache.samza.sql.client.exceptions.CommandHandlerException;
import org.apache.samza.sql.client.impl.CliCommandHandler;
import org.apache.samza.sql.client.impl.CliCommandType;
import org.apache.samza.sql.client.interfaces.CommandHandler;
import org.apache.samza.sql.client.interfaces.ExecutionContext;
import org.apache.samza.sql.client.exceptions.ExecutorException;
import org.apache.samza.sql.client.interfaces.SqlExecutor;
import org.apache.samza.sql.client.exceptions.CliException;
import org.apache.samza.sql.client.util.CliUtil;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.UserInterruptException;
import org.jline.reader.impl.DefaultParser;
import org.jline.reader.impl.completer.StringsCompleter;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.jline.utils.InfoCmp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The shell UI.
 */
public class CliShell {
  private static final Logger LOG = LoggerFactory.getLogger(CliShell.class);
  private final Terminal terminal;
  private final PrintWriter writer;
  private final LineReader lineReader;
  private final String firstPrompt;
  private SqlExecutor executor;
  private List<CommandHandler> commandHandlers;
  private final ExecutionContext exeContext;
  private boolean keepRunning = true;

  CliShell(CliEnvironment environment) throws ExecutorException {
    if (environment == null) {
      throw new IllegalArgumentException();
    }

    // Terminal
    try {
      terminal = TerminalBuilder.builder().name(CliConstants.WINDOW_TITLE).build();
    } catch (IOException e) {
      throw new CliException("Error when creating terminal", e);
    }

    // Terminal writer
    writer = terminal.writer();

    // LineReader
    final DefaultParser parser = new DefaultParser().eofOnEscapedNewLine(true).eofOnUnclosedQuote(true);
    lineReader = LineReaderBuilder.builder()
        .appName(CliConstants.APP_NAME)
        .terminal(terminal)
        .parser(parser)
        .highlighter(new CliHighlighter())
        .completer(new StringsCompleter(CliCommandType.getAllCommands()))
        .build();

    // Command Prompt
    firstPrompt = new AttributedStringBuilder().style(AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW))
        .append(CliConstants.PROMPT_1ST + CliConstants.PROMPT_1ST_END)
        .toAnsi();

    // Execution context and executor
    executor = environment.getExecutor();
    exeContext = new ExecutionContext();
    executor.start(exeContext);

    // Command handlers
    if (commandHandlers == null) {
      commandHandlers = new ArrayList<>();
    }
    commandHandlers.add(new CliCommandHandler());
    commandHandlers.addAll(environment.getCommandHandlers());
    for (CommandHandler commandHandler : commandHandlers) {
      LOG.info("init commandHandler {}", commandHandler.getClass().getName());
      commandHandler.init(this, environment, terminal, exeContext);
    }
  }

  Terminal getTerminal() {
    return terminal;
  }

  SqlExecutor getExecutor() {
    return executor;
  }

  ExecutionContext getExeContext() {
    return exeContext;
  }

  /**
   * Actually run the shell. Does not return until user choose to exit.
   */
  void open(String message) {
    // Remember we cannot enter alternate screen mode here as there is only one alternate
    // screen and we need it to show streaming results. Clear the screen instead.
    clearScreen();
    writer.write(CliConstants.WELCOME_MESSAGE);
    printVersion();
    if (!CliUtil.isNullOrEmpty(message)) {
      writer.println(message);
    }
    writer.println();

    try {
      // Check if jna.jar exists in class path
      try {
        ClassLoader.getSystemClassLoader().loadClass("com.sun.jna.NativeLibrary");
      } catch (ClassNotFoundException e) {
        // Something's wrong. It could be a dumb terminal if neither jna nor jansi lib is there
        writer.write("Warning: jna.jar does NOT exist. It may lead to a dumb shell or a performance hit.\n");
      }

      while (keepRunning) {
        String line;
        try {
          line = lineReader.readLine(firstPrompt);
        } catch (UserInterruptException e) {
          continue;
        } catch (EndOfFileException e) {
          keepRunning = false;
          break;
        }

        if (CliUtil.isNullOrEmpty(line))
          continue;

        String helpCmdText = CliCommandType.HELP.getCommandName();
        if (line.toUpperCase().startsWith(helpCmdText)) {
          if (line.toLowerCase().trim().equals(helpCmdText)) {
            printHelpMessage();
          } else {
            CommandAndHandler commandAndHandler = findHandlerForCommand(line.substring(helpCmdText.length()));
            if (commandAndHandler.handler != null) {
              commandAndHandler.handler.handleCommand(commandAndHandler.handler.parseLine(line));
            } else {
              printHelpMessage();
            }
          }
          continue;
        }

        CommandAndHandler commandAndHandler = null;
        try {
          commandAndHandler = findHandlerForCommand(line);
          if (commandAndHandler.handler == null) {
            LOG.info("no commandHandler found for command {}", line);
            printHelpMessage();
            continue;
          }
          keepRunning = commandAndHandler.handler.handleCommand(commandAndHandler.command);
        } catch (CommandHandlerException e) {
          writer.println("Error: " + e);
          LOG.error("Error in {}: ", commandAndHandler.command.getCommandType(), e);
          writer.flush();
        }
      }
    } catch (Exception e) {
      writer.print(e.getClass().getSimpleName());
      writer.print(". ");
      writer.println(e.getMessage());
      e.printStackTrace(writer);
      writer.println();
      writer.println("We are sorry but SamzaSqlShell has encountered a problem and must stop.");
    }

    writer.write("Cleaning up... ");
    writer.flush();
    try {
      executor.stop(exeContext);
      writer.write("Done.\nBye.\n\n");
      writer.flush();
      terminal.close();
    } catch (IOException | ExecutorException e) {
      // Doesn't matter
    }
  }

  private void printVersion() {
    String version = String.format("Shell version %s, Executor is %s, version %s",
            this.getClass().getPackage().getImplementationVersion(),
            executor.getClass().getName(),
            executor.getVersion());
    writer.println(version);
  }

  private void clearScreen() {
    terminal.puts(InfoCmp.Capability.clear_screen);
  }

  private void printHelpMessage() {
    for (CommandHandler commandHandler : commandHandlers) {
      commandHandler.printHelpMessage();
    }
    writer.println("HELP <COMMAND> to get help for a specific command.");
    writer.flush();
  }

  private CommandAndHandler findHandlerForCommand(String line) {
    CommandHandler commandHandler = null;
    CliCommand parsedCommand = null;
    for (CommandHandler curCommandHandler : commandHandlers) {
      parsedCommand = curCommandHandler.parseLine(line);
      if (parsedCommand != null && !parsedCommand.getCommandType().getCommandName().equals(CliCommandType.INVALID_COMMAND.getCommandName())) {
        commandHandler = curCommandHandler;
        LOG.info("Found commandHandler {} to handle command {}", commandHandler.getClass().getName(),
            parsedCommand.getFullCommand());
        break;
      }
    }
    return new CommandAndHandler(parsedCommand, commandHandler);
  }

  class CommandAndHandler {
    CliCommand command;
    CommandHandler handler;

    CommandAndHandler(CliCommand aCommand, CommandHandler itsHandler) {
      command = aCommand;
      handler = itsHandler;
    }
  }
}