/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.openejb.client;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Properties;

/**
 * @version $Rev$ $Date$
 */
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public class CommandParser {

    private final Map<String, Option> opts = new LinkedHashMap<String, Option>();

    public CommandParser() {
        init();
    }

    protected void init() {
    }

    protected List<String> validate(final Arguments arguments) {
        return new ArrayList<String>();
    }

    protected List<String> usage() {
        return new ArrayList<String>();
    }

    public void category(final String name) {
        opts.put("-@" + name, new Category(name));
    }

    public Option opt(final String _long) {
        return opt(new Option(_long));
    }

    public Option opt(final char _short, final String _long) {
        return opt(new Option(_short, _long));
    }

    private Option opt(final Option o) {
        opts.put(o.getName(), o);
        if (o.getMini() != null) {
            opts.put(o.getMini(), o);
        }
        return o;
    }

    private void help() {

        final Iterator<String> usage = usage().iterator();

        if (usage.hasNext()) {
            System.out.println("Usage: " + usage.next());
        }

        while (usage.hasNext()) {
            System.out.println("  or   " + usage.next());
        }

        final List<Option> seen = new ArrayList<Option>();

        for (final Option option : opts.values()) {
            if (seen.contains(option)) {
                continue;
            }
            seen.add(option);

            if (option instanceof Category) {
                System.out.println("");
                System.out.println(option.getName() + ":");
                continue;
            }

            final StringBuilder s = new StringBuilder();
            if (option.getMini() != null) {
                s.append(" -");
                s.append(option.getMini());
                s.append(",  ");
            } else {
                s.append("      ");
            }

            s.append("--");
            s.append(option.getName());
            if (option.getType() != null) {
                s.append("=");
                s.append(option.getType().getSimpleName());
            }

            if (option.getDescription() != null || option.getValue() != null) {
                for (int i = s.length(); i < 29; i++) {
                    s.append(' ');
                }

                s.append(' ');
                s.append(' ');

                if (option.getDescription() != null) {
                    s.append(option.getDescription());
                }

                if (option.getValue() != null) {
                    if (option.getDescription() != null) {
                        s.append(" ");
                    }
                    s.append("Default is ");
                    s.append(option.getValue());
                }
            }

            System.out.println(s.toString());
        }
    }

    public Arguments parse(final String[] args) throws HelpException, InvalidOptionsException {
        final List<String> list = new ArrayList<String>(Arrays.asList(args));

        final ListIterator<String> items = list.listIterator();

        final Properties props = new Properties();

        while (items.hasNext()) {
            String arg = items.next();

            if (!arg.startsWith("-")) {
                break;
            }

            items.remove();

            final boolean longOption = arg.startsWith("--");

            arg = arg.replaceFirst("-+", "");

            if (longOption) {
                final String name;
                final String value;

                if (arg.equals("help")) {
                    props.put("help", "");
                    continue;
                } else if (arg.contains("=")) {
                    name = arg.substring(0, arg.indexOf("="));
                    value = arg.substring(arg.indexOf("=") + 1);
                } else {
                    name = arg;

                    final Option option = opts.get(name);

                    value = value(items, option);
                }

                final Option option = opts.get(name);

                checkOption("--" + name, option);
                checkValue("--" + name, value, option);
                props.put(option.getName(), value);
            } else {
                final char[] chars = new char[arg.length()];
                arg.getChars(0, chars.length, chars, 0);

                for (int i = 0; i < chars.length; i++) {
                    final char c = chars[i];

                    final String name = c + "";

                    final Option option = opts.get(name);

                    checkOption("-" + name, option);

                    String value = "";

                    if (i + 1 >= chars.length) {
                        // Last char so, next item could be a value
                        value = value(items, option);
                    }

                    checkValue("-" + c, value, option);
                    props.put(option.getName(), value);
                }
            }
        }

        final Arguments arguments = new Arguments(new Options(props), list);

        if (arguments.options().has("help")) {

            help();

            throw new HelpException();
        }

        final List<String> issues = validate(arguments);

        if (issues.size() > 0) {

            for (final String issue : issues) {
                System.err.println(issue);
            }

            System.err.println();

            help();

            throw new InvalidOptionsException();
        }

        return arguments;
    }

    private void checkValue(final String optString, final String value, final Option option) {
        checkOption(optString, option);

        if (option.getType() == null) {
            return;
        }

        if (value == null || value.equals("")) {
            System.err.println("Missing param " + optString + " <" + option.getType().getSimpleName() + ">");
            System.exit(1);
        }
    }

    private void checkOption(final String invalid, final Option option) {
        if (option != null) {
            return;
        }

        System.err.println("Unknown option: " + invalid);

        help();

        System.exit(1);
    }

    private String value(final ListIterator<String> items, final Option option) {
        if (option.getType() != null && items.hasNext()) {

            final String possibleValue = items.next();

            if (!possibleValue.startsWith("-")) {
                items.remove();
                // next item is the value
                return possibleValue;
            } else {
                // next item is an option
                items.previous();
            }
        }
        return "";
    }

    public static final class Arguments {

        private final Options options;
        private final List<String> args;

        public Arguments(final Options options, final List<String> args) {
            this.options = options;
            this.args = args;
        }

        public Options options() {
            return options;
        }

        public List<String> get() {
            return args;
        }
    }

    public static class Category extends Option {

        public Category(final String name) {
            super(name);
        }
    }

    public static class Option {

        private final String mini;
        private final String name;
        private String description;
        private Class type;
        private Object value;

        Option(final char mini, final String name) {
            this(mini + "", name);
        }

        Option(final String name) {
            this(null, name);
        }

        Option(final String mini, final String name) {
            this.mini = mini;
            this.name = name;
        }

        public Option description(String description) {
            if (!description.endsWith(".")) {
                description += ".";
            }
            this.setDescription(description);
            return this;
        }

        public Option type(final Class type) {
            this.setType(type);
            return this;
        }

        public Option value(final Object value) {
            this.setValue(value);
            if (getType() == null) {
                this.setType(value.getClass());
            }
            return this;
        }

        public String getMini() {
            return mini;
        }

        public String getName() {
            return name;
        }

        public String getDescription() {
            return description;
        }

        public void setDescription(final String description) {
            this.description = description;
        }

        public Class getType() {
            return type;
        }

        public void setType(final Class type) {
            this.type = type;
        }

        public Object getValue() {
            return value;
        }

        public void setValue(final Object value) {
            this.value = value;
        }
    }

    public static class HelpException extends Exception {

    }

    public static class InvalidOptionsException extends Exception {

    }
}
