using System;
using System.Collections.Generic;
using System.Linq;
using Dacryptero.Search.AST;

namespace Dacryptero.Search.Parser
{
    public class AstTransformer
    {
        /// <summary>
        /// This adds more transformation options to the transformation pipeline. It is only
        /// recommended to apply this flag after <see cref="AstPostTransformer" /> has completed
        /// its transformation.
        /// </summary>
        public bool ExtendedTransformation { get; set; }

        public AstBase Transform(AstBase ast)
        {
            bool changed;
            do
            {
                var @new = ast.Transform(CombinedTransformer);
                changed = @new is not null;
                ast = @new ?? ast;
            }
            while (changed);
            return ast;
        }

        private AstBase? CombinedTransformer(AstBase ast)
        {
            var changed = false;
            foreach (var transformer in GetTransformer())
            {
                var @new = transformer(ast);
                if (@new is not null)
                {
                    changed = true;
                    ast = @new;
                }
            }
            return changed ? ast : null;
        }

        private IEnumerable<Func<AstBase, AstBase?>> GetTransformer()
        {
            yield return RemoveEmptyGroups;
            yield return RemoveDoubleNegation;
            yield return FlipBooleanLogic;
            // yield return MoveOptions;
            yield return MoveSingleCollections;
            yield return QuickFail;
            yield return QuickSucceed;
            yield return RemoveNeutralElement;
            yield return MergeSameGroups;
            yield return MergeAndOptions;
            yield return RemoveConflictingOptions;
            yield return MoveSortingsUp;
            yield return EliminateConflictingSortings;

            if (!ExtendedTransformation)
                yield break;
            
            yield return RemoveSortings;
        }

        private AstBase? RemoveEmptyGroups(AstBase ast)
        {
            if (ast is Group group)
            {
                if (group.Items.Count == 0 && group.Options.Count == 0)
                    return Fail.Default;
            }
            else if (ast is AstCollectionBase col)
            {
                if (col.Items.Count == 0)
                    return Fail.Default;
            }
            return null;
        }

        private AstBase? RemoveDoubleNegation(AstBase ast)
        {
            if (ast is Negation neg1 && neg1.Item is Negation neg2)
            {
                return neg2.Item;
            }
            return null;
        }

        private AstBase? FlipBooleanLogic(AstBase ast)
        {
            if (ast is Negation neg)
            {
                if (neg.Item is Succeed)
                    return Fail.Default;
                if (neg.Item is Fail)
                    return Succeed.Default;
            }
            return null;
        }
    
        private AstBase? MoveSingleCollections(AstBase ast)
        {
            if (ast is AstCollectionBase col && col.Items.Count == 1
                && (ast is not Group group || group.Options.Count == 0)
            )
                return col.Items[0];
            return null;
        }
    
        private AstBase? QuickFail(AstBase ast)
        {
            if (ast is AndGroup group && group.Items.Any(x => x is Fail))
                return Fail.Default;
            return null;
        }

        private AstBase? QuickSucceed(AstBase ast)
        {
            if (ast is OrGroup group && group.Items.Any(x => x is Succeed))
                return Succeed.Default;
            return null;
        }
    
        private AstBase? RemoveNeutralElement(AstBase ast)
        {
            bool changed = false;
            if (ast is Group group)
            {
                for (int i = 0; i < group.Items.Count; ++i)
                    if (group.Items[i] is Fail)
                    {
                        group.Items.RemoveAt(i);
                        i--;
                        changed = true;
                    }
            }
            if (ast is AndGroup and && and.CalcMaximum)
            {
                for (int i = 0; i < and.Items.Count; ++i)
                    if (and.Items[i] is Succeed)
                    {
                        and.Items.RemoveAt(i);
                        i--;
                        changed = true;
                    }
            }
            if (ast is OrGroup or)
            {
                for (int i = 0; i < or.Items.Count; ++i)
                    if (or.Items[i] is Fail)
                    {
                        or.Items.RemoveAt(i);
                        i--;
                        changed = true;
                    }
            }
            if (ast is XorGroup xor && xor.Items.Count >= 2)
            {
                for (int i = 0; i < xor.Items.Count; ++i)
                {
                    if (xor.Items[i] is Fail)
                    {
                        xor.Items.RemoveAt(i);
                        changed = true;
                        i--;
                    }
                    else if (xor.Items[i] is Succeed && xor.Items.Count >= 2)
                    {
                        if (i == 0)
                            xor.Items[1] = new Negation(xor.Items[1]);
                        else xor.Items[i - 1] = new Negation(xor.Items[i - 1]);
                        xor.Items.RemoveAt(i);
                    }
                }
            }
            return changed ? ast : null;
        }
    
        private AstBase? MergeSameGroups(AstBase ast)
        {
            bool changed = false;
            if (ast is Group group)
            {
                for (int i = 0; i < group.Items.Count; ++i)
                    if (group.Items[i] is Group other)
                    {
                        changed = true;
                        // special case: group is empty
                        if (other.Items.Count == 0 && other.Options.Count == 0)
                        {
                            group.Items[i] = Fail.Default;
                            continue;
                        }
                        // merge content
                        group.Options.AddRange(other.Options);
                        group.Items.RemoveAt(i);
                        group.Items.EnsureCapacity(other.Items.Count);
                        group.Items.AddRange(other.Items);
                        i--;
                    }
            }
            if (ast is AndGroup and)
            {
                for (int i = 0; i < and.Items.Count; ++i)
                    if (and.Items[i] is AndGroup other && and.CalcMaximum == other.CalcMaximum)
                    {
                        changed = true;
                        and.Items.RemoveAt(i);
                        and.Items.EnsureCapacity(other.Items.Count);
                        and.Items.AddRange(other.Items);
                        i--;
                    }
            }
            if (ast is OrGroup or)
            {
                for (int i = 0; i < or.Items.Count; ++i)
                    if (or.Items[i] is OrGroup other && or.CalcMaximum == other.CalcMaximum)
                    {
                        changed = true;
                        or.Items.RemoveAt(i);
                        or.Items.EnsureCapacity(other.Items.Count);
                        or.Items.AddRange(other.Items);
                        i--;
                    }
            }
            return changed ? ast : null;
        }
    
        private AstBase? RemoveConflictingOptions(AstBase ast)
        {
            if (ast is Group group)
            {
                // check for conflicting roles
                var roleOpts = group.Options
                    .Where(x => x is RoleOption)
                    .Cast<RoleOption>()
                    .Select(x => x.Role)
                    .Distinct()
                    .Count();
                if (roleOpts > 1)
                    return Fail.Default;
                // check for conflicting types
                var typeOpts = group.Options
                    .Where(x => x is TypeOption)
                    .Cast<TypeOption>()
                    .Select(x => x.Type)
                    .Distinct()
                    .Count();
                if (typeOpts > 1)
                    return Fail.Default;
                // check for type/role/attr conflict
                var selectedType = group.Options
                    .Where(x => x is TypeOption)
                    .Cast<TypeOption>()
                    .Select(x => (SearchResultType?)x.Type)
                    .FirstOrDefault();
                if (selectedType != null && selectedType.Value != SearchResultType.Person)
                {
                    var conflict = group.Options
                        .Any(x => x is AttributeOption || x is RoleOption);
                    if (conflict)
                        return Fail.Default;
                }
            }
            return null;
        }
    
        private AstBase? MergeAndOptions(AstBase ast)
        {
            if (ast is AndGroup and)
            {
                var canPerform = and.Items
                    .All(x => x is Group group && group.Options.Count > 0 && group.Items.Count == 0);
                if (!canPerform)
                    return null;
                var @new = new Group();
                foreach (var item in and.Items)
                    @new.Options.AddRange(((Group)item).Options);
                return @new;
            }
            return null;
        }

        private AstBase? MoveSortingsUp(AstBase ast)
        {
            if (ast is AndGroup and)
            {
                var sorts = new List<OptionBase>();
                foreach (var item in and.Items)
                {
                    if (item is Group group)
                    {
                        sorts.AddRange(group.Options.Where(x => x is SortOption));
                        group.Options.RemoveAll(x => x is SortOption);
                    }
                }
                if (sorts.Count > 0)
                {
                    var group = new Group();
                    group.Options.AddRange(sorts);
                    group.Items.Add(and);
                    return group;
                }
            }
            if (ast is OrGroup or)
            {
                var sorts = new List<OptionBase>();
                foreach (var item in or.Items)
                {
                    if (item is Group group)
                    {
                        if (sorts.Count == 0)
                            sorts.AddRange(group.Options.Where(x => x is SortOption));
                        group.Options.RemoveAll(x => x is SortOption);
                    }
                }
                if (sorts.Count > 0)
                {
                    var group = new Group();
                    group.Options.AddRange(sorts);
                    group.Items.Add(or);
                    return group;
                }
            }
            if (ast is XorGroup xor)
            {
                bool removed = false;
                foreach (var item in xor.Items)
                {
                    if (item is Group group)
                    {
                        removed |= group.Options.RemoveAll(x => x is SortOption) > 0;
                    }
                }
                if (removed)
                    return xor;
            }
            if (ast is Negation negation)
            {
                if (negation.Item is Group group)
                {
                    var sorts = new List<OptionBase>();
                    sorts.AddRange(group.Options
                        .Where(x => x is SortOption)
                        .Cast<SortOption>()
                        .Select(x =>
                            new SortOption(x.Target, x.Name,
                                x.Direction switch
                                {
                                    SortOption.SortDirection.Ascending => SortOption.SortDirection.Descending,
                                    SortOption.SortDirection.Descending => SortOption.SortDirection.Ascending,
                                    _ => x.Direction,
                                }
                            )
                        )
                    );
                    group.Options.RemoveAll(x => x is SortOption);
                    if (sorts.Count > 0)
                    {
                        var result = new Group();
                        result.Options.AddRange(sorts);
                        result.Items.Add(negation);
                        return result;
                    }
                }
            }
            return null;
        }
    
        private AstBase? EliminateConflictingSortings(AstBase ast)
        {
            if (ast is Group group && group.Options.Count > 0)
            {
                var changed = false;
                var sortings = new HashSet<int>();
                for (int i = 0; i < group.Options.Count; ++i)
                {
                    if (group.Options[i] is SortOption sort1)
                    {
                        var @break = false;
                        foreach (var id in sortings)
                        {
                            var sort2 = (SortOption)group.Options[id];
                            if (sort1.Equals(sort2))
                            {
                                group.Options.RemoveAt(i);
                                i--;
                                @break = true;
                                break;
                            }
                            if (sort1.IsConflicting(sort2))
                            {
                                group.Options.RemoveAt(i);
                                group.Options.RemoveAt(id);
                                sortings.Remove(id);
                                i -= 2;
                                @break = true;
                                break;
                            }
                        }
                        if (!@break)
                            sortings.Add(i);
                        else changed = true;
                    }
                }
                if (changed)
                    return ast;
            }
            return null;
        }
    
        private AstBase? RemoveSortings(AstBase ast)
        {
            if (ast is Group group)
            {
                var changed = group.Options.RemoveAll(x => x is SortOption);
                if (changed > 0)
                {
                    if (group.Options.Count == 0 && group.Items.Count == 0)
                        return Succeed.Default;
                    return group;
                }
                else return null;
            }
            return null;
        }
    }
}