#!/usr/bin/env python3
#
# Copyright (C) 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018  Rickard Lindberg, Roger Lindberg
#
# This file is part of Timeline.
#
# Timeline 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.
#
# Timeline 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 Timeline.  If not, see <http://www.gnu.org/licenses/>.


"""
The CmdLineParser reads the command line input and stores the separate
parts as properties, according to the given specification.

A command line has two types of arguments
    - Positional argument
    - Named argument

Arguments are separated with on or more spaces.

A named argument is always prefixed with a dash (-) which a named argument isn't.

A named argument has two types. One with a single dash prefix and one with a double
dash prefix. The first one is intended for single character names and the second
one for long names. Both can have a specified value.

If a name contains a dash character, the attribute name of the parser will be the same
but with the dash replaced with an underscore character.

The name 'invalid-args' is reserved for the parser and cannot be used as a command line
argument.

By default, the input argument list is taken from sys.argv[1:], but it can be
specified as an argument to the CmdLineParser constructor.

Example:
    cmd line:  pgm.exe -v --log-name mylog filename

    pgm.exe             The name of the executed program
    -v                  A named argument without a value
    --log-name mylog    A named argument with the value mylog
    filename            A positional argument

Usage:
    # Define valid arguments
    specs = [
        PositionalArg(name="filename"),
        NamedArg(name="v", long_name="verbose"),
        NamedArg(long_name="log-name", has_value=True),
    ]

    # Create the parser and process the command line input
    parser = CmdLineParser(specs)

    # Use input data
    if parser.verbose:
        print("Verbose mode")
    print(f"logfile : {parser.log_name}")
    print(f"filename: {parser.filename}")

"""

import sys


class Arg:
    def __init__(self, name="", long_name="", has_value=False):
        self._name = name
        self._long_name = long_name
        self._has_value = has_value

    @property
    def name(self):
        return self._name

    @property
    def long_name(self):
        return self._long_name

    @property
    def has_value(self):
        return self._has_value


class PositionalArg(Arg):
    def __init__(self, name):
        Arg.__init__(self, name)


class NamedArg(Arg):
    def __init__(self, name="", long_name="", has_value=None):
        Arg.__init__(self, name, long_name, has_value)


class CmdLineParser:
    """
    No  arguments given on the command line

    >>> sys.argv = ["pgm.exe"]
    >>> specs = [
    ... PositionalArg(name="filename"),
    ... NamedArg(name="v", long_name="verbose"),
    ... NamedArg(name="l", long_name="log-name", has_value=True),]
    >>> parser = CmdLineParser(specs)
    >>> parser.filename
    False
    >>> parser.log_name
    False
    >>> parser.l
    False
    >>> parser.verbose
    False
    >>> parser.v
    False
    >>> parser.invalid_args
    []

    Command line arguments given

    >>> sys.argv = ["pgm.exe", "-v", "--log-name", "mylog", "myfile"]
    >>> parser = CmdLineParser(specs)
    >>> parser.filename
    'myfile'
    >>> parser.v and parser.verbose
    True
    >>> parser.log_name_value
    'mylog'
    >>> parser.l_value
    'mylog'

    Invalid Command line arguments given

    >>> sys.argv = ["pgm.exe", "-vv", "--log-name", "mylog", "myfile"]
    >>> parser = CmdLineParser(specs)
    >>> len(parser.invalid_args)
    1
    >>> parser.invalid_args[0]
    '-vv'

    Same arg given twice

    >>> sys.argv = ["pgm.exe", "--log-name", "mylog2", "--log-name", "mylog2", "myfile1", "myfile2"]
    >>> parser = CmdLineParser(specs)

    If named args are duplicated the last one is used

    >>> parser.log_name_value
    'mylog2'

    Positional arguments are always assigned from left to right.

    >>> parser.filename
    'myfile1'
    >>> parser.invalid_args[0]
    'myfile2'

    The 'name' agument is not mandatory for NamedArg

    >>> sys.argv = ["pgm.exe", "--verbose", "myfile1"]
    >>> specs = [
    ... PositionalArg(name="filename"),
    ... NamedArg(long_name="verbose")]
    >>> parser = CmdLineParser(specs)
    >>> parser.verbose
    True
    """

    def __init__(self, specs, args=None):
        self._positional_specs = [s for s in specs if isinstance(s, PositionalArg)]
        self._named_specs = [s for s in specs if isinstance(s, NamedArg)]
        self._args = args or sys.argv[1:]
        self._invalid_args = []
        self._positional_args = []
        self._init_args()
        self._parse()

    @property
    def invalid_args(self):
        return self._invalid_args

    def _init_args(self):
        for spec in self._positional_specs:
            spec.attr_name = self._attr_name(spec.name)
            setattr(self, spec.attr_name, False)
        for spec in self._named_specs:
            self._create_attribute(spec)
            self._set_attribute(spec, False)
            self._set_attribute_value(spec, None)

    def _create_attribute(self, spec):
        if len(spec.name) > 0:
            spec.attr_name = self._attr_name(spec.name)
        if len(spec.long_name) > 0:
            spec.attr_long_name = self._attr_name(spec.long_name)

    def _set_attribute(self, spec, value):
        if len(spec.name) > 0:
            setattr(self, spec.attr_name, value)
        if len(spec.long_name) > 0:
            setattr(self, spec.attr_long_name, value)

    def _set_attribute_value(self, spec, value):
        if len(spec.name) > 0:
            setattr(self, f"{spec.attr_name}_value", value)
        if len(spec.long_name) > 0:
            setattr(self, f"{spec.attr_long_name}_value", value)

    def _attr_name(self, name):
        return name.replace("-", "_")

    @property
    def _next_arg(self):
        return self._args[0]

    @property
    def _more_args_exists(self):
        return len(self._args) > 0

    @property
    def _next_arg_is_named(self):
        return self._next_arg.startswith("-")

    @property
    def _next_arg_is_positional(self):
        return not self._next_arg_is_named

    def _remove_first_arg(self):
        self._args = self._args[1:]

    def _parse(self):
        while self._more_args_exists:
            if self._next_arg_is_named:
                self._parse_named_arg()
            else:
                self._parse_positional_arg()

    def _parse_named_arg(self):
        spec = self._find_spec()
        if spec is None:
            self._invalid_args.append(self._next_arg)
            self._remove_first_arg()
        else:
            self._set_attribute(spec, True)
            self._remove_first_arg()
            if spec.has_value and self._more_args_exists:
                if self._next_arg_is_positional:
                    self._set_attribute_value(spec, self._next_arg)
                    self._remove_first_arg()

    def _find_spec(self):
        for spec in self._named_specs:
            if (
                f"-{spec.name}" == self._next_arg
                or f"--{spec.long_name}" == self._next_arg
            ):
                return spec

    def _parse_positional_arg(self):
        index = len(self._positional_args)
        try:
            spec = self._positional_specs[index]
            setattr(self, spec.attr_name, self._next_arg)
            self._positional_args.append(self._next_arg)
        except IndexError:
            self._invalid_args.append(self._next_arg)
        self._remove_first_arg()


if __name__ == "__main__":
    import doctest

    doctest.testmod()
