/*
 * 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.Xml;

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

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

        /// <summary>
        /// Encode XML string to EXI binary data
        /// </summary>
        /// <param name="xmlString">XML string to encode</param>
        /// <returns>EXI binary data</returns>
        public byte[] EncodeFromXml(string xmlString)
        {
            if (string.IsNullOrEmpty(xmlString))
                throw new ArgumentException("XML string cannot be null or empty", nameof(xmlString));

            var xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(xmlString);
            
            return EncodeFromXmlDocument(xmlDoc);
        }

        /// <summary>
        /// Encode XmlDocument to EXI binary data
        /// </summary>
        /// <param name="xmlDoc">XmlDocument to encode</param>
        /// <returns>EXI binary data</returns>
        public byte[] EncodeFromXmlDocument(XmlDocument xmlDoc)
        {
            if (xmlDoc == null)
                throw new ArgumentNullException(nameof(xmlDoc));

            var outputStream = new BitOutputStream();
            
            try
            {
                // Write EXI header
                WriteHeader(outputStream);
                
                // Encode document
                EncodeDocument(xmlDoc, outputStream);
                
                return outputStream.ToArray();
            }
            catch (EXIException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new EXIException(EXIErrorCodes.EXI_ERROR_UNKOWN_EVENT, 
                    "Error during EXI encoding", ex);
            }
        }

        /// <summary>
        /// Write EXI header with options
        /// </summary>
        /// <param name="outputStream">Output bit stream</param>
        private void WriteHeader(BitOutputStream outputStream)
        {
            // Write EXI cookie ($EXI)
            outputStream.WriteBytes(new byte[] { (byte)'$', (byte)'E', (byte)'X', (byte)'I' });
            
            // Format version (4 bits) - currently 0
            outputStream.WriteBits(0, 4);
            
            // Options presence flag (1 bit) - false for simplicity
            outputStream.WriteBit(0);
            
            // Align to byte boundary
            outputStream.AlignToByteBank();
        }

        /// <summary>
        /// Encode XML document content
        /// </summary>
        /// <param name="xmlDoc">XML document</param>
        /// <param name="outputStream">Output bit stream</param>
        private void EncodeDocument(XmlDocument xmlDoc, BitOutputStream outputStream)
        {
            // Write START_DOCUMENT event
            WriteEventCode(outputStream, EXIEvent.START_DOCUMENT);
            
            // Encode root element and its children
            if (xmlDoc.DocumentElement != null)
            {
                EncodeElement(xmlDoc.DocumentElement, outputStream);
            }
            
            // Write END_DOCUMENT event
            WriteEventCode(outputStream, EXIEvent.END_DOCUMENT);
        }

        /// <summary>
        /// Encode XML element
        /// </summary>
        /// <param name="element">XML element</param>
        /// <param name="outputStream">Output bit stream</param>
        private void EncodeElement(XmlElement element, BitOutputStream outputStream)
        {
            // Write START_ELEMENT event
            WriteEventCode(outputStream, EXIEvent.START_ELEMENT);
            
            // Write element name (simplified - in real implementation would use string tables)
            WriteElementName(outputStream, element.Name);
            
            // Encode attributes
            EncodeAttributes(element, outputStream);
            
            // Encode child nodes
            foreach (XmlNode child in element.ChildNodes)
            {
                switch (child.NodeType)
                {
                    case XmlNodeType.Element:
                        EncodeElement((XmlElement)child, outputStream);
                        break;
                        
                    case XmlNodeType.Text:
                    case XmlNodeType.CDATA:
                        EncodeTextContent(child.Value ?? string.Empty, outputStream);
                        break;
                        
                    case XmlNodeType.Comment:
                        if (_config != null) // Preserve comments if configured
                        {
                            // Skip for simplicity
                        }
                        break;
                }
            }
            
            // Write END_ELEMENT event
            WriteEventCode(outputStream, EXIEvent.END_ELEMENT);
        }

        /// <summary>
        /// Encode element attributes
        /// </summary>
        /// <param name="element">XML element</param>
        /// <param name="outputStream">Output bit stream</param>
        private void EncodeAttributes(XmlElement element, BitOutputStream outputStream)
        {
            foreach (XmlAttribute attr in element.Attributes)
            {
                // Write ATTRIBUTE event
                WriteEventCode(outputStream, EXIEvent.ATTRIBUTE);
                
                // Write attribute name and value (simplified)
                WriteAttributeName(outputStream, attr.Name);
                WriteAttributeValue(outputStream, attr.Value);
            }
        }

        /// <summary>
        /// Encode text content
        /// </summary>
        /// <param name="text">Text content</param>
        /// <param name="outputStream">Output bit stream</param>
        private void EncodeTextContent(string text, BitOutputStream outputStream)
        {
            if (!string.IsNullOrEmpty(text))
            {
                // Write CHARACTERS event
                WriteEventCode(outputStream, EXIEvent.CHARACTERS);
                
                // Write text content
                WriteCharacters(outputStream, text);
            }
        }

        /// <summary>
        /// Write event code to stream
        /// </summary>
        /// <param name="outputStream">Output bit stream</param>
        /// <param name="eventType">Event type</param>
        private void WriteEventCode(BitOutputStream outputStream, EXIEvent eventType)
        {
            // Simplified event code writing - in real implementation,
            // this would be based on current grammar state
            uint code = eventType switch
            {
                EXIEvent.START_DOCUMENT => 0,
                EXIEvent.START_ELEMENT => 0,
                EXIEvent.END_ELEMENT => 1,
                EXIEvent.CHARACTERS => 2,
                EXIEvent.ATTRIBUTE => 3,
                EXIEvent.END_DOCUMENT => 3,
                _ => 0
            };
            
            outputStream.WriteBits(code, 2);
        }

        /// <summary>
        /// Write element name to stream
        /// </summary>
        /// <param name="outputStream">Output bit stream</param>
        /// <param name="name">Element name</param>
        private void WriteElementName(BitOutputStream outputStream, string name)
        {
            // Simplified name encoding - in real implementation would use string tables
            var nameBytes = System.Text.Encoding.UTF8.GetBytes(name);
            outputStream.WriteUnsignedInteger((uint)nameBytes.Length);
            outputStream.WriteBytes(nameBytes);
        }

        /// <summary>
        /// Write attribute name to stream
        /// </summary>
        /// <param name="outputStream">Output bit stream</param>
        /// <param name="name">Attribute name</param>
        private void WriteAttributeName(BitOutputStream outputStream, string name)
        {
            // Simplified attribute name encoding
            var nameBytes = System.Text.Encoding.UTF8.GetBytes(name);
            outputStream.WriteUnsignedInteger((uint)nameBytes.Length);
            outputStream.WriteBytes(nameBytes);
        }

        /// <summary>
        /// Write attribute value to stream
        /// </summary>
        /// <param name="outputStream">Output bit stream</param>
        /// <param name="value">Attribute value</param>
        private void WriteAttributeValue(BitOutputStream outputStream, string value)
        {
            // Simplified attribute value encoding
            var valueBytes = System.Text.Encoding.UTF8.GetBytes(value ?? string.Empty);
            outputStream.WriteUnsignedInteger((uint)valueBytes.Length);
            outputStream.WriteBytes(valueBytes);
        }

        /// <summary>
        /// Write character data to stream
        /// </summary>
        /// <param name="outputStream">Output bit stream</param>
        /// <param name="text">Character data</param>
        private void WriteCharacters(BitOutputStream outputStream, string text)
        {
            var encoding = _config.Strings switch
            {
                EXIConfig.StringRepresentation.ASCII => System.Text.Encoding.ASCII,
                EXIConfig.StringRepresentation.UCS => System.Text.Encoding.UTF8,
                _ => System.Text.Encoding.UTF8
            };
            
            var textBytes = encoding.GetBytes(text);
            outputStream.WriteUnsignedInteger((uint)textBytes.Length);
            outputStream.WriteBytes(textBytes);
        }
    }
}