/*
 * RegExOps.cs --
 *
 * Copyright (c) 2007-2012 by Joe Mistachkin.  All rights reserved.
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * RCS: @(#) $Id: $
 */

using System;
using System.Text;
using System.Text.RegularExpressions;
using Eagle._Attributes;
using Eagle._Components.Public;
using Eagle._Constants;
using Eagle._Containers.Public;
using Eagle._Interfaces.Public;

namespace Eagle._Components.Private
{
    [ObjectId("b45f7d61-390b-4aae-a80b-cd88a1444bdf")]
    internal static class RegExOps
    {
        #region Regular Expression Support Methods
        public static string RegsubCountMatchCallback(
            Match match
            )
        {
            string result = null;

            IAnyPair<Interpreter, IClientData> pair =
                Interpreter.GetActivePair(typeof(RegsubClientData));

            if (pair != null)
            {
                Interpreter interpreter = pair.X;
                RegsubClientData regsubClientData = pair.Y as RegsubClientData;

                if ((interpreter != null) && (regsubClientData != null))
                {
                    //
                    // NOTE: Keep track of how many matches we have been given.
                    //
                    regsubClientData.Count++;

                    //
                    // NOTE: Get the additional parameters we need to perform
                    //       the callback from the clientData.
                    //
                    string pattern = regsubClientData.Pattern;
                    string input = regsubClientData.Input;
                    string replacement = regsubClientData.Replacement;
                    bool quote = regsubClientData.Quote;
                    bool args = regsubClientData.Args;
                    bool strict = regsubClientData.Strict;

                    //
                    // NOTE: Perform our custom replacements and return the
                    //       result.
                    //
                    result = TranslateRegsubSpec(pattern, input, replacement,
                        replacement, quote, args, strict, match);
                }
            }

            return result;
        }

        ///////////////////////////////////////////////////////////////////////

        public static string RegsubEvaluateMatchCallback(
            Match match
            )
        {
            Result result = null;

            IAnyPair<Interpreter, IClientData> pair =
                Interpreter.GetActivePair(typeof(RegsubClientData));

            if (pair != null)
            {
                Interpreter interpreter = pair.X;
                RegsubClientData regsubClientData = pair.Y as RegsubClientData;

                if ((interpreter != null) && (regsubClientData != null))
                {
                    //
                    // NOTE: Keep track of how many matches we have been given.
                    //
                    regsubClientData.Count++;

                    //
                    // NOTE: Get the script to evaluate from the clientData.
                    //
                    string text = regsubClientData.Text;
                    IScriptLocation textLocation = regsubClientData.TextLocation;

                    if (!String.IsNullOrEmpty(text))
                    {
                        //
                        // NOTE: Get the additional parameters we need to
                        //       perform the script callback from the
                        //       clientData.
                        //
                        string pattern = regsubClientData.Pattern;
                        string input = regsubClientData.Input;
                        string replacement = regsubClientData.Replacement;
                        bool quote = regsubClientData.Quote;
                        bool args = regsubClientData.Args;
                        bool strict = regsubClientData.Strict;

                        //
                        // NOTE: Special processing to pass data to the script
                        //       to be evaluated.
                        //
                        StringList list = null;

                        //
                        // WARNING: Cannot cache list representation here, the
                        //          list is modified below.
                        //
                        ReturnCode code = Parser.SplitList(
                            interpreter, text, 0, Length.Invalid,
                            false, ref list, ref result);

                        if (code == ReturnCode.Ok)
                        {
                            for (int index = 0; index < list.Count; index++)
                                list[index] = TranslateRegsubSpec(
                                    pattern, input, replacement,
                                    list[index], quote, args, strict, match);

                            text = list.ToString();

                            code = interpreter.EvaluateScript(
                                text, textLocation, ref result);
                        }

                        if (code != ReturnCode.Ok)
                        {
                            Engine.AddErrorInformation(interpreter, result,
                                String.Format("{0}    (-regsub command)",
                                    Environment.NewLine));

                            //
                            // NOTE: This is our only way out of here.  This
                            //       exception will be caught by the command
                            //       handler for regsub and converted into a
                            //       script error.
                            //
                            throw new ScriptException(code, result);
                        }
                    }
                    else
                    {
                        //
                        // NOTE: This is allowed, translate a null or empty
                        //       script into an empty result.
                        //
                        result = String.Empty;
                    }
                }
            }

            return result;
        }

        ///////////////////////////////////////////////////////////////////////

        private static string TranslateRegsubSpec(
            string pattern,     // the original pattern string.
            string input,       // the original input string.
            string replacement, // the original replacement string.
            string text,        // string containing the subSpecs to process.
            bool quote,         // use list element quoting?
            bool args,          // use of \P, \I, and \S permitted in subSpec.
            bool strict,        // use for strict conformance to the Tcl docs.
            Match match         // the current Regex match, if any.
            )
        {
            string result;

            if (!String.IsNullOrEmpty(text))
            {
                //
                // NOTE: We handle the following Tcl compatible regsub
                //       subSpecs:
                //
                //       "\&"          : Always replaced with a literal
                //                       ampersand.
                //
                //       "\\"          : Always replaced with a literal
                //                       backslash.
                //
                //       "&"           : Translated to "$&" OR replaced with
                //                       the portion of string that matched
                //                       exp if a valid match was supplied.
                //
                //       "\n" (0 to 9) : Translated to "$n", OR replaced with
                //                       the portion of string that matched the
                //                       Nth parenthesized subexpression of exp,
                //                       except for "\0" (which will be treated
                //                       just like "&") if a valid match was
                //                       supplied.
                //
                //       We also handle the following custom extensions
                //       typically used with "regsub -eval" (these are ONLY
                //       recognized if the "args" parameter is true; otherwise,
                //       they are ignored):
                //
                //       "\P"          : Always replaced with the original
                //                       pattern.  ONLY recognized if the
                //                       "args" parameter is true.
                //
                //       "\I"          : Always replaced with the original
                //                       input string.  ONLY recognized if
                //                       the "args" parameter is true.
                //
                //       "\S"          : Always replaced with the original
                //                       replacement (subSpec).  ONLY
                //                       recognized if the "args" parameter
                //                       is true.
                //
                //       Anything else will be passed through verbatim.
                //
                StringBuilder builder = StringOps.NewStringBuilder();

                for (int index = 0; index < text.Length; index++)
                {
                    char character = text[index];

                    switch (character)
                    {
                        case Characters.Ampersand:
                            {
                                //
                                // NOTE: If a match was supplied, replace this with
                                //       the entire matched value.
                                //
                                if (match != null)
                                {
                                    //
                                    // NOTE: Append the value of the entire match,
                                    //       quoting it if requested.
                                    //
                                    builder.Append(quote ?
                                        Parser.Quote(match.Value) :
                                        match.Value);
                                }
                                else
                                {
                                    //
                                    // NOTE: Translate an unescaped ampersand to the
                                    //       .NET Framework equivalent, which includes
                                    //       a dollar sign prefix and represents the
                                    //       entired matched expression.  An example
                                    //       is "&" to "$&".
                                    //
                                    builder.Append(
                                        Characters.DollarSign.ToString() +
                                        character.ToString());
                                }
                                break;
                            }
                        case Characters.Backslash:
                            {
                                //
                                // NOTE: Are there more characters remaining after
                                //       this one?
                                //
                                if ((index + 1) < text.Length)
                                {
                                    //
                                    // NOTE: Something has been escaped with a
                                    //       backslash, we need to look at the next
                                    //       character.
                                    //
                                    char nextCharacter = text[index + 1];

                                    switch (nextCharacter)
                                    {
                                        case Characters.Ampersand:
                                            {
                                                //
                                                // NOTE: We hit an escaped ampersand,
                                                //       insert the literal ampersand.
                                                //
                                                builder.Append(nextCharacter);
                                                break;
                                            }
                                        case Characters.Backslash:
                                            {
                                                //
                                                // NOTE: We hit an escaped backslash,
                                                //       insert the literal backslash.
                                                //
                                                builder.Append(nextCharacter);
                                                break;
                                            }
                                        case Characters.P:
                                            {
                                                //
                                                // NOTE: This feature is not supported
                                                //       in strict mode.
                                                //
                                                if (!args) goto default;

                                                //
                                                // NOTE: Append the original "exp"
                                                //       (pattern string) argument.
                                                //
                                                builder.Append(pattern);
                                                break;
                                            }
                                        case Characters.I:
                                            {
                                                //
                                                // NOTE: This feature is not supported
                                                //       in strict mode.
                                                //
                                                if (!args) goto default;

                                                //
                                                // NOTE: Append the original "string"
                                                //       (input string) argument.
                                                //
                                                builder.Append(input);
                                                break;
                                            }
                                        case Characters.S:
                                            {
                                                //
                                                // NOTE: This feature is not supported
                                                //       in strict mode.
                                                //
                                                if (!args) goto default;

                                                //
                                                // NOTE: Append the original "subSpec"
                                                //       (replacement string) argument.
                                                //
                                                builder.Append(replacement);
                                                break;
                                            }
                                        default:
                                            {
                                                //
                                                // NOTE: Is this a backslash followed by
                                                //       a digit?  If so, we need to
                                                //       append the applicable match
                                                //       group value, if any.
                                                //
                                                if (StringOps.CharIsAsciiDigit(nextCharacter))
                                                {
                                                    if (match != null)
                                                    {
                                                        int groupIndex = nextCharacter - Characters.Zero;

                                                        if (groupIndex < match.Groups.Count)
                                                        {
                                                            //
                                                            // NOTE: Append the value of
                                                            //       the match group,
                                                            //       quoting it if requested.
                                                            //
                                                            builder.Append(quote ?
                                                                Parser.Quote(match.Groups[groupIndex].Value) :
                                                                match.Groups[groupIndex].Value);
                                                        }
                                                    }
                                                    else
                                                    {
                                                        //
                                                        // NOTE: We hit a properly escaped subSpec,
                                                        //       insert the .NET Framework equivalent,
                                                        //       which includes a dollar sign prefix.
                                                        //       An example is "\1" to "$1".
                                                        //
                                                        builder.Append(
                                                            Characters.DollarSign.ToString() +
                                                            nextCharacter.ToString());
                                                    }
                                                }
                                                else
                                                {
                                                    if (strict)
                                                    {
                                                        //
                                                        // BUGFIX: No, we do not actually want to do that.
                                                        //         Even though this portion of the subSpec
                                                        //         pattern argument handling for [regsub]
                                                        //         is poorly specified in the Tcl
                                                        //         documentation, what we actually need to
                                                        //         do here is insert the literal backslash
                                                        //         followed by the literal character we
                                                        //         just encountered.
                                                        //
                                                        //         The exact rule is as follows:
                                                        //
                                                        //         "Any backslash in the subSpec pattern
                                                        //          argument NOT followed by an ampersand,
                                                        //          a single decimal digit, or another
                                                        //          backslash is treated as a literal
                                                        //          backslash."
                                                        //
                                                        //         As a consequence of the above rule, any
                                                        //         backslash followed by any character NOT
                                                        //         covered by the above rule will be
                                                        //         inserted into the output string literally.
                                                        //
                                                        builder.Append(character);
                                                        builder.Append(nextCharacter);
                                                    }
                                                    else
                                                    {
                                                        //
                                                        // NOTE: We hit an escaped character we do not
                                                        //       recognize, just insert the unescaped
                                                        //       equivalent.
                                                        //
                                                        builder.Append(nextCharacter);
                                                    }
                                                }
                                                break;
                                            }
                                    }

                                    //
                                    // NOTE: Now, skip beyond the escaped character
                                    //       we just handled.
                                    //
                                    index++;
                                }
                                else
                                {
                                    //
                                    // NOTE: We hit an isolated backslash at the
                                    //       end of the string, just insert a
                                    //       literal backslash.
                                    //
                                    builder.Append(character);
                                }
                                break;
                            }
                        default:
                            {
                                //
                                // NOTE: Anything we do not handle gets added
                                //       verbatim.
                                //
                                builder.Append(character);
                                break;
                            }
                    }
                }

                result = builder.ToString();
            }
            else
            {
                //
                // NOTE: Garbage in, garbage out.
                //
                result = text;
            }

            return result;
        }
        #endregion
    }
}
