
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Serilog;

namespace GatewaySCO
{
    /// <summary>
    ///  Clase Utilitarios para operaciones sobre mensajes.
    /// </summary>
    public class Util
    {
        /// <summary>
        ///  Concatena dos arreglos de bytes y retorna arreglo con el resultado.
        /// </summary>
        public static byte[] ConcatenaArreglosBytes(byte[] array1, byte[] array2)
        {
            byte[] result = new byte[array1.Length + array2.Length];
            Buffer.BlockCopy(array1, 0, result, 0, array1.Length);
            Buffer.BlockCopy(array2, 0, result, array1.Length, array2.Length);
            return result;
        }

        /// <summary>
        ///  Convierte el string del parámetro en un arreglo de bytes, anteponiendo
        ///  al inicio del arreglo 4 bytes con la longitud del resto del arreglo, en los
        ///  primeros 4 bytes.
        /// </summary>
        public static byte[] ConvierteEnBufferBytes(string mensaje)
        {
            // Codifica longitud del mensaje en un entero sin signo en los 4 primeros bytes.
            int longitud = Convert.ToInt32(mensaje.Length);
            byte[] bytesConLongMensaje = BitConverter.GetBytes(longitud);
            // Bytes mas significativos deben ir primero, usa 'big-endian'.
            if (BitConverter.IsLittleEndian)
                Array.Reverse(bytesConLongMensaje);
            // Codifica en bytes texto del mensaje.
            byte[] bytesConMensaje = Encoding.UTF8.GetBytes(mensaje);
            // Copia los 2 arreglos de bytes en un arreglo unificado.
            byte[] buffer = new byte[bytesConLongMensaje.Length + bytesConMensaje.Length];
            Buffer.BlockCopy(bytesConLongMensaje, 0, buffer, 0, bytesConLongMensaje.Length);
            Buffer.BlockCopy(bytesConMensaje, 0, buffer, bytesConLongMensaje.Length, bytesConMensaje.Length);
            return buffer;
        }

        /// <summary>
        /// Longitud del mensaje, retorna entero representado en los 4 primeros bytes 
        /// del arreglo de bytes del parámetro, que corresponde a un mensaje codificado en bytes.
        /// </summary>
        public static int LongitudCodificada(byte[] bytesMensaje)
        {
            // Extrae longitud. Primeros 4 bytes del arreglo.
            byte[] bytesLongitudMensaje = new byte[4];
            Buffer.BlockCopy(bytesMensaje, 0, bytesLongitudMensaje, 0, 4);
            if (BitConverter.IsLittleEndian)
                Array.Reverse(bytesLongitudMensaje);
            int longitud = BitConverter.ToInt32(bytesLongitudMensaje, 0);
            return longitud;
        }

        /// <summary>
        /// Extrae componente de encabezado del mensaje. 
        /// Retorna cadena vacía si no encuentra un mensaje adecuadamente formado.
        /// </summary>
        public static string ExtraeEncabezado(string mensaje)
        {
            // Extrae encabezado. String con patron soeps~{texto>} 
            int inicioXML = mensaje.IndexOf('<');
            if (inicioXML == -1 || inicioXML == 0)
            {
                return "";
            }
            string parteEncabezado = mensaje[..inicioXML];
            return parteEncabezado;
        }

        /// <summary>
        /// Extrae componente de datos (cuerpo) del mensaje. 
        /// Asume que la cadena solo contiene un mensaje.
        /// Retorna cadena vacía si no encuentra un mensaje adecuadamente formado.
        /// </summary>
        public static string ExtraeCuerpo(string mensaje)
        {
            // Identifica el inicio del documento XML.
            int inicioXML = mensaje.IndexOf('<');
            if (inicioXML == -1 || inicioXML == 0)
            {
                return "";
            }
            string parteCuerpo = mensaje[inicioXML..];
            return parteCuerpo;
        }

        /// <summary>
        /// Interpreta el arreglo de bytes que codifica uno o mas mensajes del parámetro 
        /// asumiendo el formato CHEC/POSBC, es decir, por mensaje:
        ///     4 bytes con longitud del mensaje
        ///     encabezado
        ///     cuerpo del mensaje.
        /// Retorna un string con esta representación, separando cada mensaje y cada
        /// componente con un salto de línea.
        /// Usado para documentar los mensajes.
        /// </summary>
        public static string DetalleMensajes(byte[] buffer)
        {
            StringBuilder contenido = new();
            contenido.Append($"\tMensaje en buffer de {buffer.Length} bytes.");
            bool continua = true;
            int puntero = 3; // Se salta los primeros 4 bytes con la longitud.
            while (continua)
            {
                // Extrae longitud.
                int longitud = LongitudCodificada(buffer);
                contenido.Append($"\n\tlongitud: {longitud}");
                if (longitud + puntero > buffer.Length) 
                {
                    contenido.Append($"\n\tlongitud {longitud} codificada en mensaje supera longitud restante en buffer.");
                    continua = false;
                    continue;
                }
                string mensaje = Encoding.UTF8.GetString(buffer, puntero, longitud);
                // Notar que se asume que puede haber mas de un mensaje incluido en el buffer.
                // Extrae encabezado.
                string encabezado = ExtraeEncabezado(mensaje);
                contenido.Append($"\n\tencabezado:\n{encabezado}");
                // Extrae mensaje.
                string cuerpo = ExtraeCuerpo(mensaje);
                contenido.Append($"\n\tcuerpo:\n{cuerpo}");
                // Hay mas datos?.
                if (longitud + puntero < buffer.Length)
                {
                    // El buffer tiene mas datos de los que indica la longitud en su encabezado.
                    puntero += longitud + 4;
                } else {
                    continua = false;
                }
            }
            return contenido.ToString();
        }

        /// <summary>
        ///  Utilitario para convertir en string contenido XML de objeto XmlNode.
        /// </summary>
        public static string ContenidoXmlComoString(XmlNode docXml)
        {
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.OmitXmlDeclaration = true;
            settings.NewLineOnAttributes = true;
            using (var stringWriter = new StringWriter())
            using (var xmlTextWriter = XmlWriter.Create(stringWriter, settings))
            {
                docXml.WriteTo(xmlTextWriter);
                xmlTextWriter.Flush();
                return stringWriter.GetStringBuilder().ToString();
            }
        }


        /// <summary>
        ///  Lee valor int de entidad XML indicada por xpath..
        /// </summary>
        public static int LeeIntEnNodo(XmlNode nodo, string xpath)
        {
            int valor = 0;
            XmlNode? xnodo = nodo.SelectSingleNode(xpath);
            if (xnodo == null)
                throw new Exception($"Nodo <{nodo.Name}> no contiene a <{xpath}>.");
            string? xvalor = xnodo.InnerText;
            if (xvalor == null)
                throw new Exception($"Nodo <{xpath}> con contenido nulo.");
            try
            {
                valor = int.Parse(xvalor);
            }
            catch (Exception e)
            {
                throw new Exception($"Error '{e.Message}' convirtiendo valor <{xvalor}> de <{xpath}> en entero.");
            }
            return valor;
        }

        /// <summary>
        ///  Lee valor long de entidad XML indicada por xpath..
        /// </summary>
        public static long LeeLongEnNodo(XmlNode nodo, string xpath)
        {
            long valor = 0;
            XmlNode? xnodo = nodo.SelectSingleNode(xpath);
            if (xnodo == null)
                throw new Exception($"Nodo <{nodo.Name}> no contiene a <{xpath}>.");
            string? xvalor = xnodo.InnerText;
            if (xvalor == null)
                throw new Exception($"Nodo <{xpath}> con contenido nulo.");
            try
            {
                valor = long.Parse(xvalor);
            }
            catch (Exception e)
            {
                throw new Exception($"Error '{e.Message}' convirtiendo valor <{xvalor}> de <{xpath}> en entero.");
            }
            return valor;
        }


        /// <summary>
        ///  Lee valor bool de entidad XML indicada por xpath.
        /// </summary>
        public static bool LeeBoolEnNodo(XmlNode nodo, string xpath)
        {
            bool valor = false;
            XmlNode? xnodo = nodo.SelectSingleNode(xpath);
            if (xnodo == null)
                throw new Exception($"Nodo '{nodo.Name}' no contiene a '{xpath}'.");
            string? xvalor = xnodo.InnerText;
            if (xvalor == null)
                throw new Exception($"Nodo '{xpath}' con contenido nulo.");
            else if (xvalor.ToLower() == "false")
                valor = false;
            else if (xvalor.ToLower() == "true")
                valor = true;
            else
                throw new Exception($"Valor '{xvalor}' de '{xpath}' no se puede convertir a Boolean.");
            return valor;
        }

        /// <summary>
        ///  Lee valor string de entidad XML indicada por xpath.
        /// </summary>
        public static string LeeStringEnNodo(XmlNode nodo, string xpath)
        {
            XmlNode? xnode = nodo.SelectSingleNode(xpath);
            if (xnode == null)
                throw new Exception($"Nodo '{nodo.Name}' no contiene a '{xpath}'.");
            string? valor = xnode.InnerText;
            if (valor == null)
                throw new Exception($"Nodo '{xpath}' con contenido nulo.");
            return valor;
        }

        /// <summary>
        ///  Remplaza contenido entre caracteres <| y |> por valores pasados como parémtros, en orden.
        ///  Si el número de conenidos y de parámetros no corresponde, lanza excepción.
        ///  Ejemplo: "Texto con <|o1|> para remplazar, junto con <|o2|>", si o1=123 y o2=true
        ///  da como resultado: "Texto con 123 para remplazar, junto con true"
        /// </summary>
        public static string ReplaceValuesInString(string inputString, params object[] values)
        {
            string pattern = @"<\|([^|]+)\|>";
            Regex regex = new Regex(pattern);

            MatchEvaluator evaluator = (match) =>
            {
                string variableName = match.Groups[1].Value;
                int index = int.Parse(variableName.Substring(1));
                if (index >= 0 && index < values.Length)
                {
                    return Convert.ToString(values[index])
                                ?? throw new ArgumentException($"ReplaceValuesInString: values {values}, index {index}");
                }
                return match.Value;
            };

            string result = regex.Replace(inputString, evaluator);
            return result;
        }
    }
}