/*
 * Copyright (C) 2013-2015 René Jeschke <rene_jeschke@yahoo.de>
 *
 * Licensed 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 com.javachina.ext.markdown.cmd;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

/**
 * Generic command line parser.
 *
 * This is a copy from {@link https://github.com/rjeschke/neetutils-base}.
 *
 * @author René Jeschke (rene_jeschke@yahoo.de)
 */
final class CmdLineParser
{
    private CmdLineParser()
    {
        // meh!
    }

    enum Type
    {
        UNSUPPORTED, STRING, BYTE, SHORT, INT, LONG, FLOAT, DOUBLE, LIST, BOOL;
    }

    final static HashMap<Class<?>, Type> TYPE_MAP        = new HashMap<Class<?>, Type>();
    final static Class<?>[]              TYPE_CLASS_LIST = Colls.<Class<?>> objArray(String.class, byte.class,
                                                                 Byte.class, short.class, Short.class, int.class,
                                                                 Integer.class, long.class, Long.class, float.class,
                                                                 Float.class, double.class, Double.class, List.class,
                                                                 Boolean.class, boolean.class);
    final static Type[]                  TYPE_TYPE_LIST  = Colls.objArray(Type.STRING, Type.BYTE, Type.BYTE,
                                                                 Type.SHORT,
                                                                 Type.SHORT, Type.INT, Type.INT, Type.LONG, Type.LONG,
                                                                 Type.FLOAT, Type.FLOAT, Type.DOUBLE, Type.DOUBLE,
                                                                 Type.LIST, Type.BOOL, Type.BOOL);

    final static HashSet<String>         BOOL_TRUE       = new HashSet<String>(Colls.list("on", "true", "yes"));
    final static HashSet<String>         BOOL_FALSE      = new HashSet<String>(Colls.list("off", "false", "no"));

    static
    {
        for (int i = 0; i < TYPE_CLASS_LIST.length; i++)
        {
            TYPE_MAP.put(TYPE_CLASS_LIST[i], TYPE_TYPE_LIST[i]);
        }
    }

    static Type getTypeFor(final Class<?> clazz)
    {
        final Type type = TYPE_MAP.get(clazz);

        if (type != null)
        {
            return type;
        }

        if (Classes.implementsInterface(clazz, List.class))
        {
            return Type.LIST;
        }

        return Type.UNSUPPORTED;
    }

    static String defaultToString(final Object value, final Type type, final Arg arg)
    {
        if (value == null || arg.isSwitch || arg.catchAll || !arg.printDefault)
        {
            return null;
        }

        if (type == Type.LIST)
        {
            final List<?> list = (List<?>)value;

            if (list.isEmpty())
            {
                return null;
            }

            final StringBuilder sb = new StringBuilder();
            final Once<String> once = Once.of("", Character.toString(arg.itemSep));
            for (final Object o : list)
            {
                sb.append(once.get());
                sb.append(o.toString());
            }
            return sb.toString();
        }

        return value.toString();
    }

    private static void parseArgs(final Object[] objs, final List<Arg> allArgs, final HashMap<String, Arg> shortArgs,
            final HashMap<String, Arg> longArgs)
            throws IOException
    {
        for (final Object obj : objs)
        {
            final Class<?> cl = obj.getClass();
            final Field[] fields = cl.getDeclaredFields();

            for (final Field f : fields)
            {
                if (f.isAnnotationPresent(CmdArgument.class))
                {
                    final Arg arg = new Arg(f.getAnnotation(CmdArgument.class), obj, f);

                    if (arg.type == Type.UNSUPPORTED)
                    {
                        throw new IOException("Unsupported parameter type: " + f.getType().getCanonicalName()
                                + " for: " + arg);
                    }

                    if (arg.listType == Type.UNSUPPORTED || arg.listType == Type.LIST)
                    {
                        throw new IOException("Unsupported list type: " + f.getType().getCanonicalName() + " for: "
                                + arg);
                    }

                    if (Strings.isEmpty(arg.s) && Strings.isEmpty(arg.l))
                    {
                        throw new IOException("Missing parameter name");
                    }

                    if (!Strings.isEmpty(arg.s))
                    {
                        if (shortArgs.containsKey(arg.s))
                        {
                            throw new IOException("Duplicate short argument: -" + arg.s);
                        }
                        shortArgs.put(arg.s, arg);
                    }

                    if (!Strings.isEmpty(arg.l))
                    {
                        if (longArgs.containsKey(arg.l))
                        {
                            throw new IOException("Duplicate long argument: --" + arg.l);
                        }
                        longArgs.put(arg.l, arg);
                    }

                    if (arg.isCatchAll() && arg.type != Type.LIST)
                    {
                        throw new IOException("Parameter '" + arg + "' requires a List field.");
                    }

                    if (arg.isSwitch && arg.type != Type.BOOL)
                    {
                        throw new IOException("Parameter '" + arg + "' requires a Boolean/boolean field.");
                    }

                    allArgs.add(arg);
                }
            }
        }
    }

    /**
     * Generates a formatted help (Unix-style) for the given argument objects.
     *
     * @param columnWidth
     *            Maximum column width. Words get wrapped at spaces.
     * @param sort
     *            Set {@code true} to sort arguments before printing.
     * @param objs
     *            One or more objects with annotated public fields.
     * @return The formatted argument help text.
     * @throws IOException
     *             if a parsing error occurred.
     * @see CmdArgument
     */
    public static String generateHelp(final int columnWidth, final boolean sort, final Object... objs)
            throws IOException
    {
        final List<Arg> allArgs = Colls.list();
        final HashMap<String, Arg> shortArgs = new HashMap<String, Arg>();
        final HashMap<String, Arg> longArgs = new HashMap<String, Arg>();

        parseArgs(objs, allArgs, shortArgs, longArgs);

        int minArgLen = 0;

        for (final Arg a : allArgs)
        {
            int len = a.toString().length();
            if (!a.isSwitch)
            {
                ++len;
                len += a.getResolvedType().toString().length();
                if (a.isCatchAll())
                {
                    ++len;
                }
                else if (a.isList())
                {
                    len += 6;
                }
            }
            minArgLen = Math.max(minArgLen, len);
        }
        minArgLen += 2;
        if (sort)
        {
            Collections.sort(allArgs);
        }

        final StringBuilder sb = new StringBuilder();

        for (final Arg a : allArgs)
        {
            final StringBuilder line = new StringBuilder();
            line.append(' ');
            line.append(a);
            if (!a.isSwitch)
            {
                line.append(' ');
                line.append(a.getResolvedType().toString().toLowerCase());
                if (a.isCatchAll())
                {
                    line.append('s');
                }
                else if (a.isList())
                {
                    line.append('[');
                    line.append(a.itemSep);
                    line.append("...]");
                }
            }
            while (line.length() < minArgLen)
            {
                line.append(' ');
            }

            line.append(':');

            final StringBuilder desc = new StringBuilder(a.desc.trim());

            final String defVal = defaultToString(a.safeFieldGet(), a.type, a);

            if (defVal != null)
            {
                desc.append(" Default is: '");
                desc.append(defVal);
                desc.append("'.");
            }

            final List<String> toks = Strings.split(desc.toString(), ' ');

            for (final String s : toks)
            {
                if (line.length() + s.length() + 1 > columnWidth)
                {
                    sb.append(line);
                    sb.append('\n');
                    line.setLength(0);
                    while (line.length() <= minArgLen)
                    {
                        line.append(' ');
                    }
                    line.append(' ');
                }
                line.append(' ');
                line.append(s);
            }

            if (line.length() > minArgLen)
            {
                sb.append(line);
                sb.append('\n');
            }
        }

        return sb.toString();
    }

    /**
     * Parses command line arguments.
     *
     * @param args
     *            Array of arguments, like the ones provided by
     *            {@code void main(String[] args)}
     * @param objs
     *            One or more objects with annotated public fields.
     * @return A {@code List} containing all unparsed arguments (i.e. arguments
     *         that are no switches)
     * @throws IOException
     *             if a parsing error occurred.
     * @see CmdArgument
     */
    public static List<String> parse(final String[] args, final Object... objs) throws IOException
    {
        final List<String> ret = Colls.list();

        final List<Arg> allArgs = Colls.list();
        final HashMap<String, Arg> shortArgs = new HashMap<String, Arg>();
        final HashMap<String, Arg> longArgs = new HashMap<String, Arg>();

        parseArgs(objs, allArgs, shortArgs, longArgs);

        for (int i = 0; i < args.length; i++)
        {
            final String s = args[i];

            final Arg a;

            if (s.startsWith("--"))
            {
                a = longArgs.get(s.substring(2));
                if (a == null)
                {
                    throw new IOException("Unknown switch: " + s);
                }
            }
            else if (s.startsWith("-"))
            {
                a = shortArgs.get(s.substring(1));
                if (a == null)
                {
                    throw new IOException("Unknown switch: " + s);
                }
            }
            else
            {
                a = null;
                ret.add(s);
            }

            if (a != null)
            {
                if (a.isSwitch)
                {
                    a.setField("true");
                }
                else
                {
                    if (i + 1 >= args.length)
                    {
                        System.out.println("Missing parameter for: " + s);
                    }
                    if (a.isCatchAll())
                    {
                        final List<String> ca = Colls.list();
                        for (++i; i < args.length; ++i)
                        {
                            ca.add(args[i]);
                        }
                        a.setCatchAll(ca);
                    }
                    else
                    {
                        ++i;
                        a.setField(args[i]);
                    }
                }
                a.setPresent();
            }
        }

        for (final Arg a : allArgs)
        {
            if (!a.isOk())
            {
                throw new IOException("Missing mandatory argument: " + a);
            }
        }

        return ret;
    }

    private static class Arg implements Comparable<Arg>
    {
        final String  s;
        final String  l;
        final String  id;
        final String  desc;
        final char    itemSep;
        final boolean isSwitch;
        final boolean required;
        final boolean catchAll;
        final boolean printDefault;
        final Type    type;
        final Type    listType;
        boolean       present = false;
        final Object  object;
        final Field   field;

        public Arg(final CmdArgument arg, final Object obj, final Field field)
        {
            this.s = arg.s() == 0 ? "" : Character.toString(arg.s());
            this.l = arg.l();
            this.desc = arg.desc();
            this.isSwitch = arg.isSwitch();
            this.required = arg.required();
            this.catchAll = arg.catchAll();
            this.itemSep = arg.listSep();
            this.printDefault = arg.printDefault();
            this.id = this.s + "/" + this.l;

            this.object = obj;
            this.field = field;
            this.type = getTypeFor(this.field.getType());
            this.listType = getTypeFor(arg.listType());
        }

        public Type getResolvedType()
        {
            return this.isList() ? this.listType : this.type;
        }

        public boolean isCatchAll()
        {
            return this.catchAll;
        }

        public boolean isList()
        {
            return this.type == Type.LIST;
        }

        public void setCatchAll(final List<String> list) throws IOException
        {
            this.setListField(list);
        }

        public void setListField(final List<String> list) throws IOException
        {
            try
            {
                if (this.listType == Type.STRING)
                {
                    this.field.set(this.object, list);
                }
                else
                {
                    final List<Object> temp = Colls.list();
                    for (final String i : list)
                    {
                        temp.add(this.toObject(i, this.listType));
                    }
                    this.field.set(this.object, temp);
                }
            }
            catch (final IllegalArgumentException e)
            {
                throw new IOException("Failed to write value", e);
            }
            catch (final IllegalAccessException e)
            {
                throw new IOException("Failed to write value", e);
            }
        }

        Object safeFieldGet()
        {
            try
            {
                return this.field.get(this.object);
            }
            catch (final Exception e)
            {
                return null;
            }
        }

        private Object toObject(final String value, final Type type) throws IOException
        {
            try
            {
                switch (type)
                {
                case STRING:
                    return value;
                case BYTE:
                    return Byte.parseByte(value);
                case SHORT:
                    return Short.parseShort(value);
                case INT:
                    return Integer.parseInt(value);
                case LONG:
                    return Long.parseLong(value);
                case FLOAT:
                    return Float.parseFloat(value);
                case DOUBLE:
                    return Double.parseDouble(value);
                case BOOL:
                    if (BOOL_TRUE.contains(value.toLowerCase()))
                    {
                        return true;
                    }
                    if (BOOL_FALSE.contains(value.toLowerCase()))
                    {
                        return false;
                    }
                    throw new IOException("Illegal bool value for:" + this.toString());
                default:
                    throw new IOException("Illegal type: " + type.toString().toLowerCase());
                }
            }
            catch (final Throwable t)
            {
                throw new IOException("Parsing error for: " + this.toString() + "; '" + value + "'", t);
            }
        }

        public void setField(final String value) throws IOException
        {
            try
            {
                if (this.isList())
                {
                    this.setListField(Strings.split(value, this.itemSep));
                }
                else
                {
                    this.field.set(this.object, this.toObject(value, this.type));
                }
            }
            catch (final IllegalArgumentException e)
            {
                throw new IOException("Failed to write field: " + this.field.getName(), e);
            }
            catch (final IllegalAccessException e)
            {
                throw new IOException("Failed to write field: " + this.field.getName(), e);
            }
        }

        public void setPresent()
        {
            this.present = true;
        }

        public boolean isOk()
        {
            return !this.required || this.present;
        }

        @Override
        public int hashCode()
        {
            return this.id.hashCode();
        }

        @Override
        public boolean equals(final Object obj)
        {
            if (obj instanceof Arg)
            {
                return this.id.equals(((Arg)obj).id);
            }
            return false;
        }

        @Override
        public String toString()
        {
            if (Strings.isEmpty(this.s))
            {
                return "    --" + this.l;
            }
            if (Strings.isEmpty(this.l))
            {
                return "-" + this.s;
            }
            return "-" + this.s + ", --" + this.l;
        }

        @Override
        public int compareTo(final Arg o)
        {
            final String a = Strings.isEmpty(this.s) ? this.l : this.s;
            final String b = Strings.isEmpty(o.s) ? o.l : o.s;
            return a.compareTo(b);
        }
    }

    private static class Once<T>
    {
        private final T first;
        private final T next;
        private boolean isFirst = true;

        public Once(final T first, final T next)
        {
            this.first = first;
            this.next = next;
        }

        public static <T> Once<T> of(final T first, final T next)
        {
            return new Once<T>(first, next);
        }

        public T get()
        {
            if (this.isFirst)
            {
                this.isFirst = false;
                return this.first;
            }

            return this.next;
        }
    }

    private final static class Colls
    {
        final static <T> T[] objArray(final T... ts)
        {
            return ts;
        }

        final static <A> List<A> list(final A... coll)
        {
            final List<A> ret = new ArrayList<A>(coll.length);
            for (int i = 0; i < coll.length; i++)
            {
                ret.add(coll[i]);
            }
            return ret;
        }
    }

    private final static class Classes
    {
        final static boolean implementsInterface(final Class<?> clazz, final Class<?> interfce)
        {
            for (final Class<?> c : clazz.getInterfaces())
            {
                if (c.equals(interfce))
                {
                    return true;
                }
            }

            return false;
        }
    }

    private final static class Strings
    {
        public final static boolean isEmpty(final String str)
        {
            return str == null || str.isEmpty();
        }

        public final static List<String> split(final String str, final char ch)
        {
            final List<String> ret = Colls.list();

            if (str != null)
            {
                int s = 0, e = 0;
                while (e < str.length())
                {
                    if (str.charAt(e) == ch)
                    {
                        ret.add(str.substring(s, e));
                        s = e + 1;
                    }
                    e++;
                }
                ret.add(str.substring(s, e));
            }

            return ret;
        }
    }
}
