import 'dart:typed_data';

import 'package:pointycastle/asn1/asn1_object.dart';
import 'package:pointycastle/asn1/asn1_tags.dart';
import 'package:pointycastle/asn1/asn1_utils.dart';
import 'package:pointycastle/asn1/primitives/asn1_bit_string.dart';
import 'package:pointycastle/asn1/primitives/asn1_bmp_string.dart';
import 'package:pointycastle/asn1/primitives/asn1_boolean.dart';
import 'package:pointycastle/asn1/primitives/asn1_generalized_time.dart';
import 'package:pointycastle/asn1/primitives/asn1_ia5_string.dart';
import 'package:pointycastle/asn1/primitives/asn1_integer.dart';
import 'package:pointycastle/asn1/primitives/asn1_null.dart';
import 'package:pointycastle/asn1/primitives/asn1_object_identifier.dart';
import 'package:pointycastle/asn1/primitives/asn1_octet_string.dart';
import 'package:pointycastle/asn1/primitives/asn1_printable_string.dart';
import 'package:pointycastle/asn1/primitives/asn1_sequence.dart';
import 'package:pointycastle/asn1/primitives/asn1_set.dart';
import 'package:pointycastle/asn1/primitives/asn1_teletext_string.dart';
import 'package:pointycastle/asn1/primitives/asn1_utc_time.dart';
import 'package:pointycastle/asn1/primitives/asn1_utf8_string.dart';
import 'package:pointycastle/asn1/unsupported_asn1_tag_exception.dart';

///
/// The ASN1Parser to parse bytes into ASN1 Objects
///
class ASN1Parser {
  ///
  /// The bytes to parse
  ///
  final Uint8List? bytes;

  ///
  /// The current position in the byte array.
  ///
  /// The inital value is 0.
  ///
  int _position = 0;

  ASN1Parser(this.bytes);

  ///
  /// Returns true if there is still an object to parse. Otherwise false.
  ///
  bool hasNext() {
    return _position < bytes!.length;
  }

  ///
  /// Parses the next object in the [bytes].
  ///
  ASN1Object nextObject() {
    // Get the current tag in the list bytes
    var tag = bytes![_position];

    // Get the length of the value bytes for the current object
    var length = ASN1Utils.decodeLength(bytes!.sublist(_position));

    var valueStartPosition =
        ASN1Utils.calculateValueStartPosition(bytes!.sublist(_position));

    var isIndefiniteLength = false;

    if (length == -1) {
      length = ASN1Utils.calculateIndefiniteLength(bytes!, _position) + 2;
      isIndefiniteLength = true;
    } else if (bytes!.length - _position > length + valueStartPosition) {
      length = length + valueStartPosition;
    } else {
      length = bytes!.length - _position;
    }

    // Create new view from the bytes
    var offset = _position + bytes!.offsetInBytes;
    var subBytes = Uint8List.view(bytes!.buffer, offset, length);

    // Parse the view and the tag to an ASN1Object
    var isConstructed = ASN1Utils.isConstructed(tag);
    var isPrimitive = (0xC0 & tag) == 0;
    //var isApplication = (0x40 & tag) != 0;

    ASN1Object obj;
    if (isConstructed) {
      obj = _createConstructed(tag, subBytes);
    } else if (isPrimitive) {
      obj = _createPrimitive(tag, subBytes);
    } else {
      // create a vanilla object
      obj = ASN1Object.fromBytes(subBytes);
    }

    // Update the position
    _position =
        _position + obj.totalEncodedByteLength + (isIndefiniteLength ? 2 : 0);
    return obj;
  }

  ///
  /// Creates a constructed ASN1Object depending on the given [tag] and [bytes]
  ///
  ASN1Object _createConstructed(int tag, Uint8List bytes) {
    switch (tag) {
      case ASN1Tags.SEQUENCE: // sequence
        return ASN1Sequence.fromBytes(bytes);
      case ASN1Tags.SET:
        return ASN1Set.fromBytes(bytes);
      case ASN1Tags.IA5_STRING_CONSTRUCTED:
        return ASN1IA5String.fromBytes(bytes);
      case ASN1Tags.BIT_STRING_CONSTRUCTED:
        return ASN1BitString.fromBytes(bytes);
      case ASN1Tags.OCTET_STRING_CONSTRUCTED:
        return ASN1OctetString.fromBytes(bytes);
      case ASN1Tags.PRINTABLE_STRING_CONSTRUCTED:
        return ASN1PrintableString.fromBytes(bytes);
      case ASN1Tags.T61_STRING_CONSTRUCTED:
        return ASN1TeletextString.fromBytes(bytes);
      case 0xA0:
      case 0xA1:
      case 0xA2:
      case 0xA3:
      case 0xA4:
        return ASN1Object.fromBytes(bytes);
      default:
        throw UnsupportedASN1TagException(tag);
    }
  }

  ///
  /// Creates a primitive ASN1Object depending on the given [tag] and [bytes]
  ///
  ASN1Object _createPrimitive(int tag, Uint8List bytes) {
    switch (tag) {
      case ASN1Tags.OCTET_STRING:
        return ASN1OctetString.fromBytes(bytes);
      case ASN1Tags.UTF8_STRING:
        return ASN1UTF8String.fromBytes(bytes);
      case ASN1Tags.IA5_STRING:
        return ASN1IA5String.fromBytes(bytes);
      case ASN1Tags.INTEGER:
      case ASN1Tags.ENUMERATED:
        return ASN1Integer.fromBytes(bytes);
      case ASN1Tags.BOOLEAN:
        return ASN1Boolean.fromBytes(bytes);
      case ASN1Tags.OBJECT_IDENTIFIER:
        return ASN1ObjectIdentifier.fromBytes(bytes);
      case ASN1Tags.BIT_STRING:
        return ASN1BitString.fromBytes(bytes);
      case ASN1Tags.NULL:
        return ASN1Null.fromBytes(bytes);
      case ASN1Tags.PRINTABLE_STRING:
        return ASN1PrintableString.fromBytes(bytes);
      case ASN1Tags.UTC_TIME:
        return ASN1UtcTime.fromBytes(bytes);
      case ASN1Tags.T61_STRING:
        return ASN1TeletextString.fromBytes(bytes);
      case ASN1Tags.GENERALIZED_TIME:
        return ASN1GeneralizedTime.fromBytes(bytes);
      case ASN1Tags.BMP_STRING:
        return ASN1BMPString.fromBytes(bytes);
      default:
        throw UnsupportedASN1TagException(tag);
    }
  }
}
