package com.jscape.util.cli.ext;

import java.util.Iterator;

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

/**
 * Represents a generic command line pair option <name, value>: "-f myfile".
 *
 * @author Alex Usun
 */
public class PairOption<T>
        implements Option {

    private final String name;
    private final Type type;
    private final boolean required;
    private T value;

    /**
     * Creates a string option.
     *
     * @param name     an option name
     * @param required an option requirements flag
     *
     * @return an option instance
     */
    public static PairOption<String> stringOption(String name, boolean required) {
        return new PairOption<>(name, Type.STRING, required);
    }

    /**
     * Creates an integer option.
     *
     * @param name     an option name
     * @param required an option requirement flag
     *
     * @return an option instance
     */
    public static PairOption<Integer> integerOption(String name, boolean required) {
        return new PairOption<>(name, Type.INTEGER, required);
    }

    /**
     * Creates a long option.
     *
     * @param name     an option name
     * @param required an option requirement flag
     *
     * @return an option instance
     */
    public static PairOption<Long> longOption(String name, boolean required) {
        return new PairOption<>(name, Type.LONG, required);
    }

    /**
     * Creates a boolean option (takes "true|false" as a value).
     *
     * @param name     an option name
     * @param required an option requirement flag
     *
     * @return an option instance
     */
    public static PairOption<Boolean> booleanOption(String name, boolean required) {
        return new PairOption<>(name, Type.BOOLEAN, required);
    }

    public PairOption(String name, Type type, boolean required, T value) {
        assertNotNull(name);
        this.name = name;

        assertNotNull(type);
        this.type = type;

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

    /**
     * Creates a new option instance.
     *
     * @param name     an option name
     * @param type     an option type
     * @param required an option requirement flag
     */
    public PairOption(String name, Type type, boolean required) {
        this(name, type, required, null);
    }

    public PairOption(PairOption<T> that) {
        this(that.name, that.type, that.required, that.value);
    }

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

    /**
     * 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 T 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 T value(T defaultValue) {
        return present() ? value() : defaultValue;
    }

    /**
     * Sets an option value.
     *
     * @param value an option value
     */
    @SuppressWarnings("unchecked")
    public void setValue(String value) {
        this.value = this.type.cast(value);
    }

    public PairOption<T> copy() {
        return new PairOption<>(this);
    }

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

    /**
     * An option type. Used for option value conversion.
     */
    @SuppressWarnings("unchecked")
    public enum Type {

        STRING() {
            public <T> T cast(String value) {
                return (T) value;
            }
        },
        INTEGER() {
            public <T> T cast(String value) {
                return (T) Integer.valueOf(value);
            }
        },
        LONG() {
            public <T> T cast(String value) {
                return (T) Long.valueOf(value);
            }
        },
        BOOLEAN() {
            public <T> T cast(String value) {
                return (T) Boolean.valueOf(value);
            }
        };

        public abstract <T> T cast(String value);
    }

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

        public void parse(Option option, Arguments arguments)
                throws InvalidArgumentsException {
            PairOption targetOption = (PairOption) option;
            String optionName = targetOption.name();

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

                if (!argument.parsed() && optionName.equals(argument.value()) && i.hasNext()) {
                    Argument valueArgument = i.next();
                    assertNotParsed(targetOption, valueArgument);
                    setOptionValue(targetOption, valueArgument.value());
                    argument.setParsed();
                    valueArgument.setParsed();

                    return;
                }
            }

            assertRequiredOptionFound(targetOption);
        }

        private void assertNotParsed(PairOption option, Argument argument)
                throws InvalidArgumentsException {
            if (argument.parsed()) {
                throw new InvalidArgumentsException(String.format("Option [%s] does not have required argument.", option.name()));
            }
        }

        private void setOptionValue(PairOption option, String value)
                throws InvalidArgumentsException {
            try {
                option.setValue(value);
            } catch (Exception e) {
                throw new InvalidArgumentsException(String.format("Invalid option [%s] argument type.", option.name()));
            }
        }

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