package Crypt::Perl::ECDSA::ECParameters;

=encoding utf-8

=head1 NAME

Crypt::Perl::ECDSA::ECParameters - Parse RFC 3279 explicit curves

=head1 DISCUSSION

This interface is undocumented for now.

=cut

use strict;
use warnings;

use Try::Tiny;

use Module::Load ();

use Crypt::Perl::BigInt ();
use Crypt::Perl::ECDSA::EncodedPoint ();
use Crypt::Perl::ECDSA::Utils ();
use Crypt::Perl::X ();

#NOTE: This needs never to use Crypt::Perl::ECDSA::DB
#so that extract_openssl_curves.pl will work.

use constant {
    OID_ecPublicKey => '1.2.840.10045.2.1',
    OID_prime_field => '1.2.840.10045.1.1',
    OID_characteristic_two_field => '1.2.840.10045.1.2',
};

use constant EXPORTABLE => qw( p a b n h gx gy );

#cf. RFC 3279
use constant ASN1_ECParameters => q<
    Trinomial ::= INTEGER

    Pentanomial ::= SEQUENCE {
        k1  INTEGER,
        k2  INTEGER,
        k3  INTEGER
    }

    FG_Basis_Parameters ::= CHOICE {
        gnBasis NULL,
        tpBasis Trinomial,
        ppBasis Pentanomial
    }

    Characteristic-two ::= SEQUENCE {
        m           INTEGER,
        basis       OBJECT IDENTIFIER,
        parameters  FG_Basis_Parameters
    }

    FG_Field_Parameters ::= CHOICE {
        prime-field         INTEGER,    -- p
        characteristic-two  Characteristic-two
    }

    FieldID ::= SEQUENCE {
        fieldType   OBJECT IDENTIFIER,
        parameters  FG_Field_Parameters
    }

    FieldElement ::= OCTET STRING

    Curve ::= SEQUENCE {
        a           FieldElement,
        b           FieldElement,
        seed        BIT STRING OPTIONAL
    }

    ECPoint ::= OCTET STRING

    ECPVer ::= INTEGER

    -- Look for this.
    ECParameters ::= SEQUENCE {
        version         ECPVer,     -- always 1
        fieldID         FieldID,
        curve           Curve,
        base            ECPoint,    -- generator
        order           INTEGER,    -- n

        -- ECDH needs it; ECDSA doesn’t (RFC 3279, p14)
        cofactor        INTEGER OPTIONAL  -- h
    }
>;

#This must return the same information as
#Crypt::Perl::ECDSA::EC::DB::get_curve_data_by_oid().
#
#It also expects the same structure that Convert::ASN1 parses,
#including array references for BIT STRINGs.
#
sub normalize {
    my ($parsed_or_der) = @_;

    my $params;
    if (ref $parsed_or_der) {
        $params = $parsed_or_der;
    }
    else {
        die Crypt::Perl::X::create('Generic', 'TODO');
    }

    my $field_type = $params->{'fieldID'}{'fieldType'};
    if ($field_type ne OID_prime_field() ) {
        if ($field_type eq OID_characteristic_two_field() ) {
            die Crypt::Perl::X::create('ECDSA::CharacteristicTwoUnsupported');
        }

        die Crypt::Perl::X::create('Generic', "Unknown field type OID: “$field_type”");
    }

    #“seed” isn’t necessary here for calculations (… right??)
    my %curve = (
        p => $params->{'fieldID'}{'parameters'}{'prime-field'},
        a => $params->{'curve'}{'a'},
        b => $params->{'curve'}{'b'},
        n => $params->{'order'},
        h => $params->{'cofactor'},
        seed => $params->{'curve'}{'seed'}[0],
    );

    my @ints_to_upgrade = qw( p n );
    if ( defined $curve{'h'} ) {
        push @ints_to_upgrade, 'h';
    }

    #Ensure that numbers like 0 and 1 are represented as BigInt, too.
    ref || ($_ = Crypt::Perl::BigInt->new($_)) for @curve{@ints_to_upgrade};

    $_ = Crypt::Perl::BigInt->from_bytes($_) for @curve{'a', 'b'};

    #We might receive the base point as compressed, uncompressed, or hybrid.
    #Support all of those formats.
    my $base = Crypt::Perl::ECDSA::EncodedPoint->new($params->{'base'})->get_uncompressed(\%curve);
    @curve{'gx', 'gy'} = Crypt::Perl::ECDSA::Utils::split_G_or_public( $base );

    my @strings_to_upgrade = qw( gx gy );
    if ( defined $curve{'seed'} ) {
        push @strings_to_upgrade, 'seed';
    }

    $_ = Crypt::Perl::BigInt->from_bytes($_) for @curve{@strings_to_upgrade};

    defined($curve{$_}) || delete($curve{$_}) for qw( h seed );

#----------------------------------------------------------------------
#    my $db_params;
#
#    try {
#        $db_params = Crypt::Perl::ECDSA::EC::DB::get_curve_name_by_data(\%curve);
#    }
#    catch {
#        if ( !try { $_->isa('Crypt::Perl::X::ECDSA::NoCurveForParameters') } ) {
#            local $@ = $_;
#            die;
#        }
#    };
#
#    #We only get here if there’s no cofactor
#    #or if the one given is correct.
#    if (!$curve{'h'} && !$db_params) {
#        die Crypt::Perl::X::create('Generic', 'This library currently requires a cofactor (“h”) for custom curves.');
#    }
#----------------------------------------------------------------------

#    if ( $params->{'curve'}{'seed'} ) {
#        $curve{'seed'} = Crypt::Perl::BigInt->from_bytes($params->{'curve'}{'seed'});
#
#        #Make sure that the given seed is either for an unknown curve
#        #or is correct for the given curve.
#
#
#        #_get_db_params_or_undef(\%curve);
#
#        #If it’s a known curve, verify that the seed matches.
#        #if ($db_params) {
#        #    my $seed_hex = unpack 'H*', $params->{'curve'}{'seed'};
#        #
#        #    if ($seed_hex ne $db_params->{'seed'}) {
#        #        Crypt::Perl::X::create('Generic', "Curve parameters match “$curve_name”, but the seed ($seed_hex) does not match expected value ($db_params->{'seed'})");
#        #    }
#        #}
#    }
#
#    if (grep { !defined $curve{$_} } 'h', 'seed') {
#        Module::Load::load('Crypt::Perl::ECDSA::EC::DB');
#
#        #TODO: Would it be worthwhile to support arbitrary curves that don’t
#        #give a cofactor? I’d need to figure out how to determine the
#        #cofactor from the other parameters.
#        my $curve_name = Crypt::Perl::ECDSA::EC::DB::get_curve_name_by_data(\%curve);
#        my $params_hr = Crypt::Perl::ECDSA::EC::DB::get_curve_data_by_name($curve_name);
#
#        @curve{'h', 'seed'} = @{$params}{'h', 'seed'};
#    }

    return \%curve;
}

#sub _get_db_params_or_undef {
#    my ($curve_hr) = @_;
#
#    my ($curve_name, $params_hr);
#    try {
#        $curve_name = Crypt::Perl::ECDSA::EC::DB::get_curve_name_by_data(\%curve);
#        $params_hr = Crypt::Perl::ECDSA::EC::DB::get_curve_data_by_name($curve_name);
#    }
#    catch {
#        if ( !try { $_->isa('Crypt::Perl::X::ECDSA::NoCurveForParameters') } ) {
#            local $@ = $_;
#            die;
#        }
#    };
#
#    return $params_hr;
#}

#----------------------------------------------------------------------

sub _asn1 {
    my ($class) = @_;

    return Crypt::Perl::ASN1->new()->prepare($class->ASN1_ECParameters())->find('ECParameters');
}

1;
