using System;
using System.Collections.Generic;
using BrotliImpl.Utils;
using BrotliLib.Brotli.Components;
using BrotliLib.Brotli.Components.Data;
using BrotliLib.Brotli.Components.Header;
using BrotliLib.Brotli.Encode;
using BrotliLib.Brotli.Parameters;

namespace BrotliImpl.Encoders{
    /// <summary>
    /// Encodes bytes into a series of compressed meta-blocks. For each byte, it attempts to find the nearest and longest copy within the sliding window, or dictionary word.
    /// </summary>
    public abstract class EncodeGreedySearch : IBrotliEncoder{
        private protected abstract Copy? FindCopy(BrotliFileParameters parameters, in ArraySegment<byte> bytes, int start, int maxLength);

        // Implementations

        public sealed class OnlyBackReferences : EncodeGreedySearch{
            private readonly int minLength;

            public OnlyBackReferences(int minLength){
                this.minLength = Math.Max(minLength, InsertCopyLengths.MinCopyLength);
            }

            private protected override Copy? FindCopy(BrotliFileParameters parameters, in ArraySegment<byte> bytes, int start, int maxLength){
                int length = bytes.Count;

                if (start < InsertCopyLengths.MinCopyLength || start >= length - InsertCopyLengths.MinCopyLength || maxLength < InsertCopyLengths.MinCopyLength){
                    return null;
                }

                maxLength = Math.Min(maxLength, InsertCopyLengths.MaxCopyLength);
                int maxDistance = Math.Min(start, parameters.WindowSize.Bytes);

                for(int distance = 1; distance <= maxDistance; distance++){
                    int match = Match.DetermineLength(bytes, start, start - distance, Math.Min(maxLength, length - start));

                    if (match >= minLength){
                        return new Copy.BackReference(match, distance);
                    }
                }

                return null;
            }
        }

        public sealed class OnlyDictionary : EncodeGreedySearch{
            private readonly int minLength;

            public OnlyDictionary(int minLength){
                this.minLength = minLength;
            }

            private protected override Copy? FindCopy(BrotliFileParameters parameters, in ArraySegment<byte> bytes, int start, int maxLength){
                var entries = parameters.Dictionary.Index.Find(bytes.Slice(start), minLength, maxLength);

                if (entries.Count == 0){
                    return null;
                }

                var bestEntry = entries[0];
                int bestOutputLength = bestEntry.OutputLength;
                int bestPacked = bestEntry.Packed;

                for(int index = 1; index < entries.Count; index++){
                    var entry = entries[index];

                    if (entry.OutputLength > bestOutputLength || (entry.OutputLength == bestOutputLength && entry.Packed < bestPacked)){
                        bestEntry = entry;
                    }
                }

                return new Copy.Dictionary(bestEntry);
            }
        }

        public sealed class Mixed : EncodeGreedySearch{
            private readonly EncodeGreedySearch findBackReferences;
            private readonly EncodeGreedySearch findDictionary;

            public Mixed(int minCopyLength, int minDictionaryLength){
                this.findBackReferences = new OnlyBackReferences(minCopyLength);
                this.findDictionary = new OnlyDictionary(minDictionaryLength);
            }

            private protected override Copy? FindCopy(BrotliFileParameters parameters, in ArraySegment<byte> bytes, int start, int maxLength){
                Copy? found1 = findBackReferences.FindCopy(parameters, bytes, start, maxLength);
                Copy? found2 = findDictionary.FindCopy(parameters, bytes, start, maxLength);

                return (found1?.OutputLength ?? 0) >= (found2?.OutputLength ?? 0) ? found1 : found2;
            }
        }

        // Generation

        public (MetaBlock, BrotliEncodeInfo) Encode(BrotliEncodeInfo info){
            var bytes = info.Bytes;
            int length = bytes.Count;

            var builder = info.NewBuilder();
            var nextLiteralBatch = new List<Literal>();

            for(int index = 0; index < length;){
                var copy = FindCopy(info.FileParameters, in bytes, index, DataLength.MaxUncompressedBytes - nextLiteralBatch.Count);
                int mbSize;

                if (copy == null){
                    nextLiteralBatch.Add(new Literal(bytes[index]));
                    index++;

                    mbSize = nextLiteralBatch.Count;
                }
                else{
                    index += copy.AddCommand(builder, nextLiteralBatch);
                    nextLiteralBatch.Clear();
                    mbSize = builder.OutputSize;
                }

                if (mbSize == DataLength.MaxUncompressedBytes){
                    return builder.Build(info);
                }
            }

            if (nextLiteralBatch.Count > 0){
                builder.AddInsert(nextLiteralBatch);
            }

            return builder.Build(info);
        }
    }
}