using Microsoft.Data.Sqlite;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Hashing;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace Elwig.Helpers {
    public static partial class Extensions {

        [LibraryImport("msvcrt.dll")]
        [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
        private static unsafe partial int memcmp(void* b1, void* b2, long count);

        public static unsafe int CompareBuffers(char[] buffer1, int offset1, char[] buffer2, int offset2, int count) {
            fixed (char* b1 = buffer1, b2 = buffer2) {
                return memcmp(b1 + offset1, b2 + offset2, count);
            }
        }

        public static string? ReadUntil(this StreamReader reader, string delimiter) {
            return ReadUntil(reader, delimiter.ToCharArray());
        }

        public static string? ReadUntil(this StreamReader reader, char delimiter) {
            return ReadUntil(reader, [ delimiter ]);
        }

        public static string? ReadUntil(this StreamReader reader, char[] delimiter) {
            var buf = new char[512];
            int bufSize = 0, ret;
            while (!reader.EndOfStream && bufSize < buf.Length - 1) {
                if ((ret = reader.Read()) == -1)
                    return null;
                buf[bufSize++] = (char)ret;

                if (bufSize >= delimiter.Length && CompareBuffers(buf, bufSize - delimiter.Length, delimiter, 0, delimiter.Length) == 0)
                    return new string(buf, 0, bufSize);
            }
            return null;
        }

        public static Task<string?> ReadUntilAsync(this StreamReader reader, string delimiter) {
            return ReadUntilAsync(reader, delimiter.ToCharArray());
        }

        public static Task<string?> ReadUntilAsync(this StreamReader reader, char delimiter) {
            return ReadUntilAsync(reader, [ delimiter ]);
        }

        public static async Task<string?> ReadUntilAsync(this StreamReader reader, char[] delimiter) {
            var buf = new char[512];
            int bufSize = 0;
            var tmpBuf = new char[1];
            while (!reader.EndOfStream && bufSize < buf.Length - 1) {
                if ((await reader.ReadAsync(tmpBuf, 0, 1)) != 1)
                    return null;
                buf[bufSize++] = tmpBuf[0];

                if (bufSize >= delimiter.Length && CompareBuffers(buf, bufSize - delimiter.Length, delimiter, 0, delimiter.Length) == 0)
                    return new string(buf, 0, bufSize);
            }
            return null;
        }

        public static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long>? progress = null, CancellationToken cancellationToken = default) {
            ArgumentNullException.ThrowIfNull(source);
            if (!source.CanRead) throw new ArgumentException("Has to be readable", nameof(source));
            ArgumentNullException.ThrowIfNull(destination);
            if (!destination.CanWrite) throw new ArgumentException("Has to be writable", nameof(destination));
            ArgumentOutOfRangeException.ThrowIfNegative(bufferSize);

            var buffer = new byte[bufferSize];
            long totalBytesRead = 0;
            int bytesRead;
            while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) {
                await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
                totalBytesRead += bytesRead;
                progress?.Report(totalBytesRead);
            }
        }

        public static async Task DownloadAsync(this HttpClient client, string requestUri, Stream destination, IProgress<double>? progress = null, CancellationToken cancellationToken = default) {
            using var response = await client.GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
            response.EnsureSuccessStatusCode();
            var contentLength = response.Content.Headers.ContentLength;

            using var download = await response.Content.ReadAsStreamAsync(cancellationToken);
            if (progress == null || !contentLength.HasValue) {
                await download.CopyToAsync(destination, cancellationToken);
                return;
            }

            var relativeProgress = new Progress<long>(totalBytes => progress.Report((double)totalBytes / contentLength.Value));
            await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken);
            progress.Report(100.0);
        }

        public static async Task CheckIntegrity(this ZipArchive zip) {
            var crc = new Crc32();
            foreach (var entry in zip.Entries) {
                crc.Reset();
                using var stream = entry.Open();
                await crc.AppendAsync(stream);
                if (crc.GetCurrentHashAsUInt32() != entry.Crc32)
                    throw new InvalidDataException($"CRC-32 mismatch in '{entry.FullName}'");
            }
        }

        public static async Task ExecuteBatch(this SqliteConnection cnx, string sql) {
            using var cmd = cnx.CreateCommand();
            cmd.CommandText = sql;
            using var reader = await cmd.ExecuteReaderAsync();
            while (await reader.NextResultAsync()) ;
        }

        public static async Task ExecuteEmbeddedScript(this SqliteConnection cnx, Assembly asm, string name) {
            using var stream = asm.GetManifestResourceStream(name) ?? throw new FileNotFoundException("Unable to load embedded resource");
            using var reader = new StreamReader(stream);
            await ExecuteBatch(cnx, await reader.ReadToEndAsync());
        }

        public static async Task<object?> ExecuteScalar(this SqliteConnection cnx, string sql) {
            using var cmd = cnx.CreateCommand();
            cmd.CommandText = sql;
            return await cmd.ExecuteScalarAsync();
        }

        public static async Task<(string Table, long RowId, string Parent, long FkId)[]> ForeignKeyCheck(this SqliteConnection cnx) {
            using var cmd = cnx.CreateCommand();
            cmd.CommandText = "PRAGMA foreign_key_check";
            using var reader = await cmd.ExecuteReaderAsync();
            var list = new List<(string, long, string, long)>();
            while (await reader.ReadAsync()) {
                var table = reader.GetString(0);
                var rowid = reader.GetInt64(1);
                var parent = reader.GetString(2);
                var fkid = reader.GetInt64(3);
                list.Add((table, rowid, parent, fkid));
            }
            return [.. list];
        }
    }
}
