package de.duehl.basics.logic;

/*
 * Copyright 2020 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.basics.text.Text;

/**
 * Dieses Klasse analysiert Parameter, die ein Programm von der Kommandozeile entgegen genommen hat.
 *
 * @version 1.01     2020-01-24
 * @author Christian Dühl
 */

public class CommandLineArguments {

    /**
     * Liste mit den Optionen (Parameter, die mit -- anfangen und vor einem echten Parameter
     * stehen) ohne einen mit Gleichheitszeichen zugeordneten Wert.
     *
     * Beispiel "--useMonospacedFont"
     */
    private final List<String> flagOptions;

    /**
     * Liste mit den Optionen (Parameter, die mit -- anfangen und vor einem echten Parameter
     * stehen) mit einem mit Gleichheitszeichen zugeordneten Wert.
     *
     * Beispiel "--processIdentifier=Pc179-1"
     */
    private final Map<String, String> valueOptions;

    /** Liste mit den echten Parametern. */
    private final List<String> parameters;

    /** Originale Kommandozeile. */
    private final String commandLine;

    /**
     * Konstruktor.
     *
     * @param args
     *            Array mit den Parametern von der Kommandozeile.
     */
    public CommandLineArguments(String[] args) {
        this(Arrays.asList(args));
    }

    /**
     * Konstruktor.
     *
     * @param args
     *            Liste mit den Parametern von der Kommandozeile.
     */
    public CommandLineArguments(List<String> args) {
        commandLine = buildCommandLine(args);
        flagOptions = analyseFlagOptions(args);
        valueOptions = analyseValueOptions(args);
        parameters = analyseParameters(args);
    }

    /**
     * Erzeugt die originale Kommandozeile aus den Parameter.
     *
     * @param args
     *            Liste mit den Parametern von der Kommandozeile.
     * @return Originale Kommandozeile.
     */
    private String buildCommandLine(List<String> args) {
        return Text.join(" ", args);
    }

    /**
     * Extrahiert die Optionen ohne zugeordneten Wert (Parameter die mit "--" beginnen) aus den
     * Aufrufparametern. Ein "--" für sich alleine zeigt an, dass darauf nur normale Parameter
     * folgen, selbst wenn diese mit einem "--" beginnen.
     *
     * @param args
     *            Liste mit den Aufrufparametern.
     * @return Liste mit den Optionen ohne zugeordneten Wert.
     */
    private static List<String> analyseFlagOptions(List<String> args) {
        List<String> flagOptions = new ArrayList<>();

        for (String arg : args) {
            if ("--".equals(arg)) {
                break;
            }
            if (arg.startsWith("--") && !arg.contains("=")) {
                flagOptions.add(arg);
            }
        }

        return flagOptions;
    }

    /**
     * Extrahiert die Optionen mit zugeordneten Wert (Parameter die mit "--" beginnen und ein
     * Gleichheitszeichen enthalten) aus den Aufrufparametern. Ein "--" für sich alleine zeigt an,
     * dass darauf nur normale Parameter folgen, selbst wenn diese mit einem "--" beginnen.
     *
     * @param args
     *            Liste mit den Aufrufparametern.
     * @return Liste mit den Optionen ohne zugeordneten Wert.
     */
    private static Map<String, String> analyseValueOptions(List<String> args) {
        Map<String, String> valueOptions = new HashMap<>();

        for (String arg : args) {
            if ("--".equals(arg)) {
                break;
            }
            if (arg.startsWith("--") && arg.contains("=")) {
                int eqIndex = arg.indexOf("=");
                String name = arg.substring(0, eqIndex);
                String value = arg.substring(eqIndex + 1);
                valueOptions.put(name, value);
            }
        }

        return valueOptions;
    }

    /**
     * Extrahiert die echten Parameter (Parameter, die nicht mit "--" beginnen) aus den
     * Aufrufparametern. Ein "--" für sich alleine zeigt an, dass darauf nur normale Parameter
     * folgen, selbst wenn diese mit einem "--" beginnen.
     *
     * @param args
     *            Liste mit den Aufrufparametern.
     * @return Liste mit den echten Aufrufparametern.
     */
    private static List<String> analyseParameters(List<String> args) {
        List<String> parameters = new ArrayList<>();
        boolean takeEverything = false;
        for (String arg : args) {
            if ("--".equals(arg)) {
                takeEverything = true;
            }
            else if (takeEverything || !arg.startsWith("--")) {
                parameters.add(arg);
            }
        }
        return parameters;
    }

    /**
     * Prüft, ob die angegebene Option in den Aufrufparametern gesetzt wurde.
     *
     * @param option
     *            Zu prüfende Option (mit führenden "--").
     * @return Wahrheitswert: true genau dann, wenn die Option gesetzt worden
     *         ist.
     */
    public boolean hasFlagOption(String option) {
        return flagOptions.contains(option);
    }

    /** Gibt an, ob Optionen ohne zugeordneten Wert vorhanden sind. */
    public boolean hasFlagOptions() {
        return !flagOptions.isEmpty();
    }

    /** Gibt an, ob die Option mit zugeordnetem Wert mit dem angegeben Namen vorhanden ist. */
    public boolean hasValueOption(String name) {
        return valueOptions.containsKey(name);
    }

    /** Gibt an, ob Optionen mit zugeordnetem Wert vorhanden sind. */
    public boolean hasValueOptions() {
        return !valueOptions.isEmpty();
    }

    /**
     * Gibt den Wert einer Option mit zugeordnetem Wert zurück.
     *
     * @param valueOption
     *            Name der Option.
     */
    public String getValueOption(String valueOption) {
        return valueOptions.get(valueOption);
    }

    /**
     * Getter für den Parameter mit dem angegeben Index (0-basierend).
     *
     * @param index
     *            Index des Parameters (0-basierend).
     * @return Entsprechender Parameter zum angegebenen Index.
     * @throws IndexOutOfBoundsException
     *             - falls der Index zu klein oder zu groß ist.
     */
    public String getParameter(int index) {
        return parameters.get(index);
    }

    /** Getter für die Anzahl der Optionen ohne zugeordneten Wert. */
    public int getNumberOfFlagOptions() {
        return flagOptions.size();
    }

    /** Getter für die Anzahl der Optionen mit zugeordnetem Wert. */
    public int getNumberOfValueOptions() {
        return valueOptions.size();
    }

    /** Getter für die Anzahl der Parameter. */
    public int getNumberOfParameters() {
        return parameters.size();
    }

    /**
     * Gibt an, ob die Anzahl der Parameter der angegebenen Nummer entspricht.
     *
     * @param number
     *            Gewünschte Anzahl von Parametern.
     * @return Wahrheitswert: true genau dann, wenn die gewünschte Anzahl von Parametern der realen
     *         Anzahl von Parametern entspricht.
     */
    public boolean numberOfParameterIs(int number) {
        return number == getNumberOfParameters();
    }

    /**
     * Gibt an, ob die Anzahl der Optionen ohne zugeordnete Werte der angegebenen Nummer entspricht.
     *
     * @param number
     *            Gewünschte Anzahl von Optionen.
     * @return Wahrheitswert: true genau dann, wenn die gewünschte Anzahl von Optionen der realen
     *         Anzahl von Optionen entspricht.
     */
    public boolean numberOfFlagOptionsIs(int number) {
        return number == getNumberOfFlagOptions();
    }

    /**
     * Gibt an, ob die Anzahl der Optionen mit zugeordneten Werten der angegebenen Nummer
     * entspricht.
     *
     * @param number
     *            Gewünschte Anzahl von Optionen.
     * @return Wahrheitswert: true genau dann, wenn die gewünschte Anzahl von Optionen der realen
     *         Anzahl von Optionen entspricht.
     */
    public boolean numberOfValueOptionsIs(int number) {
        return number == getNumberOfValueOptions();
    }

    /** Gibt an, ob keine Parameter vorhanden sind. */
    public boolean haveNoParameters() {
        return numberOfParameterIs(0);
    }

    /** Gibt an, ob keine Optionen ohne zugeordneten Wert vorhanden sind. */
    public boolean haveNoFlagOptions() {
        return numberOfFlagOptionsIs(0);
    }

    /** Gibt an, ob keine Optionen mit zugeordneten Werten vorhanden sind. */
    public boolean haveNoValueOptions() {
        return numberOfValueOptionsIs(0);
    }

    /**
     * Gibt an, ob irgendwie nach Hilfe gefragt wird. Getestet wird die Option --help und die
     * Parameter -h und /?.
     */
    public boolean haveHelpOption() {
        return hasFlagOption("--help") || hasParameter("-h") || hasParameter("/?");
    }

    /**
     * Prüft, ob der gegebene Parameter vorhanden ist.
     *
     * @param parameter
     *            Zu prüfender Parameter.
     * @return Wahrheitswert, true genau dann, wenn der Parameter in den Argumenten vorhanden ist.
     */
    private boolean hasParameter(String parameter) {
        return parameters.contains(parameter);
    }

    /** Getter für die Liste mit den echten Parametern. */
    public List<String> getParameters() {
        return parameters;
    }

    /** Erzeugt eine Darstellung der Optionen und Parameter. */
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();

        builder.append("    Optionen ohne zugeordnete Werte:\n");
        for (String flagOption : flagOptions) {
            builder.append("        ");
            builder.append(flagOption);
            builder.append("\n");
        }
        builder.append("    Optionen mit zugeordneten Werten:\n");
        for (String valueOption : CollectionsHelper.getSortedMapStringIndices(valueOptions)) {
            builder.append("        ");
            builder.append(valueOption);
            builder.append(" = ");
            builder.append(valueOptions.get(valueOption));
            builder.append("\n");
        }
        builder.append("    Parameter:\n");
        int count = 0;
        for (String parameter : parameters) {
            builder.append("        ");
            builder.append(Integer.toString(count++));
            builder.append(") ");
            builder.append(parameter);
            builder.append("\n");
        }

        return builder.toString();
    }

    /** Getter für die originale Kommandozeile. */
    public String getCommandLine() {
        return commandLine;
    }

}
