/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
 * This source file is part of the Cangjie project, licensed under Apache-2.0
 * with Runtime Library Exception.
 *
 * See https://cangjie-lang.cn/pages/LICENSE for license information.
 */

/**
 * @file
 *
 * This is a library for ArgOpt class.
 */
package std.argopt

import std.collection.*

/**
 * This is a library for ArgOpt class.
 * This module helps scripts parse command-line arguments,
 * And parse functions (including special meanings of parameters.
 * in the form of "-" and "--") , Supported by the GNU software.
 * This class uses some methods of String and Rune classes for implementation.
 *
 * @since 0.17.4
 */
@Deprecated[message: "Use global function `public func parseArguments(args: Array<String>, specs: Array<ArgumentSpec>): ParsedArguments` instead."]
public class ArgOpt {

    /* Class that actually handles work. */
    private var utils: ParserHelper

    /* Class that stores symbols. */
    private var arguments: Arguments

    /**
     * Constructor one parameters.
     * For example : shortArgFormat ="abc:d:".
     * indicates that the first letter before the colon can be parsed. that's c and d.
     *
     * @see init(args: Array<String>, shortArgFormat: String, longArgList:Array<String>.
     * @param shortArgFormat Short parameter.
     *
     * @since 0.17.4
     */
    public init(shortArgFormat: String) {
        this(Arguments.NULL, shortArgFormat, Arguments.NULL)
    }

    /**
     * Constructor one parameters.
     * For example: longArgList = {"testA=", "testB"}, args = {"--testA=foo","--test=bar"}
     * Because of the r'=' exists after testA in longArgList. you can find the value of.
     * testA in args. Conversely, no value found.
     *
     * @see init(args: Array<String>, shortArgFormat: String, longArgList: Array<String>).
     * @param longArgList Long parameter set.
     *
     * @since 0.17.4
     */
    public init(longArgList: Array<String>) {
        this(Arguments.NULL, "", longArgList)
    }

    /**
     * Constructor two parameters.
     * For example, see the context.
     *
     * @see init(shortArgFormat: String).
     * @see init(longArgList: Array<String>).
     * @see init(args: Array<String>, shortArgFormat: String, longArgList: Array<String>).
     * @param longArgList Long parameter set.
     * @param shortArgFormat Short parameter.
     *
     * @since 0.17.4
     */
    public init(shortArgFormat: String, longArgList: Array<String>) {
        this(Arguments.NULL, shortArgFormat, longArgList)
    }

    /**
     * This constructor is parsed using the ParserHelper class.
     * which implements the overall round robin scheduling.
     * The split parameters are placed in ParserHelper.argMap.
     * The indexes that do not parse parameters are placed in ParserHelper.argsList.
     * For example No.1: shortArgFormat="abc:d:", args= {"-afoo", "-bofo", "-cbar", "-d"}.
     * Because indicates that the first letter before the colon can be parsed.
     * Keys '-a', '-b', '-c', and '-d' have been saved in the key of ParserHelper.argMap.
     * so the value of r'c' is "bar", the value of r'd' is "".
     * For example No.2: longArgList= {"testA=", "testB"}, args= {"--testA=foo", "--testB=bar"}.
     * Because of the r'=' exists after testA in longArgList.
     * you can find the value of testA in args. Conversely, no value found.
     * Keys 'testA' and 'testB' have been parsed into the key of ParserHelper.argMap.
     * However, The values of keys 'testA' and 'testB' depend on the parametric form of 'longArgList'.
     * For example No.3: longArgList= {"testA=", "testB"},args= {"--testA", "foo", "--testB", "bar"}.
     * Because of the r'=' exists after testA in longArgList.
     * But, if '--testA' in 'args' does not have a value (value after r'='), the next value of '--testA' must be obtained.
     * In addition, the prefix of this next value cannot be '--' or r'-'.
     * In the same way, Keys 'testA' and 'testB' have been parsed into the key of ParserHelper.argMap.
     *
     * @see Arguments.init().
     * @see Arguments.getDoubleSign().
     * @see Arguments.getSingleSign().
     * @see Arguments.setMyArgs(String).
     * @see ParserHelper.init(Arguments).
     * @see ParserHelper.returnTupleFun(String).
     * @see ParserHelper.isArrayEmptyFun(Array<String>).
     * @param longArgList Long parameter set.
     * @param shortArgFormat Short parameter.
     * @param args setting Default parameters of myArgs.
     *
     * @since 0.17.4
     */
    public init(args: Array<String>, shortArgFormat: String, longArgList: Array<String>) {
        this.arguments = Arguments()
        this.utils = ParserHelper(this.arguments)
        this.utils.argsParser(args, shortArgFormat, longArgList)
    }

    /**
     * Getting the value of a parameter.
     * For example one: getArg("a") or getArg("-a").
     * The prefix of the input parameter can be without r'-' or can contain r'-'.
     * which does not affect the returned value.
     * For example two: getArg("--test").
     *
     * @param arg Target key.
     * @return return the option type corresponding to the parameter.
     *
     * @since 0.17.4
     */
    public func getArg(arg: String): Option<String> {
        if (arg.isEmpty()) {
            return Option<String>.None
        }

        let argMap: HashMap<String, String> = utils.getArgsMap()
        let argStr: String = if (arg.size == 1 && arg != "-") {
            "-${arg}"
        } else if ((arg.size > 2 && !arg.startsWith("--")) || !arg.startsWith("-")) {
            "--${arg}"
        } else {
            arg
        }
        return argMap.get(argStr)
    }

    /**
     * Getting the are no parsed parameters.
     *
     * @return if there are no unresolved parameters, returns Arguments.NULL.
     *
     * @since 0.17.4
     */
    public func getUnparseArgs(): Array<String> {
        let argsList: ArrayList<Int8> = utils.getArgsList()
        if (argsList.size == 0) {
            return this.arguments.getUnparseArgs()
        }
        var countZero: Int64 = 0
        var arrIndex: Int64 = 0
        for (i in 0..argsList.size) {
            if (argsList[i] != 0) {
                continue
            }
            countZero++
        }
        this.arguments.setUnparseArgs(Array<String>(countZero, repeat: ""))
        for (i in 0..argsList.size) {
            if (argsList[i] != 0) {
                continue
            }
            this.arguments.getUnparseArgs()[arrIndex] = this.arguments.getMyArgs()[i]
            arrIndex++
        }
        return this.arguments.getUnparseArgs()
    }

    /**
     * Getting all parsed parameters and values.
     *
     * @return key-value pair. The header of the key value must contain r'-' or '--'.
     *
     * @since 0.17.4
     */
    public func getArgumentsMap(): HashMap<String, String> {
        return this.utils.getArgsMap()
    }
}

/**
 * Stores parameters used by the ArgOpt class.
 * including default parameters, and common symbols.
 * Provides the get and set methods.
 *
 * @since 0.17.4
 */
class Arguments {

    /* Provide an empty Array. */
    static let NULL: Array<String> = Array<String>()

    /* Default parameters of myArgs.The default value is NULL. */
    private var myArgs: Array<String> = NULL

    /* Unparsed parameter array, The default value is NULL. */
    private var unparseArgs: Array<String> = NULL

    func setMyArgs(Args: Array<String>) {
        this.myArgs = Args
    }

    func setUnparseArgs(unparseArgs: Array<String>) {
        this.unparseArgs = unparseArgs
    }

    func getMyArgs(): Array<String> {
        return myArgs
    }

    func getUnparseArgs(): Array<String> {
        return unparseArgs
    }
}

/**
 * Exception Type Enumeration.
 *
 * @since 0.17.4
 */
enum ErrorType {
    IllegalArgument
    | IndexOutOfBounds
    | Runtime
}

/**
 * Parameter Parsing Helper class
 *
 * @since 0.17.4
 */
class ParserHelper {

    /* Initialization map creation object: HashMap */
    private let argMap: HashMap<String, String> = HashMap<String, String>()

    /* Initialization Records Resolved and Unresolved Collections:argList */
    private var argsList: ArrayList<Int8> = ArrayList<Int8>()

    /* arguments */
    private var arguments: Arguments

    /* prefix prompt for exceptions */
    private static let exceptionPrompt = "Invalid string, please check the parameter: "

    /**
     * Constructor one parameters.
     *
     * @param argument Reference to the incoming object.
     *
     * @since 0.17.4
     */
    init(argument: Arguments) {
        this.arguments = argument
    }

    /**
     * Return Members argMap.
     *
     * @return return HashMap object.
     *
     * @since 0.17.4
     */
    func getArgsMap(): HashMap<String, String> {
        return this.argMap.clone()
    }

    /**
     * Return Members argsList.
     *
     * @return returns the list set.
     *
     * @since 0.17.4
     */
    func getArgsList(): ArrayList<Int8> {
        return this.argsList.clone()
    }

    /**
     * Processes the args parameter sent by the argopt.
     * Traverse @args to determine the @arg type based on the @arg prefix.
     * If the prefix is --, use @doLongParser  to obtain @long and @unParse.
     * If the prefix is -, use @doShortParser  to obtain @long and @unParse.
     * @unParse check whether the @arg is matched and parsed in @shortStr or @longOpts.
     * @long is number of @arg consumed by each parsing operation.
     *
     * @param args
     * @param shortArgFormat
     * @param longArgList
     *
     * @since 0.17.4
     */
    func argsParser(args: Array<String>, shortArgFormat: String, longArgList: Array<String>): Unit {
        let shortStr: String = checkShortStr(shortArgFormat)
        let longOpts: Array<String> = checkLongOpt(longArgList)
        let size: Int64 = args.size
        this.arguments.setMyArgs(args)
        this.argsList = ArrayList<Int8>(size, {_ => 0})

        var i: Int64 = 0
        while (i < size) {
            let arg: String = args[i]
            if (arg.startsWith("-") && arg != "-" && arg != "--") {
                let (long, unParse): (Int64, Bool) = if (arg.startsWith("--")) {
                    this.doLongParser(longOpts, args, i)
                } else {
                    this.doShortParser(shortStr, args, i)
                }
                if (unParse) {
                    i++
                    continue
                }
                for (j in 0..long where j + i < size) {
                    this.argsList[i + j] = 1
                }
                i = i + long
                continue
            }
            i++
        }
    }

    private func doLongParser(longOpts: Array<String>, args: Array<String>, i: Int64): (Int64, Bool) {
        var key: String = args[i]
        let split = key.indexOf("=") ?? -1
        var optArg: String = ""
        var long: Int64 = 1
        if (split > -1) {
            if (split + 1 < key.size) {
                optArg = key[split + 1..]
            }
            key = key[0..split]
        }
        var unParse: Bool = true
        var hasArg: Bool = false
        for (itemStr in longOpts) {
            let split: Int64 = itemStr.indexOf("=") ?? -1
            var item: String = itemStr
            if (split > -1) {
                item = itemStr[0..=split]
            }
            let isFinished = (item.startsWith(key) && (item == key || item == key + "=")) ||
                !item.startsWith("--") && ("--" + item).startsWith(key) && ("--" + item == key || "--" + item == key +
                "=")
            if (isFinished) {
                unParse = false
                hasArg = item.endsWith("=")
                break
            }
        }
        if (unParse || (!hasArg && split != -1)) {
            return (long, true)
        }
        if (hasArg && split == -1) {
            if (i + 1 >= args.size) {
                return (long, true)
            }
            optArg = args[i + 1]
            long++
        }
        this.argMap.add(key, optArg)
        return (long, false)
    }

    private func doShortParser(shortStr: String, args: Array<String>, i: Int64): (Int64, Bool) {
        if (args[i].size < 2) {
            return (1, true)
        }
        var size: Int64 = shortStr.size
        var key: String = args[i][0..2]
        let index: Int64 = shortStr.indexOf(key[1]) ?? -1
        if (index >= 0) {
            let hasArg: Bool = if (index + 1 >= size) {
                false
            } else {
                shortStr[index + 1] == b':'
            }
            if (!hasArg) {
                if (args[i].size == 2) {
                    this.argMap.add(key, "")
                    return (1, false)
                }
                return (1, true)
            }
            if (args[i] == key) {
                if (i + 1 >= args.size) {
                    return (1, true)
                }
                this.argMap.add(key, args[i + 1])
                return (2, false)
            }
            this.argMap.add(key, args[i][2..])
            return (1, false)
        }
        return (1, true)
    }

    /**
     * @throws IllegalArgumentException if errorId is IllegalArgument.
     */
    private func checkShortStr(shortStr: String): String {
        for (item in shortStr.runes()) {
            if (item == r':' || item.isAsciiLetter()) {
                continue
            }
            throw IllegalArgumentException("${exceptionPrompt}${shortStr}\n")
        }
        return shortStr
    }

    /**
     * @throws IllegalArgumentException if errorId is IllegalArgument.
     */
    private func checkLongOpt(longOpts: Array<String>): Array<String> {
        for (item in longOpts) {
            if (item.isEmpty()) {
                continue
            }
            let isDoubleSign: Bool = item.startsWith("--")
            let isSingleSign: Bool = !isDoubleSign && item.startsWith("-")
            let itemCharArray = item.toRuneArray()
            let size: Int64 = itemCharArray.size
            if (isDoubleSign && size >= 3 && itemCharArray[2].isAsciiLetter()) {
                continue
            }
            if (isSingleSign && size >= 2 && itemCharArray[1].isAsciiLetter()) {
                continue
            }
            if (itemCharArray[0].isAsciiLetter()) {
                continue
            }
            throw IllegalArgumentException("${exceptionPrompt}${item}\n")
        }
        return longOpts
    }
}

public enum ArgumentMode <: ToString & Equatable<ArgumentMode> {
    | NoValue
    | RequiredValue
    | OptionalValue

    public func toString(): String {
        match (this) {
            case NoValue => "NoValue"
            case RequiredValue => "RequiredValue"
            case OptionalValue => "OptionalValue"
        }
    }

    public operator func ==(other: ArgumentMode): Bool {
        this.toString() == other.toString()
    }
}

public enum ArgumentSpec {
    | Short(Rune, ArgumentMode)
    | Short(Rune, ArgumentMode, (String) -> Unit)
    | Long(String, ArgumentMode)
    | Long(String, ArgumentMode, (String) -> Unit)
    | Full(String, Rune, ArgumentMode)
    | Full(String, Rune, ArgumentMode, (String) -> Unit)
    | NonOptions((Array<String>) -> Unit)

    prop name: (?String, ?Rune) {
        get() {
            match (this) {
                case Short(r, _) => (None, r)
                case Short(r, _, _) => (None, r)
                case Long(s, _) => (s, None)
                case Long(s, _, _) => (s, None)
                case Full(s, r, _) => (s, r)
                case Full(s, r, _, _) => (s, r)
                case NonOptions(_) => (None, None)
            }
        }
    }

    prop mode: ArgumentMode {
        get() {
            match (this) {
                case Short(_, m) => m
                case Short(_, m, _) => m
                case Long(_, m) => m
                case Long(_, m, _) => m
                case Full(_, _, m) => m
                case Full(_, _, m, _) => m
                case NonOptions(_) => throw NoneValueException()
            }
        }
    }

    prop optionAction: ?(String) -> Unit {
        get() {
            match (this) {
                case Short(_, _, action) => action
                case Long(_, _, action) => action
                case Full(_, _, _, action) => action
                case _ => None
            }
        }
    }

    prop optionKey: String {
        get() {
            match (this) {
                case Short(r, _) => r.toString()
                case Short(r, _, _) => r.toString()
                case Long(s, _) => s
                case Long(s, _, _) => s
                case Full(s, _, _) => s
                case Full(s, _, _, _) => s
                case NonOptions(_) => throw NoneValueException()
            }
        }
    }

    prop nonOptionAction: ?(Array<String>) -> Unit {
        get() {
            match (this) {
                case NonOptions(action) => action
                case _ => None
            }
        }
    }
}

public struct ParsedArguments {
    var _options: HashMap<String, String>
    var _nonOptions: Array<String>

    init(options: HashMap<String, String>, nonOptions: Array<String>) {
        _options = options
        _nonOptions = nonOptions
    }

    public prop options: ReadOnlyMap<String, String> {
        get() {
            _options
        }
    }

    public prop nonOptions: Array<String> {
        get() {
            _nonOptions
        }
    }
}

enum ParseState {
    Start | Option | NonOption | End
}

class ArgParser {
    let _options: HashMap<String, String> = HashMap<String, String>()
    let _nonOptions: ArrayList<String> = ArrayList<String>()
    let _longOptionSpecs: HashMap<String, ArgumentSpec> = HashMap<String, ArgumentSpec>()
    let _shortOptionSpecs: HashMap<Rune, ArgumentSpec> = HashMap<Rune, ArgumentSpec>()
    var _nonOptionSpecs: ?ArgumentSpec = None
    var _index = 0
    let _pendingCallbacks: ArrayList<((String) -> Unit, String)> = ArrayList<((String) -> Unit, String)>()
    var _pendingNonOptionCallback: ?(Array<String>) -> Unit = None
    var _pendingNonOptionArgs: ?Array<String> = None

    func checkDuplicateShortOption(r: Rune) {
        if (_shortOptionSpecs.contains(r) || _longOptionSpecs.contains(String(r))) {
            throw IllegalArgumentException("Duplicated option ${r} founded.")
        }
    }
    func checkDuplicateLongOption(s: String) {
        if (_longOptionSpecs.contains(s) || (s.size == 1 && _shortOptionSpecs.contains(Rune(s[0])))) {
            throw IllegalArgumentException("Duplicated option: ${s} founded.")
        }
    }

    func addSpec(spec: ArgumentSpec): Unit {
        match (spec) { // r: Rune for short option, s: String for long option
            case Short(r, _) =>
                checkDuplicateShortOption(r)
                _shortOptionSpecs.add(r, spec)
            case Short(r, _, _) =>
                checkDuplicateShortOption(r)
                _shortOptionSpecs.add(r, spec)
            case Long(s, m) =>
                checkDuplicateLongOption(s)
                _longOptionSpecs.add(s, spec)
            case Long(s, _, _) =>
                checkDuplicateLongOption(s)
                _longOptionSpecs.add(s, spec)
            case Full(s, r, _) =>
                checkDuplicateShortOption(r)
                checkDuplicateLongOption(s)
                _longOptionSpecs.add(s, spec)
                _shortOptionSpecs.add(r, spec)
            case Full(s, r, _, _) =>
                checkDuplicateShortOption(r)
                checkDuplicateLongOption(s)
                _longOptionSpecs.add(s, spec)
                _shortOptionSpecs.add(r, spec)
            case NonOptions(action) => _nonOptionSpecs = spec
        }
    }

    func addOptionAndExecuteAction(key: String, val: String, spec: ArgumentSpec) {
        _options.add(key, val)
        match (spec.name) {
            case (Some(longKey), Some(shortKey)) =>
                if (key == String(shortKey)) {
                    _options.add(longKey, val)
                } else if (key == longKey) {
                    _options.add(String(shortKey), val)
                }
            case _ => ()
        }
        if (let Some(action) <- spec.optionAction) {
            _pendingCallbacks.add((action, val))
        }
    }

    func parseLongOption(arg: String, next: ?String): Bool {
        let splited: Array<String> = arg.split("=", 2) // if `=` not exist in arg, [arg] will get, else [option, value] get.

        return if (let Some(spec) <- _longOptionSpecs.get(splited[0])) {
            match ((spec.mode, splited.size == 1)) { // splited.size == 1 means no `=`, simply like --option
                case (OptionalValue, true) => addOptionAndExecuteAction(splited[0], "", spec) // --option
                case (OptionalValue, false) => addOptionAndExecuteAction(splited[0], splited[1], spec) // --option=value
                case (RequiredValue, true) =>
                    addOptionAndExecuteAction(splited[0],
                        next ?? throw ArgumentParseException("Missing option value for ${splited[0]}."), spec) // --option value
                    _index += 1 // consume next string
                case (RequiredValue, false) => addOptionAndExecuteAction(splited[0], splited[1], spec) // --option=value
                case (NoValue, true) => addOptionAndExecuteAction(splited[0], "", spec) // --option
                case (NoValue, false) => throw ArgumentParseException("Unexcepted value for ${splited[0]}.")
            }
            true
        } else {
            false
        }
    }

    // Initialize and filter a list for argument specs that have no value, until first not NoValue option.
    func generateNoValueSpecForShortOption(runeArray: Array<Rune>): ArrayList<ArgumentSpec> {
        let arr: ArrayList<ArgumentSpec> = ArrayList<ArgumentSpec>()
        let set = HashSet<Rune>() // cjlint-ignore !G.VAR.02
        for ((idx, c) in enumerate(runeArray.iterator())) {
            if (let Some(spec) <- _shortOptionSpecs.get(c) && !set.contains(c) && !_options.contains(spec.optionKey) &&
                spec.mode == NoValue) { // cjlint-ignore !G.EXP.03 
                arr.add(spec)
                set.add(c)
            } else {
                break
            }
        }
        arr
    }
    func parseNoValueForShortOption(runeArray: Array<Rune>, next: ?String) {
        let noValueSpecs = generateNoValueSpecForShortOption(runeArray)

        for (spec in noValueSpecs) {
            let key = spec.name[1]?.toString() ?? "" // should not be "", just for pass the compile check.
            addOptionAndExecuteAction(key, "", spec)
        }

        let cur = runeArray[0]
        if (runeArray.size - 1 == noValueSpecs.size) { // for last rune
            let spec = _shortOptionSpecs.get(runeArray[runeArray.size - 1]) ?? throw ArgumentParseException(
                "Unknown option: ${cur}.")
            let key = String(runeArray[runeArray.size - 1])
            match (spec.mode) {
                case OptionalValue => addOptionAndExecuteAction(key, "", spec)
                case RequiredValue =>
                    let value = next ?? throw ArgumentParseException("Missing option value for ${cur}.")
                    _index += 1
                    addOptionAndExecuteAction(key, value, spec)
                case NoValue => throw ArgumentParseException("Should never reach here becauese of previously check.")
            }
        } else if (runeArray.size - 1 > noValueSpecs.size) { // for not last rune
            let spec = _shortOptionSpecs.get(runeArray[noValueSpecs.size]) ?? throw ArgumentParseException(
                "Unknown option: ${runeArray[noValueSpecs.size]}.")
            let key = String(runeArray[noValueSpecs.size])
            if (spec.mode == RequiredValue && noValueSpecs.size < runeArray.size - 2) {
                addOptionAndExecuteAction(key, String(runeArray[noValueSpecs.size + 1..]),
                    spec)
            } else if (spec.mode == OptionalValue) {
                addOptionAndExecuteAction(key, String(runeArray[noValueSpecs.size + 1..]),
                    spec)
            } else {
                throw ArgumentParseException("Unknown option: ${runeArray[noValueSpecs.size]}.")
            }
        }
    }

    func parseShortOption(arg: String, next: ?String) {
        let runeArray = arg.toRuneArray()
        let runeSize = runeArray.size
        let cur = runeArray.get(0).getOrThrow()

        if (let Some(spec) <- _shortOptionSpecs.get(cur)) {
            let key = String(cur)
            match (spec.mode) {
                case OptionalValue =>
                    let value = if (runeSize > 1) {
                        String(runeArray[1..]) // rune after a OptionalValue option is value
                    } else {
                        ""
                    }
                    addOptionAndExecuteAction(key, value, spec)
                case RequiredValue =>
                    let value = if (runeSize == 1) { // -o v
                        _index += 1
                        next ?? throw ArgumentParseException("Missing option value for ${cur}.")
                    } else { // -ov
                        String(runeArray[1..])
                    }
                    addOptionAndExecuteAction(key, value, spec)
                case NoValue => parseNoValueForShortOption(runeArray, next)
            }
        } else {
            throw ArgumentParseException("Unknown option: ${arg}.")
        }
    }

    // '-' or '--' should be removed before this function called.
    func parseOption(arg: String, next: ?String, longOptionOnly!: Bool = false) {
        if (arg.size == 0) {
            throw ArgumentParseException("Option cannot be empty.")
        }

        if (!parseLongOption(arg, next)) {
            if (longOptionOnly) {
                throw ArgumentParseException("Unknown option: ${arg}.")
            }
            parseShortOption(arg, next)
        }
    }

    func executeCallbacks() {
        for ((callback, value) in _pendingCallbacks) {
            callback(value)
        }
        if (let Some(action) <- _pendingNonOptionCallback) {
            let args = _pendingNonOptionArgs ?? Array<String>()
            action(args)
        }
    }

    func parse(args: Array<String>, specs: Array<ArgumentSpec>): ParsedArguments {
        specs |> forEach(addSpec)

        var state = ParseState.Start
        while (_index < args.size) {
            let cur = args[_index]
            match (state) {
                case ParseState.Start =>
                    if (cur == "--") {
                        state = ParseState.End
                    } else if (!cur.isEmpty() && cur[0] == b'-') {
                        state = ParseState.Option
                        _index -= 1
                    } else {
                        state = ParseState.NonOption
                        _index -= 1
                    }
                case ParseState.Option =>
                    if (cur.startsWith("--")) {
                        parseOption(cur[2..], args.get(_index + 1), longOptionOnly: true)
                    } else if (cur.startsWith("-")) {
                        parseOption(cur[1..], args.get(_index + 1))
                    } else {
                        throw ArgumentParseException("Illegal option format for ${cur}.") // just for branch coverage, should never be here
                    }
                    state = ParseState.Start
                case ParseState.NonOption =>
                    _nonOptions.add(cur)
                    state = ParseState.Start
                case ParseState.End => _nonOptions.add(cur)
            }
            _index += 1
        }
        if (let Some(action) <- _nonOptionSpecs?.nonOptionAction) {
            _pendingNonOptionCallback = action
            _pendingNonOptionArgs = _nonOptions.toArray()
        }
        let result = ParsedArguments(_options, _nonOptions.toArray())
        executeCallbacks()
        return result
    }
}

public class ArgumentParseException <: Exception {
    public init() {
        super()
    }

    public init(message: String) {
        super(message)
    }

    protected override func getClassName(): String {
        return "ArgumentParseException"
    }
}

public func parseArguments(args: Array<String>, specs: Array<ArgumentSpec>): ParsedArguments {
    ArgParser().parse(args, specs)
}