/* Copyright (C) 2014 Vincent Breitmoser <v.breitmoser@mugenguild.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.sufficientlysecure.keychain.securitytoken;

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

/** TLV data structure and parser implementation.
 *
 * This class is used for parsing and working with TLV packets as specified in
 * the Functional Specification of the OpenPGP application on ISO Smart Card
 * Operating Systems, and ISO 7816-4.
 *
 * Objects of this class are immutable data structs parsed from BER-TLV data.
 * They include at least the T, L and V fields they were originally parsed
 * from, subclasses may also carry more specific information.
 *
 * @see { @linktourl http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-d.aspx}
 *
 */
public class Iso7816TLV {

    public final int mT;
    public final int mL;
    public final byte[] mV;

    private Iso7816TLV(int T, int L, byte[] V) {
        mT = T;
        mL = L;
        mV = V;
    }

    public String prettyPrint() {
        return prettyPrint(0);
    }

    public String prettyPrint(int indent) {
        // lol
        String padding = "                                                  ".substring(0, indent*2);
        return padding + String.format("tag T %4x L %04d", mT, mL);
    }

    /** Read a single Iso7816 TLV packet from a given ByteBuffer, either
     * recursively or flat.
     *
     * This is a convenience wrapper for readSingle with ByteBuffer argument.
     */
    public static Iso7816TLV readSingle(byte[] data, boolean recursive) throws IOException {
        return readSingle(ByteBuffer.wrap(data), recursive);
    }

    /** Read a single Iso7816 TLV packet from a given ByteBuffer, either
     * recursively or flat.
     *
     * If the recursive flag is true, a composite packet will be parsed and
     * returned as an Iso7816CompositeTLV. Otherwise, a regular Iso7816TLV
     * object will be returned regardless of actual packet type.
     *
     * This method is fail-fast, if any parsing error occurs it will throw an
     * exception.
     *
     */
    public static Iso7816TLV readSingle(ByteBuffer data, boolean recursive) throws IOException {

        int T = data.get() & 0xff;
        boolean composite = (T & 0x20) == 0x20;
        if ((T & 0x1f) == 0x1f) {
            int T2 = data.get() & 0xff;
            if ((T2 & 0x1f) == 0x1f) {
                throw new IOException("Only tags up to two bytes are supported!");
            }
            T = (T << 8) | (T2 & 0x7f);
        }

        // Log.d(Constants.TAG, String.format("T %02x", T));

        // parse length, according to ISO 7816-4 (openpgp card 2.0 specs, page 24)
        int L = data.get() & 0xff;
        if (L == 0x81) {
            L = data.get() & 0xff;
        } else if (L == 0x82) {
            L = data.get() & 0xff;
            L = (L << 8) | (data.get() & 0xff);
        } else if (L >= 0x80) {
            throw new IOException("Invalid length field!");
        }

        // Log.d(Constants.TAG, String.format("L %02x", L));

        // read L bytes into new buffer
        byte[] V = new byte[L];
        data.get(V);

        // if we are supposed to parse composites, do that
        if (recursive && composite) {
            // Log.d(Constants.TAG, "parsing composite TLV");
            Iso7816TLV[] subs = readList(V, true);
            return new Iso7816CompositeTLV(T, L, V, subs);
        }

        return new Iso7816TLV(T, L, V);

    }

    /** Parse a list of TLV packets from byte data, recursively or flat.
     *
     * This method is fail-fast, if any parsing error occurs it will throw an
     * exception.
     *
     */
    public static Iso7816TLV[] readList(byte[] data, boolean recursive) throws IOException {
        ByteBuffer buf = ByteBuffer.wrap(data);

        ArrayList<Iso7816TLV> result = new ArrayList<>();

        // read while data is available. this will fail if there is trailing data!
        while (buf.hasRemaining()) {
            // skip 0x00 and 0xFF filler bytes
            buf.mark();
            byte peek = buf.get();
            if (peek == 0xff || peek == 0x00) {
                continue;
            }
            buf.reset();

            Iso7816TLV packet = readSingle(buf, recursive);
            result.add(packet);
        }

        Iso7816TLV[] resultX = new Iso7816TLV[result.size()];
        result.toArray(resultX);
        return resultX;
    }

    /** This class represents a composite TLV packet.
     *
     * Note that only actual composite TLV packets are instances of this class.
     * A regular non-composite Iso7816TLV object may however be a composite
     * packet if it was parsed non-recursively.
     *
     */
    public static class Iso7816CompositeTLV extends Iso7816TLV {

        public final Iso7816TLV[] mSubs;

        public Iso7816CompositeTLV(int T, int L, byte[] V, Iso7816TLV[] subs) {
            super(T, L, V);

            mSubs = subs;
        }

        public String prettyPrint(int indent) {
            StringBuilder result = new StringBuilder();
            // lol
            result.append("                                                  ".substring(0, indent*2));
            result.append(String.format("composite tag T %4x L %04d", mT, mL));
            for (Iso7816TLV sub : mSubs) {
                result.append('\n');
                result.append(sub.prettyPrint(indent+1));
            }
            return result.toString();
        }

    }

    /** Recursively searches for a specific tag in a composite TLV packet structure, depth first. */
    public static Iso7816TLV findRecursive(Iso7816TLV in, int tag) {
        if (in.mT == tag) {
            return in;
        } else if (in instanceof Iso7816CompositeTLV) {
            for (Iso7816TLV sub : ((Iso7816CompositeTLV) in).mSubs) {
                Iso7816TLV result = findRecursive(sub, tag);
                if (result != null) {
                    return result;
                }
            }
        }
        return null;
    }

}
