using System;
using System.IO;
namespace Unosquare.Swan.Networking.Ldap {
  /// 
  /// This class provides LBER encoding routines for ASN.1 Types. LBER is a
  /// subset of BER as described in the following taken from 5.1 of RFC 2251:
  /// 5.1. Mapping Onto BER-based Transport Services
  /// The protocol elements of Ldap are encoded for exchange using the
  /// Basic Encoding Rules (BER) [11] of ASN.1 [3]. However, due to the
  /// high overhead involved in using certain elements of the BER, the
  /// following additional restrictions are placed on BER-encodings of Ldap
  /// protocol elements:
  /// (1) Only the definite form of length encoding will be used.
  /// (2) OCTET STRING values will be encoded in the primitive form only.
  /// (3) If the value of a BOOLEAN type is true, the encoding MUST have
  /// its contents octets set to hex "FF".
  /// 
  /// (4) If a value of a type is its default value, it MUST be absent.
  /// Only some BOOLEAN and INTEGER types have default values in this
  /// protocol definition.
  /// These restrictions do not apply to ASN.1 types encapsulated inside of
  /// OCTET STRING values, such as attribute values, unless otherwise
  /// noted.
  /// 
  /// [3] ITU-T Rec. X.680, "Abstract Syntax Notation One (ASN.1) -
  /// Specification of Basic Notation", 1994.
  /// [11] ITU-T Rec. X.690, "Specification of ASN.1 encoding rules: Basic,
  /// Canonical, and Distinguished Encoding Rules", 1994.
  /// 
  internal static class LberEncoder {
    /// 
    /// BER Encode an Asn1Boolean directly into the specified output stream.
    /// 
    /// The Asn1Boolean object to encode.
    /// The stream.
    public static void Encode(Asn1Boolean b, Stream stream) {
      Encode(b.GetIdentifier(), stream);
      stream.WriteByte(0x01);
      stream.WriteByte((Byte)(b.BooleanValue() ? 0xff : 0x00));
    }
    /// 
    /// Encode an Asn1Numeric directly into the specified outputstream.
    /// Use a two's complement representation in the fewest number of octets
    /// possible.
    /// Can be used to encode INTEGER and ENUMERATED values.
    /// 
    /// The Asn1Numeric object to encode.
    /// The stream.
    public static void Encode(Asn1Numeric n, Stream stream) {
      SByte[] octets = new SByte[8];
      SByte len;
      Int64 longValue = n.LongValue();
      Int64 endValue = longValue < 0 ? -1 : 0;
      Int64 endSign = endValue & 0x80;
      for(len = 0; len == 0 || longValue != endValue || (octets[len - 1] & 0x80) != endSign; len++) {
        octets[len] = (SByte)(longValue & 0xFF);
        longValue >>= 8;
      }
      Encode(n.GetIdentifier(), stream);
      stream.WriteByte((Byte)len);
      for(Int32 i = len - 1; i >= 0; i--) {
        stream.WriteByte((Byte)octets[i]);
      }
    }
    /// 
    /// Encode an Asn1OctetString directly into the specified outputstream.
    /// 
    /// The Asn1OctetString object to encode.
    /// The stream.
    public static void Encode(Asn1OctetString os, Stream stream) {
      Encode(os.GetIdentifier(), stream);
      EncodeLength(os.ByteValue().Length, stream);
      SByte[] tempSbyteArray = os.ByteValue();
      stream.Write(tempSbyteArray.ToByteArray(), 0, tempSbyteArray.Length);
    }
    public static void Encode(Asn1Object obj, Stream stream) {
      switch(obj) {
        case Asn1Boolean b:
          Encode(b, stream);
          break;
        case Asn1Numeric n:
          Encode(n, stream);
          break;
        case Asn1Null n:
          Encode(n.GetIdentifier(), stream);
          stream.WriteByte(0x00); // Length (with no Content)
          break;
        case Asn1OctetString n:
          Encode(n, stream);
          break;
        case Asn1Structured n:
          Encode(n, stream);
          break;
        case Asn1Tagged n:
          Encode(n, stream);
          break;
        case Asn1Choice n:
          Encode(n.ChoiceValue, stream);
          break;
        default:
          throw new InvalidDataException();
      }
    }
    /// 
    /// Encode an Asn1Structured into the specified outputstream.  This method
    /// can be used to encode SET, SET_OF, SEQUENCE, SEQUENCE_OF.
    /// 
    /// The Asn1Structured object to encode.
    /// The stream.
    public static void Encode(Asn1Structured c, Stream stream) {
      Encode(c.GetIdentifier(), stream);
      Asn1Object[] arrayValue = c.ToArray();
      using(MemoryStream output = new MemoryStream()) {
        foreach(Asn1Object obj in arrayValue) {
          Encode(obj, output);
        }
        EncodeLength((Int32)output.Length, stream);
        Byte[] tempSbyteArray = output.ToArray();
        stream.Write(tempSbyteArray, 0, tempSbyteArray.Length);
      }
    }
    /// 
    /// Encode an Asn1Tagged directly into the specified outputstream.
    /// 
    /// The Asn1Tagged object to encode.
    /// The stream.
    public static void Encode(Asn1Tagged t, Stream stream) {
      if(!t.Explicit) {
        Encode(t.TaggedValue, stream);
        return;
      }
      Encode(t.GetIdentifier(), stream);
      // determine the encoded length of the base type.
      using(MemoryStream encodedContent = new MemoryStream()) {
        Encode(t.TaggedValue, encodedContent);
        EncodeLength((Int32)encodedContent.Length, stream);
        SByte[] tempSbyteArray = encodedContent.ToArray().ToSByteArray();
        stream.Write(tempSbyteArray.ToByteArray(), 0, tempSbyteArray.Length);
      }
    }
    /// 
    /// Encode an Asn1Identifier directly into the specified outputstream.
    /// 
    /// The Asn1Identifier object to encode.
    /// The stream.
    public static void Encode(Asn1Identifier id, Stream stream) {
      Int32 c = (Int32)id.Asn1Class;
      Int32 t = id.Tag;
      SByte ccf = (SByte)((c << 6) | (id.Constructed ? 0x20 : 0));
      if(t < 30) {
#pragma warning disable CS0675 // Bitweiser OR-Operator, der bei einem signaturerweiterten Operanden verwendet wurde.
        stream.WriteByte((Byte)(ccf | t));
#pragma warning restore CS0675 // Bitweiser OR-Operator, der bei einem signaturerweiterten Operanden verwendet wurde.
      } else {
        stream.WriteByte((Byte)(ccf | 0x1F));
        EncodeTagInteger(t, stream);
      }
    }
    /// 
    /// Encodes the length.
    /// 
    /// The length.
    /// The stream.
    private static void EncodeLength(Int32 length, Stream stream) {
      if(length < 0x80) {
        stream.WriteByte((Byte)length);
      } else {
        SByte[] octets = new SByte[4]; // 4 bytes sufficient for 32 bit int.
        SByte n;
        for(n = 0; length != 0; n++) {
          octets[n] = (SByte)(length & 0xFF);
          length >>= 8;
        }
        stream.WriteByte((Byte)(0x80 | n));
        for(Int32 i = n - 1; i >= 0; i--) {
          stream.WriteByte((Byte)octets[i]);
        }
      }
    }
    /// 
    /// Encodes the provided tag into the stream.
    /// 
    /// The value.
    /// The stream.
    private static void EncodeTagInteger(Int32 val, Stream stream) {
      SByte[] octets = new SByte[5];
      Int32 n;
      for(n = 0; val != 0; n++) {
        octets[n] = (SByte)(val & 0x7F);
        val >>= 7;
      }
      for(Int32 i = n - 1; i > 0; i--) {
        stream.WriteByte((Byte)(octets[i] | 0x80));
      }
      stream.WriteByte((Byte)octets[0]);
    }
  }
}