import 'dart:convert';
import 'dart:typed_data';

import 'package:pointycastle/asn1.dart';
import 'package:pointycastle/asn1/object_identifiers.dart';
import 'package:pointycastle/ecc/api.dart';

///
/// Utils class holding different methods to ease the handling of ANS1Objects and their byte representation.
///
class ASN1Utils {
  ///
  /// Calculates the start position of the value bytes for the given [encodedBytes].
  ///
  /// It will return 2 if the **length byte** is less than or equal 127 or the length calculate on the **length byte** value.
  /// This will throw a [RangeError] if the given [encodedBytes] has length < 2.
  ///
  static int calculateValueStartPosition(Uint8List encodedBytes) {
    // TODO tag length can be >1
    var length = encodedBytes[1];
    if (length <= 0x7F) {
      return 2;
    } else {
      return 2 + (length & 0x7F);
    }
  }

  ///
  /// Calculates the length of the **value bytes** for the given [encodedBytes].
  ///
  /// Will return **-1** if the length byte equals **0x80**. Throws an [ArgumentError] if the length could not be calculated for the given [encodedBytes].
  ///
  static int decodeLength(Uint8List encodedBytes) {
    var valueStartPosition = 2;
    var length = encodedBytes[1];
    if (length <= 0x7F) {
      return length;
    }
    if (length == 0x80) {
      return -1;
    }
    if (length > 127) {
      var length = encodedBytes[1] & 0x7F;

      var numLengthBytes = length;

      length = 0;
      for (var i = 0; i < numLengthBytes; i++) {
        length <<= 8;
        length |= encodedBytes[valueStartPosition++] & 0xFF;
      }
      return length;
    }
    throw ArgumentError('Could not calculate the length from the given bytes.');
  }

  ///
  /// Encode the given [length] to byte representation.
  ///
  static Uint8List encodeLength(int length, {bool longform = false}) {
    Uint8List e;
    if (length <= 127 && longform == false) {
      e = Uint8List(1);
      e[0] = length;
    } else {
      var x = Uint32List(1);
      x[0] = length;
      var y = Uint8List.view(x.buffer);
      // Skip null bytes
      var num = 3;
      while (y[num] == 0) {
        --num;
      }
      e = Uint8List(num + 2);
      e[0] = 0x80 + num + 1;
      for (var i = 1; i < e.length; ++i) {
        e[i] = y[num--];
      }
    }
    return e;
  }

  ///
  /// Checks if the given int [i] is constructed according to <https://www.bouncycastle.org/asn1_layman_93.txt> section 3.2.
  ///
  /// The Identifier octets (represented by the given [i]) is marked as constructed if bit 6 has the value **1**.
  ///
  /// Example with the IA5 String tag:
  ///
  /// 0x36 = 0 0 1 1 0 1 1 0
  ///
  /// 0x16 = 0 0 0 1 0 1 1 0
  /// ```
  /// ASN1Utils.isConstructed(0x36);  // true
  /// ASN1Utils.isConstructed(0x16);  // false
  /// ```
  ///
  ///
  static bool isConstructed(int i) {
    // Shift bits
    var newNum = i >> (6 - 1);
    // Check if bit is set to 1
    return (newNum & 1) == 1;
  }

  static bool isASN1Tag(int i) {
    return ASN1Tags.TAGS.contains(i);
  }

  ///
  /// Checks if the given [bytes] ends with 0x00, 0x00
  ///
  static bool hasIndefiniteLengthEnding(Uint8List bytes) {
    var last = bytes.elementAt(bytes.length - 1);
    var lastMinus1 = bytes.elementAt(bytes.length - 2);
    if (last == 0 && lastMinus1 == 0) {
      return true;
    }
    return false;
  }

  ///
  /// Calculates the indefinite length of the ASN1 object.
  /// Throws an [ArgumentError] if the end of content octets is not found.
  ///
  static int calculateIndefiniteLength(Uint8List bytes, int startPosition) {
    var currentPosition = startPosition;
    var indefiniteLengthObjects = 0;
    while (currentPosition < bytes.length - 1) {
      if (bytes[currentPosition] == 0x00 &&
          bytes[currentPosition + 1] == 0x00) {
        indefiniteLengthObjects--;
        if (indefiniteLengthObjects == 0) {
          return currentPosition - startPosition;
        }
        currentPosition += 2;
      } else {
        final nextLength =
            ASN1Utils.decodeLength(bytes.sublist(currentPosition));
        final valueStartPosition = ASN1Utils.calculateValueStartPosition(
            bytes.sublist(currentPosition));
        if (nextLength == 0) {
          throw ArgumentError('Invalid length of zero.');
        }
        if (valueStartPosition <= 0) {
          throw ArgumentError(
              'Invalid value start position: $valueStartPosition');
        }

        if (nextLength == -1) {
          indefiniteLengthObjects++;
          currentPosition += valueStartPosition;
        } else {
          currentPosition += valueStartPosition + nextLength;
        }
      }
    }

    throw ArgumentError('End of content octets not found');
  }

  static Uint8List getBytesFromPEMString(String pem,
      {bool checkHeader = true}) {
    var lines = LineSplitter.split(pem)
        .map((line) => line.trim())
        .where((line) => line.isNotEmpty)
        .toList();
    String base64;
    if (checkHeader) {
      if (lines.length < 2 ||
          !lines.first.startsWith('-----BEGIN') ||
          !lines.last.startsWith('-----END')) {
        throw ArgumentError('The given string does not have the correct '
            'begin/end markers expected in a PEM file.');
      }
      base64 = lines.sublist(1, lines.length - 1).join('');
    } else {
      base64 = lines.join('');
    }

    return Uint8List.fromList(base64Decode(base64));
  }

  static ECPrivateKey ecPrivateKeyFromDerBytes(Uint8List bytes,
      {bool pkcs8 = false}) {
    var asn1Parser = ASN1Parser(bytes);
    var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
    late String curveName;
    Uint8List x;
    if (pkcs8) {
      // Parse the PKCS8 format
      var innerSeq = topLevelSeq.elements!.elementAt(1) as ASN1Sequence;
      var b2 = innerSeq.elements!.elementAt(1) as ASN1ObjectIdentifier;
      var b2Data = b2.objectIdentifierAsString;
      var b2Curvedata = ObjectIdentifiers.getIdentifierByIdentifier(b2Data);
      if (b2Curvedata != null) {
        curveName = b2Curvedata['readableName'] as String;
      }

      var octetString = topLevelSeq.elements!.elementAt(2) as ASN1OctetString;
      asn1Parser = ASN1Parser(octetString.valueBytes);
      var octetStringSeq = asn1Parser.nextObject() as ASN1Sequence;
      var octetStringKeyData =
          octetStringSeq.elements!.elementAt(1) as ASN1OctetString;

      x = octetStringKeyData.valueBytes!;
    } else {
      // Parse the SEC1 format
      var privateKeyAsOctetString =
          topLevelSeq.elements!.elementAt(1) as ASN1OctetString;
      var choice = topLevelSeq.elements!.elementAt(2);
      var s = ASN1Sequence();
      var parser = ASN1Parser(choice.valueBytes);
      while (parser.hasNext()) {
        s.add(parser.nextObject());
      }
      var curveNameOi = s.elements!.elementAt(0) as ASN1ObjectIdentifier;
      var data = ObjectIdentifiers.getIdentifierByIdentifier(
          curveNameOi.objectIdentifierAsString);
      if (data != null) {
        curveName = data['readableName'] as String;
      }

      x = privateKeyAsOctetString.valueBytes!;
    }

    return ECPrivateKey(osp2i(x), ECDomainParameters(curveName));
  }

  static BigInt osp2i(Iterable<int> bytes, {Endian endian = Endian.big}) {
    var result = BigInt.from(0);
    if (endian == Endian.little) {
      bytes = bytes.toList().reversed;
    }

    for (var byte in bytes) {
      result = result << 8;
      result |= BigInt.from(byte);
    }

    return result;
  }
}
