package com.jscape.util.cli.ext;

import com.jscape.util.Parameters;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import static com.jscape.util.Parameters.assertNotNull;

/**
 * A mutually exclusive command line option (-ftp|-ftps|-sftp).
 *
 * @author Alex Usun
 */
public class MutuallyExclusiveOption
        implements Option {

    private final List<String> values;
    private final boolean required;
    private String value;

    /**
     * Creates a new option instance for specified values.
     *
     * @param required an option requirement flag
     * @param values   possible option values
     *
     * @return a new option instance
     */
    public static MutuallyExclusiveOption forValues(boolean required, String... values) {
        return new MutuallyExclusiveOption(Arrays.asList(values), required);
    }

    public MutuallyExclusiveOption(List<String> values, boolean required, String value) {
        assertNotNull(values);
        this.values = values;

        this.required = required;
        this.value = value;
    }

    /**
     * Creates a new option instance.
     *
     * @param values   possible option values
     * @param required an option requirement flag
     */
    public MutuallyExclusiveOption(List<String> values, boolean required) {
        assertNotNull(values);
        this.values = values;

        this.required = required;
    }

    public MutuallyExclusiveOption(MutuallyExclusiveOption that) {
        this(that.values, that.required, that.value);
    }

    /**
     * Returns the possible option values.
     *
     * @return the possible option values
     */
    public List<String> values() {
        return this.values;
    }

    /**
     * Returns the option requirement flag.
     *
     * @return <code>true</code> if option is required; <code>false</code> otherwise
     */
    public boolean required() {
        return this.required;
    }

    public boolean present() {
        return this.value != null;
    }

    public void reset() {
        this.value = null;
    }

    /**
     * Returns the current option value.
     *
     * @return the current option value
     */
    public String value() {
        return this.value;
    }

    /**
     * Returns the current option value or specified default value if option not present.
     *
     * @param defaultValue a default value
     *
     * @return the option value
     */
    public String value(String defaultValue) {
        return present() ? value() : defaultValue;
    }

    /**
     * Sets an option value.
     *
     * @param value an option value
     */
    public void setValue(String value) {
        Parameters.assertTrue(accepts(value), "Invalid option value.");
        this.value = value;
    }

    /**
     * Checks if specified value is accepted by the option.
     *
     * @param value a value to check
     *
     * @return <code>true</code> if value is acceptable; <coade>false</coade> otherwise
     */
    public boolean accepts(String value) {
        return this.values.contains(value);
    }

    public MutuallyExclusiveOption copy() {
        return new MutuallyExclusiveOption(this);
    }

    @Override
    public String toString() {
        return "MutuallyExclusiveOption {"
                + "values=" + this.values
                + ", required=" + this.required
                + ", value='" + this.value + '\''
                + '}';
    }

    /**
     * A mutually exclusive option parser.
     */
    public static class Format
            implements OptionFormat {

        public void parse(Option option, Arguments arguments)
                throws InvalidArgumentsException {
            MutuallyExclusiveOption targetOption = (MutuallyExclusiveOption) option;

            for (Iterator<Argument> i = arguments.iterator(); i.hasNext();) {
                Argument argument = i.next();

                if (!argument.parsed() && targetOption.accepts(argument.value())) {
                    assertOptionNotRepeated(targetOption, i);
                    targetOption.setValue(argument.value());
                    argument.setParsed();
                    return;
                }
            }

            assertRequiredOptionFound(targetOption);
        }

        private void assertOptionNotRepeated(MutuallyExclusiveOption option, Iterator<Argument> argumentIterator)
                throws InvalidArgumentsException {
            while (argumentIterator.hasNext()) {
                Argument argument = argumentIterator.next();

                if (!argument.parsed() && option.accepts(argument.value())) {
                    throw new InvalidArgumentsException(String.format("Ambiguous arguments found for mutually exclusive option [%s].", option.values()));
                }
            }
        }

        private void assertRequiredOptionFound(MutuallyExclusiveOption option)
                throws InvalidArgumentsException {
            if (option.required()) {
                throw new InvalidArgumentsException(String.format("Required option [%s] not found.", option.values()));
            }
        }
    }
}
