/*
 This file is part of GNUnet.
 (C) 2011, 2012 Christian Grothoff (and other contributing authors)

 GNUnet is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published
 by the Free Software Foundation; either version 3, or (at your
 option) any later version.

 GNUnet is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with GNUnet; see the file COPYING.  If not, write to the
 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 Boston, MA 02111-1307, USA.
 */

package org.gnunet.util.getopt;

import org.gnunet.construct.ReflectUtil;

import java.lang.reflect.Field;
import java.util.*;

/**
 * Parser for command line options, in the format indicated by the
 * annotated members of the target object's class.
 */
public class Parser {

    /**
     * An ArgumentError is thrown if the command line parameters do not match their
     * specification in the target object's class.
     */
    public static class ArgumentError extends RuntimeException {
        public ArgumentError(String s) {
            super(s);
        }
    }

    /**
     * An option together with its target field.
     */
    static class OptionField {
        Argument opt;
        Field f;

        public OptionField(Argument opt, Field f) {
            this.opt = opt;
            this.f = f;
        }
    }

    // todo: unify with Construct.getMessageFields
    private List<Field> getFields(Class c) {
        LinkedList<Field> fields = new LinkedList<Field>(Arrays.asList(c.getDeclaredFields()));
        while ((c = c.getSuperclass()) != null) {
            fields.addAll(0, Arrays.asList(c.getDeclaredFields()));
        }
        return fields;
    }

    private Map<String, OptionField> longOpt = new HashMap<String, OptionField>();
    private Map<String, OptionField> shortOpt = new HashMap<String, OptionField>();

    private Collection<Argument> arguments = new LinkedList<Argument>();

    private Object targetObject;


    public Parser(Object targetObject) {
        this.targetObject = targetObject;
        // gather option annotations
        for (Field f : getFields(targetObject.getClass())) {
            Argument opt = f.getAnnotation(Argument.class);
            if (opt != null) {
                if (opt.shortname().length() != 1) {
                    throw new AssertionError("short name must be of length 1");
                }

                f.setAccessible(true);

                if (shortOpt.containsKey(opt.shortname())) {
                    throw new AssertionError(
                            String.format("short option '%s' specified multiple times (fields %s and %s)",
                                    opt.shortname(), f, shortOpt.get(opt.shortname()).f));
                }


                if (longOpt.containsKey(opt.longname())) {
                    throw new AssertionError(
                            String.format("long option '%s' specified multiple times (fields %s and %s)",
                                    opt.longname(), f, longOpt.get(opt.longname()).f));
                }

                longOpt.put(opt.longname(), new OptionField(opt, f));
                shortOpt.put(opt.shortname(), new OptionField(opt, f));
                arguments.add(opt);
            }
        }
    }

    public String getHelp() {
        StringBuilder helpString = new StringBuilder();
        for (Argument opt : arguments) {
            StringBuilder line = new StringBuilder();
            line.append("  -");
            line.append(opt.shortname());
            line.append(" --");
            line.append(opt.longname());
            if (!opt.argumentName().isEmpty()) {
                line.append("=");
                line.append(opt.argumentName());
            }
            while (line.length() < 30) {
                line.append(" ");
            }
            helpString.append(line);
            helpString.append(" ");
            helpString.append(opt.description());
            helpString.append("\n");

        }
        return helpString.toString();
    }

    private void doLongOpt(final LinkedList<String> argsList, Field targetField, Argument argument, String right) {
        try {
            Class targetFieldType = targetField.getType();
            switch (argument.action()) {
                case SET:
                    if (!targetFieldType.equals(Boolean.TYPE)) {
                        throw new AssertionError("action SET only valid on boolean member");
                    }
                    targetField.set(targetObject, true);
                    break;
                case RESET:
                    if (!targetFieldType.equals(Boolean.TYPE)) {
                        throw new AssertionError("action RESET only valid on boolean member");
                    }
                    targetField.set(targetObject, false);
                    break;
                case STORE_STRING:
                    if (!targetFieldType.equals(String.class)) {
                        throw new AssertionError("action STORE_STRING only valid on boolean member");
                    }
                    if (right == null) {
                        argsList.removeFirst();
                        if (argsList.isEmpty()) {
                            throw new ArgumentError("missing string argument to option " + argument.longname());
                        }
                        targetField.set(targetObject, argsList.getFirst());
                    } else {
                        targetField.set(targetObject, right);
                    }
                    break;
                case STORE_NUMBER:
                    ReflectUtil.NumField nf = new ReflectUtil.NumField(targetField);
                    String numString;
                    if (right == null) {
                        argsList.removeFirst();
                        if (argsList.isEmpty()) {
                            throw new ArgumentError("missing number argument to option " + argument.longname());
                        }
                        numString = argsList.getFirst();
                    } else {
                        numString = right;
                    }
                    try {
                        nf.set(targetObject, Long.parseLong(numString));
                    } catch (NumberFormatException e) {
                        throw new ArgumentError("error in number format to option " + argument.longname());
                    }
                    break;
            }
        } catch (IllegalAccessException e) {
            throw new AssertionError(
                    String.format("cannot acces member %s with @Option annotation", targetField.getName()));
        }
    }

    /**
     * returns true if we processed a shortopt with a parameter, and thus have to discard the rest
     * of the current argument string (that is, stop scanning for more shortopts)
     */
    private boolean doShortOpt(final LinkedList<String> argsList, Field targetField, Argument argument, String shortName) {
        try {
            switch (argument.action()) {
                case SET:
                    if (!targetField.getType().equals(Boolean.TYPE)) {
                        throw new AssertionError("action SET only valid on boolean member");
                    }
                    targetField.set(targetObject, true);
                    break;
                case RESET:
                    if (!targetField.getType().equals(Boolean.TYPE)) {
                        throw new AssertionError("action RESET only valid on boolean field");
                    }
                    targetField.set(targetObject, false);
                    break;
                case STORE_STRING:
                    if (!targetField.getType().equals(String.class)) {
                        throw new AssertionError("action STORE_STRING only valid on 'String' field");
                    }
                    if (argsList.getFirst().length() == 2) { // -P xxx (with space)
                        argsList.removeFirst();
                        if (argsList.isEmpty()) {
                            throw new ArgumentError(String.format("no argument for short option '%s'",
                                    shortName));
                        }
                        targetField.set(targetObject, argsList.getFirst());
                    } else {
                        targetField.set(targetObject, argsList.getFirst().substring(2)); // -Pxxx...
                    }
                    return true;
                case STORE_NUMBER:
                    ReflectUtil.NumField nf = new ReflectUtil.NumField(targetField);
                    String numString;
                    if (argsList.getFirst().length() == 2) { // -X
                        argsList.removeFirst();
                        if (argsList.isEmpty()) {
                            throw new ArgumentError("missing number argument to option " + argument.longname());
                        }
                        numString = argsList.getFirst();
                    } else {
                        numString = argsList.getFirst().substring(2);
                    }
                    try {
                        nf.set(targetObject, Long.parseLong(numString));
                    } catch (NumberFormatException e) {
                        throw new ArgumentError("error in number format to option " + argument.longname());
                    }
                    return true;
            }
        } catch (IllegalAccessException e) {
            throw new ArgumentError(
                    String.format("cannot acces member %s with @Option annotation", targetField.getName()));
        }
        return false; // did not consume entire shortopt -Xxxxxx
    }

    /**
     * Parses the given arguments, and sets the target object's fields
     * according to its annotations.
     *
     * @param args array with the program arguments
     * @return positional arguments
     */
    public String[] parse(String[] args) {
        // unprocessed positional args
        Deque<String> positionalArgs = new LinkedList<String>();

        LinkedList<String> argsList = new LinkedList<String>(Arrays.asList(args));

        while (!argsList.isEmpty()) {
            // arguments after single "--" are all positional
            if (argsList.getFirst().equals("--")) {
                argsList.removeFirst();
                positionalArgs.addAll(argsList);
                break;
            }
            // long args
            if (argsList.getFirst().startsWith("--")) {
                // remove leading slashes
                String longOptionString = argsList.getFirst().substring(2);
                // maybe it is in the format --opt=val
                String[] components = longOptionString.split("=", 2);
                OptionField of = longOpt.get(components[0]);
                if (of == null) {
                    throw new ArgumentError(String.format("unknown long option: '%s'", components[0]));
                }
                String right = (components.length == 2) ? components[1] : null;
                doLongOpt(argsList, of.f, of.opt, right);
            } else if ((argsList.getFirst().length() > 1) && argsList.getFirst().startsWith("-")) {
                // handle each flag after the "-"
                for (int i = 1; i < argsList.getFirst().length(); ++i) {
                    String optShortName = argsList.getFirst().substring(i, i + 1);
                    OptionField of = shortOpt.get(optShortName);
                    if (of == null) {
                        throw new ArgumentError(
                                String.format("unknown short option: -%s", argsList.getFirst().charAt(i)));
                    }

                    boolean discard = doShortOpt(argsList, of.f, of.opt, optShortName);

                    if (discard && (i != 1)) {
                        throw new ArgumentError("short options with argument must be seperate");
                    }

                    if (discard) {
                        break;
                    }

                }
            } else {
                positionalArgs.add(argsList.getFirst());
            }

            argsList.removeFirst();
        }

        return positionalArgs.toArray(new String[positionalArgs.size()]);
    }

}
