/*
 * Copyright (C) 2007-2024 C# Port
 * Original Copyright (C) 2007-2018 Siemens AG
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 */

using V2GDecoderNet.EXI;
using System.Text;
using System.Xml;

namespace V2GDecoderNet.V2G
{
    /// <summary>
    /// EXI Decoder for converting EXI binary data to XML
    /// </summary>
    public class EXIDecoder
    {
        private readonly EXIConfig _config;

        public EXIDecoder(EXIConfig? config = null)
        {
            _config = config ?? new EXIConfig();
        }

        /// <summary>
        /// Decode EXI binary data to XML string
        /// </summary>
        /// <param name="exiData">EXI binary data</param>
        /// <returns>XML string representation</returns>
        public string DecodeToXml(byte[] exiData)
        {
            if (exiData == null || exiData.Length == 0)
                throw new ArgumentException("EXI data cannot be null or empty", nameof(exiData));

            var inputStream = new BitInputStream(exiData);
            var xmlBuilder = new StringBuilder();
            
            try
            {
                DecodeDocument(inputStream, xmlBuilder);
                return xmlBuilder.ToString();
            }
            catch (EXIException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new EXIException(EXIErrorCodes.EXI_ERROR_UNKOWN_EVENT, 
                    "Error during EXI decoding", ex);
            }
        }

        /// <summary>
        /// Decode EXI binary data to XmlDocument
        /// </summary>
        /// <param name="exiData">EXI binary data</param>
        /// <returns>XmlDocument</returns>
        public XmlDocument DecodeToXmlDocument(byte[] exiData)
        {
            string xmlString = DecodeToXml(exiData);
            var xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(xmlString);
            return xmlDoc;
        }

        /// <summary>
        /// Validate EXI header and extract options
        /// </summary>
        /// <param name="inputStream">Input bit stream</param>
        /// <returns>EXI header information</returns>
        public EXIHeader DecodeHeader(BitInputStream inputStream)
        {
            var header = new EXIHeader();
            
            // Check for EXI cookie ($EXI)
            byte[] cookie = inputStream.ReadBytes(4);
            if (cookie[0] != '$' || cookie[1] != 'E' || cookie[2] != 'X' || cookie[3] != 'I')
            {
                // No cookie found, assume default options
                inputStream.SetPosition(0);
                header.HasCookie = false;
                return header;
            }

            header.HasCookie = true;

            // Read format version
            header.FormatVersion = inputStream.ReadBits(4);
            
            // Read options presence flag
            bool hasOptions = inputStream.ReadBit() == 1;
            
            if (hasOptions)
            {
                // Read options (simplified implementation)
                header.PreserveComments = inputStream.ReadBit() == 1;
                header.PreservePIs = inputStream.ReadBit() == 1;
                header.PreserveDTD = inputStream.ReadBit() == 1;
                header.PreservePrefixes = inputStream.ReadBit() == 1;
                
                // Skip remaining option bits for now
                inputStream.AlignToByteBank();
            }

            return header;
        }

        private void DecodeDocument(BitInputStream inputStream, StringBuilder xmlBuilder)
        {
            // Decode EXI header
            var header = DecodeHeader(inputStream);
            
            // Start XML document
            xmlBuilder.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            
            // Decode document content
            DecodeDocumentContent(inputStream, xmlBuilder);
        }

        private void DecodeDocumentContent(BitInputStream inputStream, StringBuilder xmlBuilder)
        {
            var elementStack = new Stack<string>();
            bool documentStarted = false;
            
            while (!inputStream.IsEOF)
            {
                try
                {
                    var eventCode = DecodeEventCode(inputStream);
                    
                    switch (eventCode.Event)
                    {
                        case EXIEvent.START_DOCUMENT:
                            documentStarted = true;
                            break;
                            
                        case EXIEvent.END_DOCUMENT:
                            return;
                            
                        case EXIEvent.START_ELEMENT:
                        case EXIEvent.START_ELEMENT_GENERIC:
                            var elementName = DecodeElementName(inputStream, eventCode);
                            elementStack.Push(elementName);
                            xmlBuilder.Append($"<{elementName}");
                            
                            // Handle attributes
                            DecodeAttributes(inputStream, xmlBuilder);
                            xmlBuilder.AppendLine(">");
                            break;
                            
                        case EXIEvent.END_ELEMENT:
                            if (elementStack.Count > 0)
                            {
                                var endElementName = elementStack.Pop();
                                xmlBuilder.AppendLine($"</{endElementName}>");
                            }
                            break;
                            
                        case EXIEvent.CHARACTERS:
                            var text = DecodeCharacters(inputStream);
                            xmlBuilder.Append(XmlEscape(text));
                            break;
                            
                        default:
                            // Skip unsupported events
                            break;
                    }
                }
                catch (EXIException ex) when (ex.ErrorCode == EXIErrorCodes.EXI_ERROR_INPUT_STREAM_EOF)
                {
                    break;
                }
            }
        }

        private EventCode DecodeEventCode(BitInputStream inputStream)
        {
            // Simplified event code decoding - in real implementation,
            // this would be based on current grammar state
            var code = inputStream.ReadBits(2);
            
            return new EventCode
            {
                Event = code switch
                {
                    0 => EXIEvent.START_ELEMENT,
                    1 => EXIEvent.END_ELEMENT,
                    2 => EXIEvent.CHARACTERS,
                    3 => EXIEvent.END_DOCUMENT,
                    _ => EXIEvent.START_ELEMENT
                },
                Code = code
            };
        }

        private string DecodeElementName(BitInputStream inputStream, EventCode eventCode)
        {
            // Simplified element name decoding
            var nameIndex = inputStream.ReadUnsignedInteger();
            
            // In a real implementation, this would lookup from string tables
            return $"Element{nameIndex}";
        }

        private void DecodeAttributes(BitInputStream inputStream, StringBuilder xmlBuilder)
        {
            // Simplified attribute handling
            // In real implementation, would continue reading attributes until
            // a non-attribute event code is encountered
        }

        private string DecodeCharacters(BitInputStream inputStream)
        {
            // Decode character data
            var length = (int)inputStream.ReadUnsignedInteger();
            var charData = inputStream.ReadBytes(length);
            
            return _config.Strings switch
            {
                EXIConfig.StringRepresentation.ASCII => Encoding.ASCII.GetString(charData),
                EXIConfig.StringRepresentation.UCS => Encoding.UTF8.GetString(charData),
                _ => Encoding.UTF8.GetString(charData)
            };
        }

        private static string XmlEscape(string text)
        {
            return text
                .Replace("&", "&amp;")
                .Replace("<", "&lt;")
                .Replace(">", "&gt;")
                .Replace("\"", "&quot;")
                .Replace("'", "&apos;");
        }
    }

    /// <summary>
    /// EXI Header information
    /// </summary>
    public class EXIHeader
    {
        public bool HasCookie { get; set; }
        public uint FormatVersion { get; set; }
        public bool PreserveComments { get; set; }
        public bool PreservePIs { get; set; }
        public bool PreserveDTD { get; set; }
        public bool PreservePrefixes { get; set; }
    }

    /// <summary>
    /// EXI Event Code
    /// </summary>
    public class EventCode
    {
        public EXIEvent Event { get; set; }
        public uint Code { get; set; }
    }
}