/*
 * Copyright (C) 2017 Schürmann & Breitmoser GbR
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.sufficientlysecure.keychain.securitytoken;

import java.io.IOException;
import java.nio.ByteBuffer;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.google.auto.value.AutoValue;


/**
 * References:
 * [0] `Functional Specification of the OpenPGP application on ISO Smart Card Operating Systems`
 *      version 3.4.1
 *      https://gnupg.org/ftp/specs/OpenPGP-smart-card-application-3.4.1.pdf
 */
@SuppressWarnings("unused") // just expose all included data
@AutoValue
public abstract class OpenPgpCapabilities {
    // Extended Capabilites flag bit offsets are defined on page 32 of [0]
    private final static int MASK_SM = 1 << 7;
    private final static int MASK_KEY_IMPORT = 1 << 5;
    private final static int MASK_ATTRIBUTES_CHANGABLE = 1 << 2;
    private final static int MASK_KDF_DO = 1;

    private static final int MAX_PW1_LENGTH_INDEX = 1;
    private static final int MAX_PW3_LENGTH_INDEX = 3;

    @SuppressWarnings("mutable")
    public abstract byte[] getAid();
    @SuppressWarnings("mutable")
    abstract byte[] getHistoricalBytes();

    @Nullable
    @SuppressWarnings("mutable")
    public abstract byte[] getFingerprintSign();
    @Nullable
    @SuppressWarnings("mutable")
    public abstract byte[] getFingerprintEncrypt();
    @Nullable
    @SuppressWarnings("mutable")
    public abstract byte[] getFingerprintAuth();
    @SuppressWarnings("mutable")
    public abstract byte[] getPwStatusBytes();

    public abstract KeyFormat getSignKeyFormat();
    public abstract KeyFormat getEncryptKeyFormat();
    public abstract KeyFormat getAuthKeyFormat();

    abstract boolean isHasKeyImport();
    public abstract boolean isAttributesChangable();

    abstract boolean isHasSM();
    abstract boolean isHasAesSm();
    abstract boolean isHasScp11bSm();
    abstract boolean isHasKdf();

    @Nullable
    abstract Integer getMaxCmdLen();
    @Nullable
    abstract Integer getMaxRspLen();

    public static OpenPgpCapabilities fromBytes(byte[] rawOpenPgpCapabilities) throws IOException {
        Iso7816TLV[] parsedTlvData = Iso7816TLV.readList(rawOpenPgpCapabilities, true);
        return new AutoValue_OpenPgpCapabilities.Builder().updateWithTLV(parsedTlvData).build();
    }

    public KeyFormat getFormatForKeyType(@NonNull KeyType keyType) {
        switch (keyType) {
            case SIGN: return getSignKeyFormat();
            case ENCRYPT: return getEncryptKeyFormat();
            case AUTH: return getAuthKeyFormat();
        }
        return null;
    }

    @Nullable
    public byte[] getKeyFingerprint(@NonNull KeyType keyType) {
        switch (keyType) {
            case SIGN: return getFingerprintSign();
            case ENCRYPT: return getFingerprintEncrypt();
            case AUTH: return getFingerprintAuth();
        }
        return null;
    }

    boolean isPw1ValidForMultipleSignatures() {
        return getPwStatusBytes()[0] == 1;
    }

    public int getPw1MaxLength() {
        return getPwStatusBytes()[MAX_PW1_LENGTH_INDEX] & 0xFF;
    }

    public int getPw3MaxLength() {
        return getPwStatusBytes()[MAX_PW3_LENGTH_INDEX] & 0xFF;
    }

    public int getPw1TriesLeft() {
        return getPwStatusBytes()[4];
    }

    public int getPw3TriesLeft() {
        return getPwStatusBytes()[6];
    }

    @AutoValue.Builder
    @SuppressWarnings("UnusedReturnValue")
    abstract static class Builder {
        abstract Builder aid(byte[] mV);
        abstract Builder historicalBytes(byte[] historicalBytes);

        abstract Builder fingerprintSign(byte[] fingerprint);
        abstract Builder fingerprintEncrypt(byte[] fingerprint);
        abstract Builder fingerprintAuth(byte[] fingerprint);

        abstract Builder pwStatusBytes(byte[] mV);
        abstract Builder authKeyFormat(KeyFormat keyFormat);
        abstract Builder encryptKeyFormat(KeyFormat keyFormat);
        abstract Builder signKeyFormat(KeyFormat keyFormat);


        abstract Builder hasKeyImport(boolean hasKeyImport);
        abstract Builder attributesChangable(boolean attributesChangable);

        abstract Builder hasSM(boolean hasSm);
        abstract Builder hasAesSm(boolean hasAesSm);
        abstract Builder hasScp11bSm(boolean hasScp11bSm);
        abstract Builder hasKdf(boolean hasKdf);

        abstract Builder maxCmdLen(Integer maxCommandLen);
        abstract Builder maxRspLen(Integer MaxResponseLen);

        abstract OpenPgpCapabilities build();

        public Builder() {
            hasKeyImport(false);
            attributesChangable(false);
            hasSM(false);
            hasAesSm(false);
            hasScp11bSm(false);
            hasKdf(false);
        }

        Builder updateWithTLV(Iso7816TLV[] tlvs) {
            if (tlvs.length == 1 && tlvs[0].mT == 0x6E) {
                tlvs = ((Iso7816TLV.Iso7816CompositeTLV) tlvs[0]).mSubs;
            }

            for (Iso7816TLV tlv : tlvs) {
                switch (tlv.mT) {
                    case 0x4F:
                        aid(tlv.mV);
                        break;
                    case 0x5F52:
                        historicalBytes(tlv.mV);
                        break;
                    case 0x73:
                        parseDdo((Iso7816TLV.Iso7816CompositeTLV) tlv);
                        break;
                    case 0xC0:
                        parseExtendedCaps(tlv.mV);
                        break;
                    case 0xC1:
                        signKeyFormat(KeyFormat.fromBytes(tlv.mV));
                        break;
                    case 0xC2:
                        encryptKeyFormat(KeyFormat.fromBytes(tlv.mV));
                        break;
                    case 0xC3:
                        authKeyFormat(KeyFormat.fromBytes(tlv.mV));
                        break;
                    case 0xC4:
                        pwStatusBytes(tlv.mV);
                        break;
                    case 0xC5:
                        parseFingerprints(tlv.mV);
                        break;
                }
            }

            return this;
        }

        private void parseDdo(Iso7816TLV.Iso7816CompositeTLV tlvs) {
            for (Iso7816TLV tlv : tlvs.mSubs) {
                switch (tlv.mT) {
                    case 0xC0:
                        parseExtendedCaps(tlv.mV);
                        break;
                    case 0xC1:
                        signKeyFormat(KeyFormat.fromBytes(tlv.mV));
                        break;
                    case 0xC2:
                        encryptKeyFormat(KeyFormat.fromBytes(tlv.mV));
                        break;
                    case 0xC3:
                        authKeyFormat(KeyFormat.fromBytes(tlv.mV));
                        break;
                    case 0xC4:
                        pwStatusBytes(tlv.mV);
                        break;
                    case 0xC5:
                        parseFingerprints(tlv.mV);
                        break;
                }
            }
        }

        private void parseFingerprints(byte[] mV) {
            ByteBuffer fpBuf = ByteBuffer.wrap(mV);

            byte[] buf;

            buf = new byte[20];
            fpBuf.get(buf);
            fingerprintSign(buf);

            buf = new byte[20];
            fpBuf.get(buf);
            fingerprintEncrypt(buf);

            buf = new byte[20];
            fpBuf.get(buf);
            fingerprintAuth(buf);
        }

         private void parseExtendedCaps(byte[] v) {
             hasKeyImport((v[0] & MASK_KEY_IMPORT) != 0);
             attributesChangable((v[0] & MASK_ATTRIBUTES_CHANGABLE) != 0);

             if ((v[0] & MASK_SM) != 0) {
                 hasSM(true);
                 int smType = v[1];
                 hasAesSm(smType == 1 || smType == 2);
                 hasScp11bSm(smType == 3);
             }

             hasKdf((v[0] & MASK_KDF_DO) == 1);

             maxCmdLen((v[6] << 8) + v[7]);
             maxRspLen((v[8] << 8) + v[9]);
         }

    }
}
