/*
 * DrFx - FxCop Report Translator and Visualizer.
 * Copyright (C) 2010 Sasa Yuan
 * 
 * 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/>.
 */



using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Sasa.QualityTools.DrFx.Console
{
    /// <summary>
    /// コマンドライン引数の解析機能を提供します。
    /// </summary>
    internal class Getopt
    {
        private Dictionary<string, Option> longOptions;



        internal string ShortOption { get; set; }



        internal Getopt()
        {
            ShortOption = String.Empty;
            this.longOptions = new Dictionary<string, Option>();
        }


        /// <summary>
        /// コレクションに新しいオプション定義を追加します。
        /// </summary>
        /// <param name="definition">ロングオプションの定義 (プレフィックス "--" は不要)。</param>
        /// <param name="symbol">オプションを表すシンボル。</param>
        /// <param name="hasArgument">オプションが引数を持つかどうか。</param>
        internal void AddLongOption(string definition, char symbol, bool hasArgument)
        {
            this.longOptions.Add("--" + definition, new Option(symbol, hasArgument));
        }

        /// <summary>
        /// 指定されたショートオプション定義を解析し、ショートオプションオブジェクトの配列を作成します。
        /// </summary>
        /// <param name="definition">ショートオプション定義。</param>
        /// <returns>指定されたショートオプション定義をもとに作成したショートオプションオブジェクトの配列。</returns>
        internal static Option[] ParseShortOptionDefinition(string definition)
        {
            List<Option> options = new List<Option>();
            using (StringReader reader = new StringReader(definition))
            {
                int symbol;
                while ((symbol = reader.Read()) != -1)
                {
                    if ((char)reader.Peek() == ':')
                    {
                        options.Add(new Option((char)symbol, true));
                        reader.Read();
                    }
                    else
                    {
                        options.Add(new Option((char)symbol, false));
                    }
                }
            }
            return options.ToArray();
        }

        /// <summary>
        /// 指定されたコマンドライン引数 <paramref name="args"/> を解析します。
        /// </summary>
        /// <remarks>
        /// このメソッドは '-' から始まる文字列をオプションとみなします。
        /// オプションを発見した場合はオプション処理のため <paramref name="optionHandler"/> が呼び出されます。
        /// <paramref name="optionHandler"/> 呼び出し時には、発見したオプションを表すシンボルが渡されます。
        /// 引数を持つオプションの場合は、オプションの引数も一緒に渡されます。
        /// 引数を持たないオプションの場合は、オプションの引数として null が渡されます。
        /// 定義されていないオプションを発見した場合は、シンボル '?' を引数にして <paramref name="optionHandler"/> が呼び出されます。
        /// "--" という文字列を見つけた場合はオプション解析をやめ、以降は全て引数とみなします。
        /// </remarks>
        /// <param name="args">コマンドライン引数。</param>
        /// <param name="optionHandler">オプションを処理するコールバックデリゲート。</param>
        /// <returns>オプション以外の引数の配列。</returns>
        internal string[] Parse(string[] args, Action<char, string> optionHandler)
        {
            Option[] shortOptions = ParseShortOptionDefinition(ShortOption);

            List<string> nonopts = new List<string>();

            for (int i = 0; i < args.Length; i++)
            {
                if (args[i] == "--")
                {
                    i++;
                    while (i < args.Length)
                    {
                        nonopts.Add(args[i++]);
                    }
                }
                else if (args[i].StartsWith("--", StringComparison.Ordinal))
                {
                    Option option = null;
                    try
                    {
                        option = this.longOptions[args[i]];
                    }
                    catch (KeyNotFoundException)
                    {
                        optionHandler('?', args[i]);
                        continue;
                    }

                    if (option.HasArgument)
                    {
                        if (i == args.Length - 1)
                        {
                            string message = "オプション '" + args[i] + "' に引数が指定されていません。";
                            throw new MissingOptionArgumentException(message);
                        }
                        string optarg = args[++i];
                        optionHandler(option.Symbol, optarg);
                    }
                    else
                    {
                        optionHandler(option.Symbol, null);
                    }
                }
                else if (args[i].StartsWith("-", StringComparison.Ordinal))
                {
                    for (int j = 1; j < args[i].Length; j++)
                    {
                        Option option = null;
                        try
                        {
                            option = FindOption(shortOptions, args[i][j]);
                        }
                        catch (UndefinedOptionException e)
                        {
                            optionHandler('?', e.SpecifiedOption);
                            continue;
                        }

                        if (option.HasArgument)
                        {
                            if (j < args[i].Length - 1)
                            {
                                string optarg = args[i].Substring(j + 1);
                                optionHandler(option.Symbol, optarg);
                            }
                            else
                            {
                                if (i == args.Length - 1)
                                {
                                    string message = "オプション '-" + option.Symbol + "' に引数が指定されていません。";
                                    throw new MissingOptionArgumentException(message);
                                }
                                string optarg = args[++i];
                                optionHandler(option.Symbol, optarg);
                            }
                            break;
                        }
                        else
                        {
                            optionHandler(option.Symbol, null);
                        }
                    }
                }
                else
                {
                    nonopts.Add(args[i]);
                }
            }

            return nonopts.ToArray();
        }

        /// <summary>
        /// 指定されたシンボルに対応するオプション定義を検索します。
        /// </summary>
        /// <param name="options">検索対象のオプション定義の配列。</param>
        /// <param name="symbol">オプションを表すシンボル。</param>
        /// <returns>指定されたシンボルに対応するオプション定義。</returns>
        internal static Option FindOption(Option[] options, char symbol)
        {
            try
            {
                return options.First(o => o.Symbol == symbol);
            }
            catch (InvalidOperationException e)
            {
                throw new UndefinedOptionException("指定されたオプションは定義されていないため、認識できません。", "-" + symbol, e);
            }
        }
    }
}
