package com.jscape.util.cli.ext;

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

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

/**
 * Provides services for parsing command line arguments and fill specified options.
 *
 * @author Alex Usun
 */
public class OptionFactory {

    private static final Entry[] DEFAULT_ENTRIES = {
            new Entry(SingleOption.class, new SingleOption.Format()),
            new Entry(MutuallyExclusiveOption.class, new MutuallyExclusiveOption.Format()),
            new Entry(PairOption.class, new PairOption.Format()),
            new Entry(ArbitraryPairOption.class, new ArbitraryPairOption.Format()),
    };

    private final Map<Class<?>, Entry> entries;

    /**
     * Creates a service instance supporting default option classes.
     *
     * @return a service instance
     */
    public static OptionFactory defaultFactory() {
        return new OptionFactory(DEFAULT_ENTRIES);
    }

    /**
     * Creates a new service instance.
     *
     * @param entries service entries
     */
    public OptionFactory(Entry... entries) {
        this.entries = new HashMap<>();
        initEntries(entries);
    }

    /**
     * Checks if specified option is supported by the service.
     *
     * @param option an option to check
     *
     * @return <code>true</code> if option is supported; <code>false</code> otherwise
     */
    public boolean supports(Option option) {
        return supports(option.getClass());
    }

    /**
     * Checks if specified option class is supported by the service.
     *
     * @param optionClass an option class to check
     *
     * @return <code>true</code> if option class is supported; <code>false</code> otherwise
     */
    public boolean supports(Class<?> optionClass) {
        return this.entries.containsKey(optionClass);
    }

    /**
     * Parses command line arguments and fills specified options.
     *
     * @param values command line arguments
     * @param options   options to fill
     *
     * @throws InvalidArgumentsException if a parsing error occurs
     */
    public void update(String[] values, Option... options)
            throws InvalidArgumentsException {
        Arguments arguments = argumentsFor(values);
        update(options, arguments);
    }

    @Override
    public String toString() {
        return "OptionFactory {"
                + "entries=" + this.entries
                + '}';
    }

    private void initEntries(Entry[] entries) {
        for (Entry entry : entries) {
            this.entries.put(entry.optionClass, entry);
        }
    }

    private Arguments argumentsFor(String[] values) {
        List<Argument> arguments = new ArrayList<>(values.length);

        for (String argument : values) {
            arguments.add(new Argument(argument));
        }

        return new Arguments(arguments);
    }

    private void update(Option[] options, Arguments arguments)
            throws InvalidArgumentsException {
        for (Option option : options) {
            update(option, arguments);
        }
    }

    private void update(Option option, Arguments arguments)
            throws InvalidArgumentsException {
        Entry entry = entryFor(option);
        entry.format.parse(option, arguments);
    }

    private Entry entryFor(Option option) {
        Entry entry = this.entries.get(option.getClass());

        if (entry == null) {
            throw new IllegalArgumentException("Unsupported option: " + option);
        }

        return entry;
    }

    /**
     * Contains dependency between option class and corresponded parser.
     */
    public static class Entry {

        public final Class<?> optionClass;
        public final OptionFormat format;

        /**
         * Creates a new entry instance.
         *
         * @param optionClass an option class
         * @param format      an option parser
         */
        public Entry(Class<?> optionClass, OptionFormat format) {
            assertNotNull(optionClass);
            this.optionClass = optionClass;

            assertNotNull(format);
            this.format = format;
        }

        @Override
        public String toString() {
            return "Entry {"
                    + "optionClass=" + this.optionClass
                    + ", parser=" + this.format
                    + '}';
        }
    }

}
