package com.csforge.sstable;

import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Resources;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigRenderOptions;
import com.typesafe.config.ConfigValueFactory;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import jline.console.completer.*;
import jline.console.history.FileHistory;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.cql3.statements.CreateTableStatement;
import org.apache.cassandra.cql3.statements.CreateTypeStatement;
import org.apache.cassandra.cql3.statements.ParsedStatement;
import org.apache.cassandra.cql3.statements.SelectStatement;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.db.rows.Unfiltered;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.exceptions.SyntaxException;
import org.apache.commons.cli.*;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static com.csforge.sstable.TableTransformer.*;

/**
 * This is just an early hacking - proof of concept (don't judge)
 * <p/>
 * - TODO : REWRITE EVERYTHING FROM SCRATCH
 * <p/>
 * - TODO : UDTs, and UDFs
 * - TODO : EXPAND ON; like cqlsh for wide rows/tiny consoles
 * - TODO : File completer on sstable use/select
 */
public class Cqlsh {

    private static final Options options = new Options();

    private static final String SCHEMA_OPTION = "s";

    private static final String FILE_OPTION = "f";

    private static final String MISSING_SSTABLES = errorMsg("No sstables set. Set the sstables using the 'USE pathToSSTable' command.");

    private static final String QUERY_PAGING_ALREADY_ENABLED = errorMsg("Query paging is already enabled. Use PAGING OFF to disable.");

    private static final String QUERY_PAGING_ALREADY_DISABLED = errorMsg("Query paging is not enabled.");

    private static final String IMPROPER_PAGING_COMMAND = errorMsg("Improper PAGING command.");

    private static final String QUERY_PAGING_ENABLED = "Now Query paging is enabled%nPage size: %d%n";

    private static final String QUERY_PAGING_DISABLED = "Disabled Query paging.";

    private static final String PAGING_IS_ENABLED = "Query paging is currently enabled. Use PAGING OFF to disable%nPage size: %d%n";

    private static final String PAGING_IS_DISABLED = "Query paging is currently disabled. Use PAGING ON to enable.";

    private static final String CANNOT_FIND_FILE = errorMsg("Cannot find '%s'.%n");

    private static final String IMPORTED_SCHEMA = "Successfully imported schema from '%s'.%n";

    private static final String FAILED_TO_IMPORT_SCHEMA = errorMsg("Could not import schema from '%s': %s.%n");

    private static final String PERSISTENCE_NOTE = infoMsg("To not persist settings, use PERSIST OFF.");

    private static final String PREFERENCES_ARE_DISABLED = "Preferences are currently disabled.";

    private static final String PREFERENCES_ARE_ENABLED = "Preferences are currently enabled:%n%s";

    private static final String PREFERENCES_ALREADY_ENABLED = errorMsg("Preferences are already enabled.  Using PERSIST OFF to disable.");

    private static final String PREFERENCES_ALREADY_DISABLED = errorMsg("Preferences are not enabled.");

    private static final String PREFERENCES_ENABLED = "Now Preferences are enabled:%n%s";

    private static final String PREFERENCES_DISABLED = "Disabled Preferences.";

    private static final String DISABLING_SCHEMA = "Disabling user-defined schema and using sstable metadata instead.";

    private static final String USER_DEFINED_SCHEMA = "User-defined schema is:%n%s%n";

    private static final String NO_USER_DEFINED_SCHEMA = "No user-defined schema, using sstable metadata instead.";

    private static final File CONFIG_DIR = new File(System.getProperty("user.home"), ".sstable-tools");

    private static final File HISTORY_FILE = new File(CONFIG_DIR, ".history");

    private static final File PREFERENCES_FILE = new File(CONFIG_DIR, ".preferences.conf");

    private static final String PROP_SSTABLES = "sstables";

    private static final String PROP_SCHEMA = "schema";

    private static final String PROP_PAGING_ENABLED = "pagingEnabled";

    private static final String PROP_PAGING_SIZE = "pagingSize";

    private static final String PROP_PREFERENCES_ENABLED = "preferencesEnabled";

    private static final String MORE = "\n---MORE--- [enter to continue, ctrl-c to break]";

    static {
        if (!CONFIG_DIR.exists()) {
            boolean created = CONFIG_DIR.mkdirs();
            if (!created) {
                System.err.println("Failed to create preferences directory: " + CONFIG_DIR);
            }
        }
        DatabaseDescriptor.clientInitialization(false);
    }

    static String errorMsg(String msg) {
        return ANSI_RED + msg + ANSI_RESET;
    }

    static String infoMsg(String msg) {
        return ANSI_CYAN + msg + ANSI_RESET;
    }

    static {
        Option schemaOption = new Option(SCHEMA_OPTION, true, "Schema file to use.");
        schemaOption.setRequired(false);
        Option fileOption = new Option(FILE_OPTION, true, "Execute commands from FILE, then exit.");
        options.addOption(schemaOption);
        options.addOption(fileOption);
    }

    public Set<File> sstables = Sets.newHashSet();
    public FileHistory history = null;
    private final String prompt = "\u001B[1;33mcqlsh\u001B[33m> \u001B[0m";
    public CFMetaData metadata = null;
    private boolean done = false;
    ConsoleReader console;

    String innerBuffer;
    boolean inner = false;
    boolean paging = true;
    int pageSize = 100;
    boolean preferences = true;
    Config config;

    public Cqlsh() {
        try {
            Config applicationConfig = ConfigFactory.defaultApplication();
            config = ConfigFactory.parseFile(PREFERENCES_FILE).withFallback(applicationConfig);
            paging = config.getBoolean(PROP_PAGING_ENABLED);
            pageSize = config.getInt(PROP_PAGING_SIZE);
            sstables = config.getStringList(PROP_SSTABLES).stream().map(File::new).filter(f -> {
                if (!f.exists()) {
                    System.err.printf(CANNOT_FIND_FILE, f.getAbsolutePath());
                }
                return f.exists();
            }).collect(Collectors.toSet());

            boolean persistInfo = false;
            if (sstables.size() > 0) {
                System.out.println(infoMsg("Using previously defined sstables: " + sstables));
                persistInfo = true;
            }

            preferences = config.getBoolean(PROP_PREFERENCES_ENABLED);
            String schema = config.getString(PROP_SCHEMA);
            if (!Strings.isNullOrEmpty(schema)) {
                System.out.printf(infoMsg("Using previously defined schema:%n%s%n"), schema);
                persistInfo = true;
                CassandraUtils.cqlOverride = schema;
            }

            if (persistInfo) {
                System.out.println(PERSISTENCE_NOTE);
            }

            history = new FileHistory(HISTORY_FILE);
            console = new ConsoleReader();
            console.setPrompt(prompt);
            console.setHistory(history);
            console.setHistoryEnabled(true);
            List<Completer> completers = Lists.newArrayList();

            ArgumentCompleter argCompleter = new ArgumentCompleter(
                    caselessCompleter("use"),
                    new FileNameCompleter()
            );
            completers.add(argCompleter);
            argCompleter = new ArgumentCompleter(
                    caselessCompleter("describe"),
                    caselessCompleter("sstables", "schema")
            );
            completers.add(argCompleter);
            argCompleter = new ArgumentCompleter(
                    caselessCompleter("dump"),
                    caselessCompleter("where")
            );
            completers.add(argCompleter);
            argCompleter = new ArgumentCompleter(
                    caselessCompleter("create"),
                    caselessCompleter("table")
            );
            completers.add(argCompleter);
            argCompleter = new ArgumentCompleter(
                    caselessCompleter("paging"),
                    caselessCompleter("on", "off")
            );
            completers.add(argCompleter);
            argCompleter = new ArgumentCompleter(
                    caselessCompleter("persist"),
                    caselessCompleter("on", "off")
            );
            completers.add(argCompleter);
            argCompleter = new ArgumentCompleter(
                    caselessCompleter("schema"),
                    new AggregateCompleter(new FileNameCompleter(), caselessCompleter("on", "off"))
            );
            completers.add(argCompleter);
            completers.add(caselessCompleter("exit", "help", "select"));
            for (Completer c : completers) {
                console.addCompleter(c);
            }
            console.setHandleUserInterrupt(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static AggregateCompleter caselessCompleter(String... args) {
        return new AggregateCompleter(
                new StringsCompleter(Arrays.stream(args).map(String::toUpperCase).collect(Collectors.toList())),
                new StringsCompleter(Arrays.stream(args).map(String::toLowerCase).collect(Collectors.toList()))
        );
    }

    public void startShell() throws Exception {
        try {
            String line = null;
            while (!done && (line = console.readLine()) != null) {
                evalLine(line);
            }
            if (line == null) {
                done = true;
            }
        } catch (IOException e) {
            System.err.println("Error in console session: " + e.getMessage());
            System.exit(-4);
        }
    }

    public void doUse(String command) throws Exception {
        String rest = command.substring(4); // "USE "
        Pattern p = Pattern.compile("((\"[^\"]+\")|[^\" ]+)");
        Matcher m = p.matcher(rest);
        this.sstables = Sets.newHashSet();

        while (m.find()) {
            String arg = m.group().replace("\"", "");
            File sstable = new File(arg);
            if (sstable.exists() && sstable.isFile()) {
                this.sstables.add(sstable);
            } else {
                try {
                    if (sstable.exists()) {
                        PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/*-Data.db");
                        Files.walkFileTree(Paths.get(arg), Sets.newHashSet(), 1, new SimpleFileVisitor<Path>() {
                            public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
                                if (matcher.matches(path)) {
                                    sstables.add(path.toFile());
                                }
                                return FileVisitResult.CONTINUE;
                            }

                            public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                                return FileVisitResult.CONTINUE;
                            }
                        });
                    } else {
                        System.err.printf(CANNOT_FIND_FILE, sstable.getAbsolutePath());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        for (File f : sstables) {
            System.out.println("Using: " + f.getAbsolutePath());
        }
        if (!sstables.isEmpty()) {
            metadata = CassandraUtils.tableFromBestSource(sstables.iterator().next());
        }
    }

    public void doSchema(String command) throws Exception {
        String path = command.substring(6).trim().replaceAll("\"", "");

        if (path.equalsIgnoreCase("off")) {
            System.out.println(DISABLING_SCHEMA);
            CassandraUtils.cqlOverride = null;
        } else if (Strings.isNullOrEmpty(path)) {
            if (!Strings.isNullOrEmpty(CassandraUtils.cqlOverride)) {
                System.out.printf(USER_DEFINED_SCHEMA, CassandraUtils.cqlOverride);
            } else {
                System.out.println(NO_USER_DEFINED_SCHEMA);
            }
        } else {
            File schemaFile = new File(path);
            if (!schemaFile.exists()) {
                System.err.printf(CANNOT_FIND_FILE, schemaFile.getAbsolutePath());
            } else {
                String cql = new String(Files.readAllBytes(schemaFile.toPath()));
                try {
                    ParsedStatement statement = QueryProcessor.parseStatement(cql);
                    if (statement instanceof CreateTableStatement.RawStatement) {
                        CassandraUtils.cqlOverride = cql;
                        System.out.printf(IMPORTED_SCHEMA, schemaFile.getAbsolutePath());
                    } else {
                        System.err.printf(FAILED_TO_IMPORT_SCHEMA, schemaFile.getAbsoluteFile(), "Wrong type of statement, " + statement.getClass());
                    }
                } catch (SyntaxException se) {
                    System.err.printf(FAILED_TO_IMPORT_SCHEMA, schemaFile.getAbsoluteFile(), se.getMessage());
                }
            }
        }

    }

    public void doDump(String command) throws Exception {
        if (sstables.isEmpty()) {
            System.out.println(MISSING_SSTABLES);
            return;
        }
        Query query;
        if (command.length() > 5) {
            query = getQuery("select * from sstables " + command.substring(5));
        } else {
            query = getQuery("select * from sstables");
        }

        console.setHistoryEnabled(false);
        AtomicInteger totalRows = new AtomicInteger(0);
        try (UnfilteredPartitionIterator scanner = query.getScanner()) {
            int limit = Integer.MAX_VALUE;
            if (query.statement.limit != null && !query.selection.isAggregate()) {
                limit = Integer.parseInt(query.statement.limit.getText());
            }
            AtomicInteger rowsPaged = new AtomicInteger(0);
            Function<Void, Void> deferToInput = o -> {
                if (paging && rowsPaged.get() >= pageSize) {
                    rowsPaged.set(0);
                    try {
                        String input = console.readLine(MORE, ' ');
                        if (input == null) {
                            totalRows.set(Integer.MAX_VALUE);
                        }
                    } catch (UserInterruptException uie) {
                        // User interrupted, stop paging.
                        totalRows.set(Integer.MAX_VALUE);
                    } catch (IOException e) {
                        System.err.println(errorMsg("Error while paging:" + e.getMessage()));
                    }
                }
                return null;
            };

            while (scanner.hasNext() && totalRows.get() < limit) {
                UnfilteredRowIterator partition = scanner.next();
                deferToInput.apply(null);
                if (!partition.partitionLevelDeletion().isLive() && totalRows.get() < limit) {
                    System.out.println("[" + metadata.getKeyValidator().getString(partition.partitionKey().getKey()) + "] " +
                            partition.partitionLevelDeletion());
                    rowsPaged.incrementAndGet();
                    totalRows.incrementAndGet();
                }
                deferToInput.apply(null);
                if (!partition.staticRow().isEmpty() && totalRows.get() < limit) {
                    System.out.println(
                            "[" + metadata.getKeyValidator().getString(partition.partitionKey().getKey()) + "] " +
                                    partition.staticRow().toString(metadata, true));
                    rowsPaged.incrementAndGet();
                    totalRows.incrementAndGet();
                }
                deferToInput.apply(null);
                while (partition.hasNext() && totalRows.get() < limit) {
                    Unfiltered row = partition.next();
                    System.out.println(
                            "[" + metadata.getKeyValidator().getString(partition.partitionKey().getKey()) + "] " +
                                    row.toString(metadata, true));
                    rowsPaged.incrementAndGet();
                    totalRows.incrementAndGet();
                    deferToInput.apply(null);
                }
            }
        } finally {
            if (totalRows.get() < Integer.MAX_VALUE) {
                System.out.printf("%n(%s rows)%n", totalRows.get());
            }
            console.setHistoryEnabled(true);
            console.setPrompt(prompt);
        }
    }

    public void doQuery(String command) throws Exception {
        Query q = getQuery(command);
        System.out.println();
        if (q == null) {
            System.out.println(MISSING_SSTABLES);
        } else if (paging) {
            ResultSetData resultData = getQuery(command).getResults(pageSize);
            TableTransformer.dumpResults(metadata, resultData.getResultSet(), System.out);
            boolean terminated = false;
            if (resultData.getPagingData().hasMorePages()) {
                console.setHistoryEnabled(false);
                while (resultData.getPagingData().hasMorePages()) {
                    try {
                        String input = console.readLine(MORE, ' ');
                        if (input == null) {
                            done = true;
                            terminated = true;
                            break;
                        }
                    } catch (UserInterruptException uie) {
                        // User interrupted, stop paging.
                        terminated = true;
                        break;
                    }
                    resultData = getQuery(command).getResults(pageSize, resultData.getPagingData());
                    TableTransformer.dumpResults(metadata, resultData.getResultSet(), System.out);
                }
            }
            if (!terminated) {
                System.out.printf("%n(%s rows)%n", resultData.getPagingData().getRowCount());
            }
            console.setHistoryEnabled(true);
            console.setPrompt(prompt);
        } else {
            ResultSetData resultData = getQuery(command).getResults();
            TableTransformer.dumpResults(metadata, resultData.getResultSet(), System.out);
            System.out.printf("%n(%s rows)%n", resultData.getPagingData().getRowCount());
        }
    }

    public void doCreate(String command) throws Exception {
        innerBuffer = command;
        inner = true;
        history.removeLast();
        console.setHistoryEnabled(false);
        console.setPrompt("...    ");
        try {
            while (inner) {
                try {
                    ParsedStatement statement = QueryProcessor.parseStatement(innerBuffer);
                    inner = false;
                    history.add(innerBuffer);
                    if (statement instanceof CreateTableStatement.RawStatement) {
                        CassandraUtils.cqlOverride = innerBuffer;
                    } else if (statement instanceof CreateTypeStatement) {
                        try {
                            // TODO work around type mess
                            System.out.println(CassandraUtils.callPrivate(statement, "createType"));
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    //CreateTableStatement$RawStatement
                } catch (SyntaxException e) {
                    if (!innerBuffer.trim().endsWith(";")) {
                        String line = console.readLine();
                        if (!inner) {
                            evalLine(line);
                        } else {
                            innerBuffer += " " + line;
                        }
                    } else {
                        inner = false;
                        System.out.println(ANSI_RED + e.getMessage() + ANSI_RESET);
                    }
                }
            }
        } finally {
            console.setHistoryEnabled(true);
            console.setPrompt(prompt);
            innerBuffer = "";
            inner = false;
        }
    }

    public void doPagingConfig(String command) throws Exception {
        String mode = command.substring(6).trim().toLowerCase();
        switch (mode) {
            case "":
                if (paging) {
                    System.out.printf(PAGING_IS_ENABLED, pageSize);
                } else {
                    System.out.println(PAGING_IS_DISABLED);
                }
                break;
            case "on":
                if (paging) {
                    System.err.println(QUERY_PAGING_ALREADY_ENABLED);
                } else {
                    paging = true;
                    System.out.printf(QUERY_PAGING_ENABLED, pageSize);
                }
                break;
            case "off":
                if (!paging) {
                    System.err.println(QUERY_PAGING_ALREADY_DISABLED);
                } else {
                    paging = false;
                    System.out.println(QUERY_PAGING_DISABLED);
                }
                break;
            default:
                try {
                    pageSize = Integer.parseInt(mode);
                    paging = true;
                    System.out.printf(QUERY_PAGING_ENABLED, pageSize);
                } catch (NumberFormatException e) {
                    System.err.println(IMPROPER_PAGING_COMMAND);
                }
        }
    }

    public void doPersistConfig(String command) {
        String mode = command.substring(7).trim().toLowerCase();

        switch (mode) {
            case "":
                if (preferences) {
                    System.out.printf(PREFERENCES_ARE_ENABLED, generatePreferencesConfigString(false));
                } else {
                    System.out.println(PREFERENCES_ARE_DISABLED);
                }
                break;
            case "on":
                if (preferences) {
                    System.err.println(PREFERENCES_ALREADY_ENABLED);
                } else {
                    preferences = true;
                    System.out.printf(PREFERENCES_ENABLED, generatePreferencesConfigString(false));
                }
                break;
            case "off":
                if (preferences) {
                    preferences = false;
                    System.out.println(PREFERENCES_DISABLED);
                } else {
                    System.err.println(PREFERENCES_ALREADY_DISABLED);
                }
                break;
        }
    }

    public Query getQuery(String command) throws Exception {
        SelectStatement.RawStatement statement = (SelectStatement.RawStatement) QueryProcessor.parseStatement(command);
        if (statement.columnFamily().matches("sstables?")) {
            if (sstables.isEmpty()) {
                return null;
            }
            metadata = CassandraUtils.tableFromBestSource(sstables.iterator().next());
            return new Query(command, sstables, metadata);
        } else {
            File path = new File(statement.columnFamily());
            if (!path.exists()) {
                throw new FileNotFoundException(path.getAbsolutePath());
            }
            metadata = CassandraUtils.tableFromBestSource(path);
            return new Query(command, Collections.singleton(path), metadata);
        }
    }

    public void evalLine(String line) throws Exception {
        // TODO: Handle semi-colons in quoted identifers.
        String cmds[] = line.split(";");
        for (String command : cmds) {
            command = command.trim();
            if (command.isEmpty()) {
                continue;
            } else if (command.equals("exit") || command.equals("quit")) {
                done = true;
                continue;
            } else if (command.toLowerCase().trim().startsWith("describe schema")) {
                if (CassandraUtils.cqlOverride != null) {
                    System.out.println(CassandraUtils.cqlOverride);
                } else if (metadata != null) {
                    System.out.println(metadata);
                } else {
                    System.err.format("%sNo current metadata set, use a CREATE TABLE statement to set%s%n",
                            ANSI_RED, ANSI_RESET);
                }
                continue;
            } else if (command.toLowerCase().trim().startsWith("describe sstable")) {
                System.out.println();
                for (File f : sstables) {
                    System.out.println("\u001B[1;34m" + f.getAbsolutePath());
                    System.out.println(ANSI_CYAN + Strings.repeat("=", f.getAbsolutePath().length()) + ANSI_RESET);
                    CassandraUtils.printStats(f.getAbsolutePath(), System.out, console);
                    System.out.println();
                }
                continue;
            } else if (command.toLowerCase().startsWith("use ")) {
                doUse(command);
                continue;
            } else if (command.toLowerCase().startsWith("dump")) {
                doDump(command);
                continue;
            } else if (command.toLowerCase().equals("help") || command.trim().equals("?")) {
                try {
                    System.out.println(Resources.toString(Resources.getResource("cqlsh-help"), Charsets.UTF_8));
                } catch (IOException e) {
                    e.printStackTrace();
                    System.exit(-5);
                }
                continue;
            } else if (command.toLowerCase().startsWith("persist")) {
                doPersistConfig(command);
                continue;
            } else if (command.length() >= 6) {
                String queryType = command.substring(0, 6).toLowerCase();
                switch (queryType) {
                    case "select":
                        doQuery(command);
                        continue;
                    case "create":
                        doCreate(command);
                        continue;
                    case "update":
                    case "insert":
                    case "delete":
                        System.err.format("%sQuery '%s' is not supported since this tool is read-only.%s%n",
                                ANSI_RED, command, ANSI_RESET);
                        continue;
                    case "paging":
                        doPagingConfig(command);
                        continue;
                    case "schema":
                        doSchema(command);
                        continue;
                }
            }
            System.err.format("%sUnknown command: %s%s%n", ANSI_RED, command, ANSI_RESET);
        }
    }


    public Config generatePreferencesConfig() {
        Config persistConfig;

        if (preferences) {
            persistConfig = this.config
                    .withValue(PROP_SSTABLES, ConfigValueFactory.fromIterable(sstables.stream().map(File::getAbsolutePath).collect(Collectors.toSet())))
                    .withValue(PROP_PAGING_ENABLED, ConfigValueFactory.fromAnyRef(paging))
                    .withValue(PROP_PAGING_SIZE, ConfigValueFactory.fromAnyRef(pageSize))
                    .withValue(PROP_PREFERENCES_ENABLED, ConfigValueFactory.fromAnyRef(preferences))
                    .withValue(PROP_SCHEMA, ConfigValueFactory.fromAnyRef(CassandraUtils.cqlOverride != null ? CassandraUtils.cqlOverride : ""));
        } else {
            persistConfig = ConfigFactory.empty()
                    .withValue(PROP_PREFERENCES_ENABLED, ConfigValueFactory.fromAnyRef(preferences));
        }

        return persistConfig;
    }

    public String generatePreferencesConfigString(boolean json) {
        return generatePreferencesConfig().root().render(ConfigRenderOptions.defaults().setJson(json).setOriginComments(false));
    }

    public void persistState() {
        String contents = generatePreferencesConfigString(true);
        try {
            Files.write(Paths.get(PREFERENCES_FILE.getAbsolutePath()), contents.getBytes());
        } catch (IOException e) {
            System.err.printf(errorMsg("Failed writing preferences file '%s': %s%n"),
                    PREFERENCES_FILE.getAbsolutePath(), e.getMessage());
        }
    }

    public static void main(String args[]) {
        CommandLineParser parser = new PosixParser();
        CommandLine cmd = null;
        try {
            cmd = parser.parse(options, args);
        } catch (ParseException e) {
            System.err.format("%sFailure parsing arguments: %s%s%n%n", ANSI_RED, e.getMessage(), ANSI_RESET);
            try (PrintWriter errWriter = new PrintWriter(System.err, true)) {
                HelpFormatter formatter = new HelpFormatter();
                formatter.printHelp(errWriter, 120, "cqlsh sstable [sstable ...]",
                        String.format("%nOffline CQL Shell for Apache Cassandra 3.x%nOptions:"),
                        options, 2, 1, "", true);
            } finally {
                System.exit(-1);
            }
        }

        final Cqlsh sh = new Cqlsh();

        String schemaPath = cmd.getOptionValue(SCHEMA_OPTION);
        if (schemaPath != null) {
            try (InputStream schemaStream = new FileInputStream(new File(schemaPath))) {
                sh.metadata = CassandraUtils.tableFromCQL(schemaStream);
            } catch (IOException e) {
                System.err.println("Error reading schema metadata: " + e.getMessage());
                System.exit(-2);
            }
            System.setProperty("sstabletools.schema", schemaPath);
        }

        List<File> sstables = Lists.newArrayList();
        for (String sstable : cmd.getArgs()) {
            File file = new File(sstable);
            if (!file.exists()) {
                System.err.println("Non-existant sstable file provided: " + sstable);
                System.exit(-3);
            }
            sstables.add(file);
        }

        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                try {
                    sh.persistState();
                    sh.history.flush();
                } catch (IOException e) {
                    System.err.println("Couldn't flush history on shutdown.  Reason:" + e.getMessage());
                }
            }
        });

        String cqlFilePath = cmd.getOptionValue(FILE_OPTION);
        if (cqlFilePath != null) {
            try (Scanner s = new Scanner(new FileInputStream(cqlFilePath))) {
                while (s.hasNextLine()) {
                    try {
                        sh.evalLine(s.nextLine());
                    } catch (Exception e) {
                        e.printStackTrace();
                        break;
                    }
                }
            } catch (FileNotFoundException ex) {
                System.err.println("Cannot find " + cqlFilePath);
                System.exit(-4);
            }
        } else {

            while (!sh.done) {
                try {
                    sh.startShell();
                } catch (UserInterruptException e) {
                } catch (RuntimeException e) {
                    if (e.getMessage().startsWith("Unknown column")) {
                        if(Strings.isNullOrEmpty(CassandraUtils.cqlOverride)) {
                            System.out.printf("%sUnknown Column. Likely the schema does not match across sstables. Try defining your schema with an appropriate '%sCREATE TABLE%s' statement.%s%n", ANSI_RED, "\u001B[1;31m", "\u001B[0;31m", ANSI_RESET);
                        } else {
                            System.out.printf("%sUnknown Column. Likely the schema is not correct for this sstable. Try '%sSCHEMA OFF%s' to use metadata from statistics.%s%n", ANSI_RED, "\u001B[1;31m", "\u001B[0;31m", ANSI_RESET);
                        }
                    } else {
                        e.printStackTrace();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            sh.console.shutdown();
        }

    }
}