// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;

namespace Microsoft.AspNetCore.Rewrite.ApacheModRewrite;

/// <summary>
/// Parses the "CondPattern" portion of the RewriteCond.
/// RewriteCond TestString CondPattern
/// </summary>
internal sealed class ConditionPatternParser
{
    private const char Not = '!';
    private const char Dash = '-';
    private const char Less = '<';
    private const char Greater = '>';
    private const char EqualSign = '=';

    /// <summary>
    /// Given a CondPattern, create a ParsedConditionExpression, containing the type of operation
    /// and value.
    /// ParsedConditionExpression is an intermediary object, which will be made into a ConditionExpression
    /// once the flags are parsed.
    /// </summary>
    /// <param name="condition">The CondPattern portion of a mod_rewrite RewriteCond.</param>
    /// <returns>A new parsed condition.</returns>
    public static ParsedModRewriteInput ParseActionCondition(string condition)
    {
        if (condition == null)
        {
            condition = string.Empty;
        }
        var context = new ParserContext(condition);
        var results = new ParsedModRewriteInput();
        if (!context.Next())
        {
            throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
        }

        // If we hit a !, invert the condition
        if (context.Current == Not)
        {
            results.Invert = true;
            if (!context.Next())
            {
                // Dangling !
                throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
            }
        }

        // Control Block for strings. Set the operation and type fields based on the sign
        // Switch on current character
        switch (context.Current)
        {
            case Greater:
                if (!context.Next())
                {
                    // Dangling ">"
                    throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
                }
                if (context.Current == EqualSign)
                {
                    if (!context.Next())
                    {
                        // Dangling ">="
                        throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
                    }
                    results.OperationType = OperationType.GreaterEqual;
                    results.ConditionType = ConditionType.StringComp;
                }
                else
                {
                    results.OperationType = OperationType.Greater;
                    results.ConditionType = ConditionType.StringComp;
                }
                break;
            case Less:
                if (!context.Next())
                {
                    // Dangling "<"
                    throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
                }
                if (context.Current == EqualSign)
                {
                    if (!context.Next())
                    {
                        // Dangling "<="
                        throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
                    }
                    results.OperationType = OperationType.LessEqual;
                    results.ConditionType = ConditionType.StringComp;
                }
                else
                {
                    results.OperationType = OperationType.Less;
                    results.ConditionType = ConditionType.StringComp;
                }
                break;
            case EqualSign:
                if (!context.Next())
                {
                    // Dangling "="
                    throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
                }
                results.OperationType = OperationType.Equal;
                results.ConditionType = ConditionType.StringComp;
                break;
            case Dash:
                results = ParseProperty(context, results.Invert);
                if (results.ConditionType == ConditionType.PropertyTest)
                {
                    return results;
                }
                context.Next();
                break;
            default:
                results.ConditionType = ConditionType.Regex;
                break;
        }

        // Capture the rest of the string guarantee validity.
        results.Operand = condition.Substring(context.GetIndex());
        if (IsValidActionCondition(results))
        {
            return results;
        }
        else
        {
            throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(condition, context.Index));
        }
    }

    /// <summary>
    /// Given that the current index is a property (ex checks for directory or regular files), create a
    /// new ParsedConditionExpression with the appropriate property operation.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="invert"></param>
    /// <returns></returns>
    private static ParsedModRewriteInput ParseProperty(ParserContext context, bool invert)
    {
        if (!context.Next())
        {
            throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
        }

        switch (context.Current)
        {
            case 'd':
                return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.Directory, operand: null);
            case 'f':
                return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.RegularFile, operand: null);
            case 'F':
                return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.ExistingFile, operand: null);
            case 'h':
            case 'L':
                return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.SymbolicLink, operand: null);
            case 's':
                return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.Size, operand: null);
            case 'U':
                return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.ExistingUrl, operand: null);
            case 'x':
                return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.Executable, operand: null);
            case 'e':
                if (!context.Next() || context.Current != 'q')
                {
                    // Illegal statement.
                    throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
                }
                return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.Equal, operand: null);
            case 'g':
                if (!context.Next())
                {
                    throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
                }
                switch (context.Current)
                {
                    case 't':
                        return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.Greater, operand: null);
                    case 'e':
                        return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.GreaterEqual, operand: null);
                    default:
                        throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
                }
            case 'l':
                // name conflict with -l and -lt/-le, so the assumption is if there is no
                // charcters after -l, we assume it a symbolic link
                if (!context.Next())
                {
                    return new ParsedModRewriteInput(invert, ConditionType.PropertyTest, OperationType.SymbolicLink, operand: null);
                }
                switch (context.Current)
                {
                    case 't':
                        return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.Less, operand: null);
                    case 'e':
                        return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.LessEqual, operand: null);
                    default:
                        throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
                }
            case 'n':
                if (!context.Next() || context.Current != 'e')
                {
                    throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
                }
                return new ParsedModRewriteInput(invert, ConditionType.IntComp, OperationType.NotEqual, operand: null);
            default:
                throw new FormatException(Resources.FormatError_InputParserUnrecognizedParameter(context.Template, context.Index));
        }
    }

    private static bool IsValidActionCondition(ParsedModRewriteInput results)
    {
        if (results.ConditionType == ConditionType.IntComp)
        {
            // If the type is an integer, verify operand is actually an int
            if (!int.TryParse(results.Operand, NumberStyles.None, CultureInfo.InvariantCulture, out _))
            {
                return false;
            }
        }
        return true;
    }
}
