/*
 * Copyright (C) 2010 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 com.android.vcard;

import android.util.Log;

import com.android.vcard.exception.VCardException;

import java.io.IOException;
import java.util.Set;

/**
 * <p>
 * Basic implementation achieving vCard 3.0 parsing.
 * </p>
 * <p>
 * This class inherits vCard 2.1 implementation since technically they are similar,
 * while specifically there's logical no relevance between them.
 * So that developers are not confused with the inheritance,
 * {@link VCardParser_V30} does not inherit {@link VCardParser_V21}, while
 * {@link VCardParserImpl_V30} inherits {@link VCardParserImpl_V21}.
 * </p>
 * @hide
 */
/* package */ class VCardParserImpl_V30 extends VCardParserImpl_V21 {
    private static final String LOG_TAG = VCardConstants.LOG_TAG;

    private String mPreviousLine;
    private boolean mEmittedAgentWarning = false;

    public VCardParserImpl_V30() {
        super();
    }

    public VCardParserImpl_V30(int vcardType) {
        super(vcardType);
    }

    @Override
    protected int getVersion() {
        return VCardConfig.VERSION_30;
    }

    @Override
    protected String getVersionString() {
        return VCardConstants.VERSION_V30;
    }

    @Override
    protected String peekLine() throws IOException {
        if (mPreviousLine != null) {
            String ret = mPreviousLine;
            return ret;
        } else {
            return mReader.peekLine();
        }
    }

    @Override
    protected String getLine() throws IOException {
        if (mPreviousLine != null) {
            String ret = mPreviousLine;
            mPreviousLine = null;
            return ret;
        } else {
            return mReader.readLine();
        }
    }

    /**
     * vCard 3.0 requires that the line with space at the beginning of the line
     * must be combined with previous line.
     */
    @Override
    protected String getNonEmptyLine() throws IOException, VCardException {
        String line;
        StringBuilder builder = null;
        while ((line = mReader.readLine()) != null) {
            // Skip empty lines in order to accomodate implementations that
            // send line termination variations such as \r\r\n.
            if (line.length() == 0) {
                continue;
            } else if (line.charAt(0) == ' ' || line.charAt(0) == '\t') {
                // RFC 2425 describes line continuation as \r\n followed by
                // a single ' ' or '\t' whitespace character.
                if (builder == null) {
                    builder = new StringBuilder();
                }
                if (mPreviousLine != null) {
                    builder.append(mPreviousLine);
                    mPreviousLine = null;
                }
                builder.append(line.substring(1));
            } else {
                if (builder != null || mPreviousLine != null) {
                    break;
                }
                mPreviousLine = line;
            }
        }

        String ret = null;
        if (builder != null) {
            ret = builder.toString();
        } else if (mPreviousLine != null) {
            ret = mPreviousLine;
        }
        mPreviousLine = line;
        if (ret == null) {
            throw new VCardException("Reached end of buffer.");
        }
        return ret;
    }

    /*
     * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF
     *         1 * (contentline)
     *         ;A vCard object MUST include the VERSION, FN and N types.
     *         [group "."] "END" ":" "VCARD" 1 * CRLF
     */
    @Override
    protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException {
        // TODO: vCard 3.0 supports group.
        return super.readBeginVCard(allowGarbage);
    }

    /**
     * vCard 3.0 allows iana-token as paramType, while vCard 2.1 does not.
     */
    @Override
    protected void handleParams(VCardProperty propertyData, final String params)
            throws VCardException {
        try {
            super.handleParams(propertyData, params);
        } catch (VCardException e) {
            // maybe IANA type
            String[] strArray = params.split("=", 2);
            if (strArray.length == 2) {
                handleAnyParam(propertyData, strArray[0], strArray[1]);
            } else {
                // Must not come here in the current implementation.
                throw new VCardException(
                        "Unknown params value: " + params);
            }
        }
    }

    @Override
    protected void handleAnyParam(
            VCardProperty propertyData, final String paramName, final String paramValue) {
        splitAndPutParam(propertyData, paramName, paramValue);
    }

    @Override
    protected void handleParamWithoutName(VCardProperty property, final String paramValue) {
        handleType(property, paramValue);
    }

    /*
     *  vCard 3.0 defines
     *
     *  param         = param-name "=" param-value *("," param-value)
     *  param-name    = iana-token / x-name
     *  param-value   = ptext / quoted-string
     *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
     *  QSAFE-CHAR    = WSP / %x21 / %x23-7E / NON-ASCII
     *                ; Any character except CTLs, DQUOTE
     *
     *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\").
     */
    @Override
    protected void handleType(VCardProperty property, final String paramValue) {
        splitAndPutParam(property, VCardConstants.PARAM_TYPE, paramValue);
    }

    /**
     * Splits parameter values into pieces in accordance with vCard 3.0 specification and
     * puts pieces into mInterpreter.
     */
    /*
     *  param-value   = ptext / quoted-string
     *  quoted-string = DQUOTE QSAFE-CHAR DQUOTE
     *  QSAFE-CHAR    = WSP / %x21 / %x23-7E / NON-ASCII
     *                ; Any character except CTLs, DQUOTE
     *
     *  QSAFE-CHAR must not contain DQUOTE, including escaped one (\")
     */
    private void splitAndPutParam(VCardProperty property, String paramName, String paramValue) {
        // "comma,separated:inside.dquote",pref
        //   -->
        // - comma,separated:inside.dquote
        // - pref
        //
        // Note: Though there's a code, we don't need to take much care of
        // wrongly-added quotes like the example above, as they induce
        // parse errors at the top level (when splitting a line into parts).
        StringBuilder builder = null;  // Delay initialization.
        boolean insideDquote = false;
        final int length = paramValue.length();
        for (int i = 0; i < length; i++) {
            final char ch = paramValue.charAt(i);
            if (ch == '"') {
                if (insideDquote) {
                    // End of Dquote.
                    property.addParameter(paramName, encodeParamValue(builder.toString()));
                    builder = null;
                    insideDquote = false;
                } else {
                    if (builder != null) {
                        if (builder.length() > 0) {
                            // e.g.
                            // pref"quoted"
                            Log.w(LOG_TAG, "Unexpected Dquote inside property.");
                        } else {
                            // e.g.
                            // pref,"quoted"
                            // "quoted",pref
                            property.addParameter(paramName, encodeParamValue(builder.toString()));
                        }
                    }
                    insideDquote = true;
                }
            } else if (ch == ',' && !insideDquote) {
                if (builder == null) {
                    Log.w(LOG_TAG, "Comma is used before actual string comes. (" +
                            paramValue + ")");
                } else {
                    property.addParameter(paramName, encodeParamValue(builder.toString()));
                    builder = null;
                }
            } else {
                // To stop creating empty StringBuffer at the end of parameter,
                // we delay creating this object until this point.
                if (builder == null) {
                    builder = new StringBuilder();
                }
                builder.append(ch);
            }
        }
        if (insideDquote) {
            // e.g.
            // "non-quote-at-end
            Log.d(LOG_TAG, "Dangling Dquote.");
        }
        if (builder != null) {
            if (builder.length() == 0) {
                Log.w(LOG_TAG, "Unintended behavior. We must not see empty StringBuilder " +
                        "at the end of parameter value parsing.");
            } else {
                property.addParameter(paramName, encodeParamValue(builder.toString()));
            }
        }
    }

    /**
     * Encode a param value using UTF-8.
     */
    protected String encodeParamValue(String paramValue) {
        return VCardUtils.convertStringCharset(
                paramValue, VCardConfig.DEFAULT_INTERMEDIATE_CHARSET, "UTF-8");
    }

    @Override
    protected void handleAgent(VCardProperty property) {
        // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1.
        //
        // e.g.
        // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n
        //  TITLE:Area Administrator\, Assistant\n EMAIL\;TYPE=INTERN\n
        //  ET:jfriday@host.com\nEND:VCARD\n
        //
        // TODO: fix this.
        //
        // issue:
        //  vCard 3.0 also allows this as an example.
        //
        // AGENT;VALUE=uri:
        //  CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com
        //
        // This is not vCard. Should we support this?
        //
        // Just ignore the line for now, since we cannot know how to handle it...
        if (!mEmittedAgentWarning) {
            Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it");
            mEmittedAgentWarning = true;
        }
    }

    /**
     * This is only called from handlePropertyValue(), which has already
     * read the first line of this property. With v3.0, the getNonEmptyLine()
     * routine has already concatenated all following continuation lines.
     * The routine is implemented in the V21 parser to concatenate v2.1 style
     * data blocks, but is unnecessary here.
     */
    @Override
    protected String getBase64(final String firstString)
            throws IOException, VCardException {
        return firstString;
    }

    /**
     * ESCAPED-CHAR = "\\" / "\;" / "\," / "\n" / "\N")
     *              ; \\ encodes \, \n or \N encodes newline
     *              ; \; encodes ;, \, encodes ,
     *
     * Note: Apple escapes ':' into '\:' while does not escape '\'
     */
    @Override
    protected String maybeUnescapeText(final String text) {
        return unescapeText(text);
    }

    public static String unescapeText(final String text) {
        StringBuilder builder = new StringBuilder();
        final int length = text.length();
        for (int i = 0; i < length; i++) {
            char ch = text.charAt(i);
            if (ch == '\\' && i < length - 1) {
                final char next_ch = text.charAt(++i);
                if (next_ch == 'n' || next_ch == 'N') {
                    builder.append("\n");
                } else {
                    builder.append(next_ch);
                }
            } else {
                builder.append(ch);
            }
        }
        return builder.toString();
    }

    @Override
    protected String maybeUnescapeCharacter(final char ch) {
        return unescapeCharacter(ch);
    }

    public static String unescapeCharacter(final char ch) {
        if (ch == 'n' || ch == 'N') {
            return "\n";
        } else {
            return String.valueOf(ch);
        }
    }

    @Override
    protected Set<String> getKnownPropertyNameSet() {
        return VCardParser_V30.sKnownPropertyNameSet;
    }
}
