//    Copyright 2011 Brad Block, Pawjaw, LLC. (brad@pawjaw.com)
//
//    This program 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 of the License, or
//    (at your option) any later version.
//
//    This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

package com.pawjaw.utils;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

public class OptArg {
    public static Map<String, OptArg> optarg_flag = new HashMap();
    public static Map<Integer, List<OptArg>> optargs_applies = new HashMap();

    public OptType opt_type;
    public String short_opt, long_opt, help;
    public Object arg = null, default_value, data = null;

    public OptArg(int applies, OptType opt_type, String short_opt, String long_opt, Object default_value, String help) {
        this(applies, opt_type, short_opt, long_opt, default_value, help, null);
    }

    public OptArg(int applies, OptType opt_type, String short_opt, String long_opt, Object default_value, String help, Object[] possibleargs) {
        int i;
        List<OptArg> optargs;
        this.opt_type = opt_type;
        this.short_opt = short_opt;
        this.long_opt = long_opt;
        this.help = help;
        this.default_value = default_value;
        if(short_opt != null)
            optarg_flag.put(short_opt, this);
        if(long_opt != null)
            optarg_flag.put(long_opt, this);
        for(i = 0;i < 32;i++)
            if((applies >> i & 1) == 1) {
                if((optargs = optargs_applies.get(1 << i)) == null)
                    optargs_applies.put(1 << i, optargs = new ArrayList());
                optargs.add(this);
            }
        if(possibleargs == null)
            this.possibleargs = null;
        else {
            this.possibleargs = new HashSet();
            for(Object o : possibleargs)
                this.possibleargs.add(o);
        }
    }

    public static List<OptArg> parseArgs(String[] args) throws OptArgException {
        OptArg optarg = null;
        List<OptArg> optargs = new ArrayList();
        for(String arg : args)
            if(optarg == null) {
                optargs.add(optarg = parseOption(arg));
                if(optarg.arg != null || optarg.opt_type == OptType.FLAG)
                    optarg = null;
            } else {
                try {
                    parseOption(arg);
                } catch(OptArgException e) {
                    try {
                        optarg.arg = optarg.opt_type.parse(arg);
                    } catch(IllegalArgumentException f) {
                        throw new OptArgException(f.getMessage(), optarg);
                    }
                    optarg.check();
                    optarg = null;
                    continue;
                }
                throw new OptArgException("expected argument",  optarg);
            }
        if(optarg != null)
            throw new OptArgException("expected argument", optarg);
        return optargs;
    }

    public String label() {
        return (short_opt != null && long_opt != null) ? (short_opt + "|" + long_opt) : short_opt != null ? short_opt : long_opt;
    }

    public Object argOrDefault() {
        return arg != null ? arg : default_value;
    }

    public String toString() {
        return label() + (arg == null ? "" : (": " + argOrDefault()));
    }

    public String longHelp() {
        return label() + ": " + help + (opt_type == OptType.FLAG ? "" : (", requires " + opt_type.description + (default_value == null ? "" : ", default value " + default_value)));
    }

    public File getFileToRead() throws OptArgException {
        File f = null;
        String s = null;
        if(arg == null)
            s = "no file specified";
        else if(opt_type != OptType.STRING)
            s = "argument is not a string: " + arg;
        else if(!(f = new File((String)arg)).exists())
            s = "file does not exist: " + arg;
        else if(!f.isFile())
            s = "not a file: " + arg;
        else if(!f.canRead())
            s = "cannot read file: " + arg;
        if(s != null)
            throw new OptArgException(s, this);
        return f;
    }

    public File getFileToWrite() throws OptArgException {
        File f = null;
        String s = null;
        if(arg == null)
            s = "no file specified";
        else if(opt_type != OptType.STRING)
            s = "argument is not a string: " + arg;
        else if((f = new File((String)arg)).exists()) {
            if(!f.isFile())
                s = "path exists and is not a file: " + arg;
            else if(!f.canWrite())
                s = "file exists and can not be overwritten: " + arg;
        }
        if(s != null)
            throw new OptArgException(s, this);
        return f;
    }

    private static final Pattern SPLITEQS = Pattern.compile("\\s*=\\s*");
    private static final Pattern MATCHOPTEQS = Pattern.compile("--[^=]+=\\s*");

    private Set<Object> possibleargs;

    public static enum OptType {
        FLAG("no argument", new OptTypeParser() {
            public Object parse(String s) {
                return null;
            }
        }),
        STRING("string argument", new OptTypeParser() {
            public Object parse(String s) {
                return s;
            }
        }),
        BOOLEAN("boolean (true/false/0/1) argument", new OptTypeParser() {
            public Object parse(String s) {
                try {
                    if(s.equals("1"))
                        return true;
                    else if(s.equals("0"))
                        return false;
                    return Boolean.valueOf(s.toLowerCase());
                } catch(NumberFormatException e) {
                    throw new IllegalArgumentException("unexpected " + s + ", expected true/false/0/1");
                }
            }
        }),
        INT("integer argument", new OptTypeParser() {
            public Object parse(String s) {
                try {
                    return Integer.valueOf(s);
                } catch(NumberFormatException e) {
                    throw new IllegalArgumentException("unexpected " + s + ", expected integer");
                }
            }
        }),
        POSITIVEINT("positive (greater than zero) integer argument", new OptTypeParser() {
            public Object parse(String s) {
                int i;
                try {
                    if((i = Integer.valueOf(s)) > 0)
                        return i;
                } catch(NumberFormatException e) {}
                throw new IllegalArgumentException("unexpected " + s + ", expected positive (greater than zero) integer");
            }
        }),
        NONNEGATIVEINT("non-negative (greater than or equal to zero) integer argument", new OptTypeParser() {
            public Object parse(String s) {
                int i;
                try {
                    if((i = Integer.valueOf(s)) >= 0)
                        return i;
                } catch(NumberFormatException e) {}
                throw new IllegalArgumentException("unexpected " + s + ", expected non-negative (greater than or equal to zero) integer");
            }
        }),
        DOUBLE("double-precision number argument", new OptTypeParser() {
            public Object parse(String s) {
                try {
                    return Double.valueOf(s);
                } catch(NumberFormatException e) {
                    throw new IllegalArgumentException("unexpected " + s + ", expected double-precision number");
                }
            }
        }),
        POSITIVEDOUBLE("positive (greater than zero) double-precision number argument", new OptTypeParser() {
            public Object parse(String s) {
                double d;
                try {
                    if((d = Double.valueOf(s)) > 0)
                        return d;
                } catch(NumberFormatException e) {}
                throw new IllegalArgumentException("unexpected " + s + ", expected positive (greater than zero) double-precision number");
            }
        }),
        LONG("long number argument", new OptTypeParser() {
            public Object parse(String s) {
                try {
                    return Long.valueOf(s);
                } catch(NumberFormatException e) {
                    throw new IllegalArgumentException("unexpected " + s + ", expected long number");
                }
            }
        });

        public String description;
        private OptTypeParser otp;

        private OptType(String description, OptTypeParser otp) {
            this.description = description;
            this.otp = otp;
        }

        public Object parse(String s) throws IllegalArgumentException {
            if(s == null || s.length() == 0)
                throw new IllegalArgumentException("expected nonempty argument");
            return otp.parse(s);
        }

        private interface OptTypeParser {
            public Object parse(String s) throws IllegalArgumentException;
        }
    };

    private void check() throws OptArgException {
        if(possibleargs != null && !possibleargs.contains(arg))
            throw new OptArgException("argument " + arg + " not permitted", this);
    }

    private static OptArg parseOption(String arg) throws OptArgException {
        OptArg optarg;
        String[] ss;
        if(arg == null || (arg = arg.trim()).length() == 0)
            throw new OptArgException("expected nonempty option flag");
        if(arg.startsWith("--") && (ss = SPLITEQS.split(arg)).length > 1) {
            if((optarg = optarg_flag.get(ss[0].toLowerCase())) != null) {
                try {
                    optarg.arg = optarg.opt_type.parse(MATCHOPTEQS.matcher(arg).replaceAll(""));
                } catch(IllegalArgumentException e) {
                    throw new OptArgException(e.getMessage(), optarg);
                }
                optarg.check();
                return optarg;
            } else
                throw new OptArgException("unknown option flag " + ss[0]);
        } else if((optarg = optarg_flag.get(arg.toLowerCase())) != null)
            return optarg;
        throw new OptArgException("unknown option flag " + arg);
    }
}
