/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * 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 android.bluetooth.client.map;

import android.util.Log;

import com.android.vcard.VCardEntry;
import com.android.vcard.VCardEntryConstructor;
import com.android.vcard.VCardEntryHandler;
import com.android.vcard.VCardParser;
import com.android.vcard.VCardParser_V21;
import com.android.vcard.VCardParser_V30;
import com.android.vcard.exception.VCardException;
import com.android.vcard.exception.VCardVersionException;
import android.bluetooth.client.map.BluetoothMapBmessage.Status;
import android.bluetooth.client.map.BluetoothMapBmessage.Type;
import android.bluetooth.client.map.utils.BmsgTokenizer;
import android.bluetooth.client.map.utils.BmsgTokenizer.Property;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.text.ParseException;

class BluetoothMapBmessageParser {

    private final static String TAG = "BluetoothMapBmessageParser";

    private final static String CRLF = "\r\n";

    private final static Property BEGIN_BMSG = new Property("BEGIN", "BMSG");
    private final static Property END_BMSG = new Property("END", "BMSG");

    private final static Property BEGIN_VCARD = new Property("BEGIN", "VCARD");
    private final static Property END_VCARD = new Property("END", "VCARD");

    private final static Property BEGIN_BENV = new Property("BEGIN", "BENV");
    private final static Property END_BENV = new Property("END", "BENV");

    private final static Property BEGIN_BBODY = new Property("BEGIN", "BBODY");
    private final static Property END_BBODY = new Property("END", "BBODY");

    private final static Property BEGIN_MSG = new Property("BEGIN", "MSG");
    private final static Property END_MSG = new Property("END", "MSG");

    private final static int CRLF_LEN = 2;

    /*
     * length of "container" for 'message' in bmessage-body-content:
     * BEGIN:MSG<CRLF> + <CRLF> + END:MSG<CRFL>
     */
    private final static int MSG_CONTAINER_LEN = 22;

    private BmsgTokenizer mParser;

    private final BluetoothMapBmessage mBmsg;

    private BluetoothMapBmessageParser() {
        mBmsg = new BluetoothMapBmessage();
    }

    static public BluetoothMapBmessage createBmessage(String str) {
        BluetoothMapBmessageParser p = new BluetoothMapBmessageParser();

        try {
            p.parse(str);
        } catch (IOException e) {
            Log.e(TAG, "I/O exception when parsing bMessage", e);
            return null;
        } catch (ParseException e) {
            Log.e(TAG, "Cannot parse bMessage", e);
            return null;
        }

        return p.mBmsg;
    }

    private ParseException expected(Property... props) {
        boolean first = true;
        StringBuilder sb = new StringBuilder();

        for (Property prop : props) {
            if (!first) {
                sb.append(" or ");
            }
            sb.append(prop);
            first = false;
        }

        return new ParseException("Expected: " + sb.toString(), mParser.pos());
    }

    private void parse(String str) throws IOException, ParseException {

        Property prop;

        /*
         * <bmessage-object>::= { "BEGIN:BMSG" <CRLF> <bmessage-property>
         * [<bmessage-originator>]* <bmessage-envelope> "END:BMSG" <CRLF> }
         */

        mParser = new BmsgTokenizer(str + CRLF);

        prop = mParser.next();
        if (!prop.equals(BEGIN_BMSG)) {
            throw expected(BEGIN_BMSG);
        }

        prop = parseProperties();

        while (prop.equals(BEGIN_VCARD)) {

            /* <bmessage-originator>::= <vcard> <CRLF> */

            StringBuilder vcard = new StringBuilder();
            prop = extractVcard(vcard);

            VCardEntry entry = parseVcard(vcard.toString());
            mBmsg.mOriginators.add(entry);
        }

        if (!prop.equals(BEGIN_BENV)) {
            throw expected(BEGIN_BENV);
        }

        prop = parseEnvelope(1);

        if (!prop.equals(END_BMSG)) {
            throw expected(END_BENV);
        }

        /*
         * there should be no meaningful data left in stream here so we just
         * ignore whatever is left
         */

        mParser = null;
    }

    private Property parseProperties() throws ParseException {

        Property prop;

        /*
         * <bmessage-property>::=<bmessage-version-property>
         * <bmessage-readstatus-property> <bmessage-type-property>
         * <bmessage-folder-property> <bmessage-version-property>::="VERSION:"
         * <common-digit>*"."<common-digit>* <CRLF>
         * <bmessage-readstatus-property>::="STATUS:" 'readstatus' <CRLF>
         * <bmessage-type-property>::="TYPE:" 'type' <CRLF>
         * <bmessage-folder-property>::="FOLDER:" 'foldername' <CRLF>
         */

        do {
            prop = mParser.next();

            if (prop.name.equals("VERSION")) {
                mBmsg.mBmsgVersion = prop.value;

            } else if (prop.name.equals("STATUS")) {
                for (Status s : Status.values()) {
                    if (prop.value.equals(s.toString())) {
                        mBmsg.mBmsgStatus = s;
                        break;
                    }
                }

            } else if (prop.name.equals("TYPE")) {
                for (Type t : Type.values()) {
                    if (prop.value.equals(t.toString())) {
                        mBmsg.mBmsgType = t;
                        break;
                    }
                }

            } else if (prop.name.equals("FOLDER")) {
                mBmsg.mBmsgFolder = prop.value;

            }

        } while (!prop.equals(BEGIN_VCARD) && !prop.equals(BEGIN_BENV));

        return prop;
    }

    private Property parseEnvelope(int level) throws IOException, ParseException {

        Property prop;

        /*
         * we can support as many nesting level as we want, but MAP spec clearly
         * defines that there should be no more than 3 levels. so we verify it
         * here.
         */

        if (level > 3) {
            throw new ParseException("bEnvelope is nested more than 3 times", mParser.pos());
        }

        /*
         * <bmessage-envelope> ::= { "BEGIN:BENV" <CRLF> [<bmessage-recipient>]*
         * <bmessage-envelope> | <bmessage-content> "END:BENV" <CRLF> }
         */

        prop = mParser.next();

        while (prop.equals(BEGIN_VCARD)) {

            /* <bmessage-originator>::= <vcard> <CRLF> */

            StringBuilder vcard = new StringBuilder();
            prop = extractVcard(vcard);

            if (level == 1) {
                VCardEntry entry = parseVcard(vcard.toString());
                mBmsg.mRecipients.add(entry);
            }
        }

        if (prop.equals(BEGIN_BENV)) {
            prop = parseEnvelope(level + 1);

        } else if (prop.equals(BEGIN_BBODY)) {
            prop = parseBody();

        } else {
            throw expected(BEGIN_BENV, BEGIN_BBODY);
        }

        if (!prop.equals(END_BENV)) {
            throw expected(END_BENV);
        }

        return mParser.next();
    }

    private Property parseBody() throws IOException, ParseException {

        Property prop;

        /*
         * <bmessage-content>::= { "BEGIN:BBODY"<CRLF> [<bmessage-body-part-ID>
         * <CRLF>] <bmessage-body-property> <bmessage-body-content>* <CRLF>
         * "END:BBODY"<CRLF> } <bmessage-body-part-ID>::="PARTID:" 'Part-ID'
         * <bmessage-body-property>::=[<bmessage-body-encoding-property>]
         * [<bmessage-body-charset-property>]
         * [<bmessage-body-language-property>]
         * <bmessage-body-content-length-property>
         * <bmessage-body-encoding-property>::="ENCODING:"'encoding' <CRLF>
         * <bmessage-body-charset-property>::="CHARSET:"'charset' <CRLF>
         * <bmessage-body-language-property>::="LANGUAGE:"'language' <CRLF>
         * <bmessage-body-content-length-property>::= "LENGTH:" <common-digit>*
         * <CRLF>
         */

        do {
            prop = mParser.next();

            if (prop.name.equals("PARTID")) {
            } else if (prop.name.equals("ENCODING")) {
                mBmsg.mBbodyEncoding = prop.value;

            } else if (prop.name.equals("CHARSET")) {
                mBmsg.mBbodyCharset = prop.value;

            } else if (prop.name.equals("LANGUAGE")) {
                mBmsg.mBbodyLanguage = prop.value;

            } else if (prop.name.equals("LENGTH")) {
                try {
                    mBmsg.mBbodyLength = Integer.valueOf(prop.value);
                } catch (NumberFormatException e) {
                    throw new ParseException("Invalid LENGTH value", mParser.pos());
                }

            }

        } while (!prop.equals(BEGIN_MSG));

        /*
         * <bmessage-body-content>::={ "BEGIN:MSG"<CRLF> 'message'<CRLF>
         * "END:MSG"<CRLF> }
         */

        int messageLen = mBmsg.mBbodyLength - MSG_CONTAINER_LEN;
        int offset = messageLen + CRLF_LEN;
        int restartPos = mParser.pos() + offset;

        /*
         * length is specified in bytes so we need to convert from unicode
         * string back to bytes array
         */

        String remng = mParser.remaining();
        byte[] data = remng.getBytes();

        /* restart parsing from after 'message'<CRLF> */
        mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos);

        prop = mParser.next(true);

        if (prop != null) {
            if (prop.equals(END_MSG)) {
                mBmsg.mMessage = new String(data, 0, messageLen);
            } else {
                /* Handle possible exception for incorrect LENGTH value
                 * from MSE while parsing  GET Message response */
                Log.e(TAG, "Prop Invalid: "+ prop.toString());
                Log.e(TAG, "Possible Invalid LENGTH value");
                throw expected(END_MSG);
            }
        } else {

            data = null;

            /*
             * now we check if bMessage can be parsed if LENGTH is handled as
             * number of characters instead of number of bytes
             */
            if (offset < 0 || offset > remng.length()) {
                /* Handle possible exception for incorrect LENGTH value
                 * from MSE while parsing  GET Message response */
                throw new ParseException("Invalid LENGTH value", mParser.pos());
            }

            Log.w(TAG, "byte LENGTH seems to be invalid, trying with char length");

            mParser = new BmsgTokenizer(remng.substring(offset));

            prop = mParser.next();

            if (!prop.equals(END_MSG)) {
                throw expected(END_MSG);
            }

            mBmsg.mMessage = remng.substring(0, messageLen);
        }

        prop = mParser.next();

        if (!prop.equals(END_BBODY)) {
            throw expected(END_BBODY);
        }

        return mParser.next();
    }

    private Property extractVcard(StringBuilder out) throws IOException, ParseException {
        Property prop;

        out.append(BEGIN_VCARD).append(CRLF);

        do {
            prop = mParser.next();
            out.append(prop).append(CRLF);
        } while (!prop.equals(END_VCARD));

        return mParser.next();
    }

    private class VcardHandler implements VCardEntryHandler {

        VCardEntry vcard;

        @Override
        public void onStart() {
        }

        @Override
        public void onEntryCreated(VCardEntry entry) {
            vcard = entry;
        }

        @Override
        public void onEnd() {
        }
    };

    private VCardEntry parseVcard(String str) throws IOException, ParseException {
        VCardEntry vcard = null;

        try {
            VCardParser p = new VCardParser_V21();
            VCardEntryConstructor c = new VCardEntryConstructor();
            VcardHandler handler = new VcardHandler();
            c.addEntryHandler(handler);
            p.addInterpreter(c);
            p.parse(new ByteArrayInputStream(str.getBytes()));

            vcard = handler.vcard;

        } catch (VCardVersionException e1) {

            try {
                VCardParser p = new VCardParser_V30();
                VCardEntryConstructor c = new VCardEntryConstructor();
                VcardHandler handler = new VcardHandler();
                c.addEntryHandler(handler);
                p.addInterpreter(c);
                p.parse(new ByteArrayInputStream(str.getBytes()));

                vcard = handler.vcard;

            } catch (VCardVersionException e2) {
                // will throw below
            } catch (VCardException e2) {
                // will throw below
            }

        } catch (VCardException e1) {
            // will throw below
        }

        if (vcard == null) {
            throw new ParseException("Cannot parse vCard object (neither 2.1 nor 3.0?)",
                    mParser.pos());
        }

        return vcard;
    }
}
