/**
 * Copyright 2015-2017 Red Hat, Inc, and individual contributors.
 *
 * Licensed 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.wildfly.swarm.cli;

import java.io.File;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.function.Supplier;

import javax.enterprise.inject.Vetoed;

import org.wildfly.swarm.internal.SwarmMessages;

/**
 * A single option specification.
 *
 * <p>An option may have a short (single-dash) or long (double-dash) variant.</p>
 *
 * <p>Each option may optionally take a value, which may or may not be allowed
 * to be separated from the option.</p>
 *
 * <p>For instance, if {@link #valueMayBeSeparate(boolean)} is specified as false,
 * then only <code>-Dwhatever</code> would be allowed, while <code>-D whatever</code>
 * would not.</p>
 *
 * <p>Upon successful match, each <code>Option</code> has an {@link Action} associated
 * which is called with the value (if any) and the state-holding {@link CommandLine}
 * object.</p>
 *
 * @author Bob McWhirter
 */
@Vetoed
public class Option<T> {

    private static final String EQUALS = "=";

    private static final String HYPHEN = "-";

    private static final String SPACE = " ";

    private static final String DOUBLE_HYPHEN = "--";

    private static final int MAX_LINE_LENGTH = 50;

    /**
     * Construct an empty option.
     */
    public Option() {
    }

    /**
     * Associate an action with this option.
     *
     * @param action The action to call when matched.
     * @return This <code>Option</code> object.
     */
    public Option<T> then(Action<T> action) {
        this.action = action;
        return this;
    }

    /**
     * Display formatted help associated with this option.
     *
     * @param out The output stream to display the help upon.
     */
    public void displayHelp(PrintStream out) {
        out.println(merge(summary(), description()));
    }

    private List<String> summary() {
        List<String> list = new ArrayList<>();

        if (this.shortArg != null) {
            if (hasValue()) {
                if (this.valueDescription.contains(EQUALS)) {
                    list.add(HYPHEN + this.shortArg + this.valueDescription);
                } else {
                    list.add(HYPHEN + this.shortArg + EQUALS + this.valueDescription);
                }
                if (this.valueMayBeSeparate) {
                    if (this.valueDescription.contains(EQUALS)) {
                        list.add(HYPHEN + this.shortArg + this.valueDescription.replace(EQUALS, SPACE));
                    } else {
                        list.add(HYPHEN + this.shortArg + SPACE + this.valueDescription);
                    }
                }
            } else {
                list.add(HYPHEN + this.shortArg);
            }
        }
        if (this.longArg != null) {
            if (hasValue()) {
                if (this.valueDescription.contains(EQUALS)) {
                    list.add(DOUBLE_HYPHEN + this.longArg + this.valueDescription);
                } else {
                    list.add(DOUBLE_HYPHEN + this.longArg + EQUALS + this.valueDescription);
                }
                if (this.valueMayBeSeparate) {
                    if (this.valueDescription.contains(EQUALS)) {
                        list.add(DOUBLE_HYPHEN + this.longArg + this.valueDescription.replace(EQUALS, SPACE));
                    } else {
                        list.add(DOUBLE_HYPHEN + this.longArg + SPACE + this.valueDescription);
                    }
                }
            } else {
                list.add(DOUBLE_HYPHEN + this.longArg);
            }
        }
        return list;
    }

    private List<String> description() {
        List<String> list = new ArrayList<>();
        StringTokenizer tokens = new StringTokenizer(this.description);


        StringBuilder line = new StringBuilder();

        while (tokens.hasMoreElements()) {
            String token = tokens.nextToken();

            if (line.length() + (SPACE + token).length() > MAX_LINE_LENGTH) {
                list.add(line.toString());
                line = new StringBuilder();
            }

            if (line.length() != 0) {
                line.append(SPACE);
            }
            line.append(token);
        }

        list.add(line.toString());

        return list;
    }

    private String merge(List<String> summary, List<String> desc) {
        if (summary.size() > desc.size()) {
            int diff = summary.size() - desc.size();

            for (int i = 0; i < diff; ++i) {
                desc.add("");
            }
        }

        if (desc.size() > summary.size()) {
            int diff = desc.size() - summary.size();

            for (int i = 0; i < diff; ++i) {
                summary.add("");
            }
        }

        StringBuilder txt = new StringBuilder();

        for (int i = 0; i < summary.size(); ++i) {
            txt.append(String.format("  %-30s     %s", summary.get(i), desc.get(i)));
            txt.append("\n");
        }

        return txt.toString();
    }


    /**
     * Specify a short (single-dash) variant.
     *
     * <p>A single character following a single dash, such as <code>-c</code>.</p>
     *
     * @param shortArg The character.
     * @return This <code>Option</code> object.
     */
    public Option<T> withShort(Character shortArg) {
        this.shortArg = shortArg;
        return this;
    }

    /**
     * Specify a long (double-dash) variant.
     *
     * <p>A string following a double-dash, such as <code>--help</code>.</p>
     *
     * @param longArg The string (without dashes).
     * @return This <code>Option</code> object.
     */
    public Option<T> withLong(String longArg) {
        this.longArg = longArg;
        return this;
    }


    /**
     * Set a human-readable description of this option.
     *
     * @param description The description.
     * @return This <code>Option</code> object.
     */
    public Option<T> withDescription(String description) {
        this.description = description;
        return this;
    }

    /**
     * Specify that this option takes an argument value.
     *
     * <p>A non-null string passed to this method will be used to signify
     * that this option may take an argument, and to describe its format.</p>
     *
     * <p>For instance a string of <code>name[=value]</code></p> would indicate
     * that a <code>name</code> is expected, with an optional <code>value</code>
     * which should follow an equal sign.</p>
     *
     * @param valueDescription
     * @return
     */
    public Option<T> hasValue(String valueDescription) {
        this.valueDescription = valueDescription;
        return this;
    }

    private boolean hasValue() {
        return this.valueDescription != null;
    }

    /**
     * Indicate if the value to the option may be separated by whitespace. (defaults to <code>true</code>).
     *
     * <p>In some cases, because of ambiguity, it may be required to indicate that an
     * argument may <b>not</b> be separate from the option.  If an argument is to be
     * provided, it must immediately follow the option without whitespace.</p>
     *
     * @param valueMayBeSeparate <code>true</code> (the default) if value may be separate, or <code>false</code> to indicate it must be conjoined.
     * @return This <code>Option</code> object.
     */
    public Option<T> valueMayBeSeparate(boolean valueMayBeSeparate) {
        this.valueMayBeSeparate = valueMayBeSeparate;
        return this;
    }

    /**
     * Specify a default value supplier for this option.
     *
     * @param supplier The supplier.
     * @return This <code>Option</code> object.
     */
    public Option<T> withDefault(Supplier<T> supplier) {
        this.supplier = supplier;
        return this;
    }

    T defaultValue() {
        if (this.supplier == null) {
            return null;
        }
        return this.supplier.get();
    }

    boolean parse(ParseState state, CommandLine commandLine) throws Exception {
        String cur = state.la();

        if (cur == null) {
            return false;
        }

        String value = null;

        String matchedArg = null;

        if (this.shortArg != null && cur.startsWith(HYPHEN + this.shortArg)) {
            matchedArg = HYPHEN + this.shortArg;
            if (hasValue() && cur.length() >= 3) {
                if (cur.charAt(2) == '=') {
                    value = cur.substring(3);
                } else {
                    value = cur.substring(2);
                }
            }
        } else if (this.longArg != null && cur.startsWith(DOUBLE_HYPHEN + this.longArg)) {
            matchedArg = DOUBLE_HYPHEN + this.longArg;
            if (hasValue() && cur.length() >= this.longArg.length() + 3) {
                if (cur.charAt(this.longArg.length() + 2) == '=') {
                    value = cur.substring(this.longArg.length() + 3);
                } else {
                    value = cur.substring(this.longArg.length() + 2);
                }
            }
        } else {
            return false;
        }

        state.consume();

        if (value != null && value.trim().isEmpty()) {
            value = null;
        }

        if (hasValue() && value == null && !this.valueMayBeSeparate) {
            throw SwarmMessages.MESSAGES.argumentRequired(matchedArg);
        }

        if (hasValue() && value == null) {
            if (state.la() == null) {
                throw SwarmMessages.MESSAGES.argumentRequired(matchedArg);
            }
            value = state.consume();
        }

        this.action.set(commandLine, this, value);

        return true;
    }

    public String toString() {
        return "[Option: short=" + this.shortArg + "; long=" + this.longArg + "; valueDescription=" + this.valueDescription + "]";
    }

    /**
     * Helper to attempt forming a URL from a String in a sensible fashion.
     *
     * @param value The input string.
     * @return The correct URL, if possible.
     */
    public static URL toURL(String value) throws MalformedURLException {
        try {
            URL url = new URL(value);
            return url;
        } catch (MalformedURLException e) {
            try {
                return new File(value).toURI().toURL();
            } catch (MalformedURLException e2) {
                // throw the original
                throw e;
            }
        }
    }

    private Character shortArg;

    private String longArg;

    private String valueDescription;

    private boolean valueMayBeSeparate = true;

    private Action<T> action;

    private String description;

    private Supplier<T> supplier;


    /**
     * Callback functional interface for matched options.
     */
    public interface Action<T> {
        /**
         * Perform some action, being passed the value and the <code>CommandLine</code>.
         *
         * @param commandLine The state-holding <code>CommandLine</code> object.
         * @param option      The matched option.
         * @param value       The value to the option, if any.  Possibly <code>null</code>.
         * @throws if an error occurs
         */
        void set(CommandLine commandLine, Option<T> option, String value) throws Exception;
    }
}
