package act.data;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * 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.
 * #L%
 */

import act.app.ActionContext;
import org.osgl.exception.UnexpectedException;
import org.osgl.http.H;
import org.osgl.mvc.result.ErrorResult;
import org.osgl.mvc.result.Result;
import org.osgl.util.C;
import org.osgl.util.Codec;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

// Disclaim the source code is copied from Play!Framework 1.3
public class UrlEncodedParser extends RequestBodyParser {
    
    boolean forQueryString = false;

    @Override
    public Map<String, String[]> parse(ActionContext context) {
        H.Request request = context.req();
        // Encoding is either retrieved from contentType or it is the default encoding
        final String encoding = request.characterEncoding();
        InputStream is = request.inputStream();
        try {
            Map<String, String[]> params = new LinkedHashMap<String, String[]>();
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = is.read(buffer)) > 0) {
                os.write(buffer, 0, bytesRead);
            }

            String data = new String(os.toByteArray(), encoding);
            if (data.length() == 0) {
                //data is empty - can skip the rest
                return new HashMap<String, String[]>(0);
            }

            // check if data is in JSON format
            if (data.startsWith("{") && data.endsWith("}") || data.startsWith("[") && data.endsWith("]")) {
                return C.Map(ActionContext.REQ_BODY, new String[]{data});
            }

            // data is o the form:
            // a=b&b=c%12...

            // Let us lookup in two phases - we wait until everything is parsed before
            // we decoded it - this makes it possible for use to look for the
            // special _charset_ param which can hold the charset the form is encoded in.
            //
            // http://www.crazysquirrel.com/computing/general/form-encoding.jspx
            // https://bugzilla.mozilla.org/show_bug.cgi?id=18643
            //
            // NB: _charset_ must always be used with accept-charset and it must have the same value

            String[] keyValues = data.split("&");

            int httpMaxParams = context.app().config().httpMaxParams();
            // to prevent the server from being vulnerable to POST hash collision DOS-attack (Denial of Service through hash table multi-collisions),
            // we should by default not lookup the params into HashMap if the count exceeds a maximum limit
            if (httpMaxParams != 0 && keyValues.length > httpMaxParams) {
                logger.warn("Number of request parameters %d is higher than maximum of %d, aborting. Can be configured using 'act.http.params.max'", keyValues.length, httpMaxParams);
                throw new ErrorResult(H.Status.valueOf(413)); //413 Request Entity Too Large
            }

            for (String keyValue : keyValues) {
                // split this key-value on the first '='
                int i = keyValue.indexOf('=');
                String key = null;
                String value = null;
                if (i > 0) {
                    key = keyValue.substring(0, i);
                    value = keyValue.substring(i + 1);
                } else {
                    key = keyValue;
                }
                if (key.length() > 0) {
                    MapUtil.mergeValueInMap(params, key, value);
                }
            }

            // Second phase - look for _charset_ param and do the encoding
            Charset charset = Charset.forName(encoding);
            if (params.containsKey("_charset_")) {
                // The form contains a _charset_ param - When this is used together
                // with accept-charset, we can use _charset_ to extract the encoding.
                // PS: When rendering the view/form, _charset_ and accept-charset must be given the
                // same value - since only Firefox and sometimes IE actually sets it when Posting
                String providedCharset = params.get("_charset_")[0];
                // Must be sure the providedCharset is a valid encoding..
                try {
                    "test".getBytes(providedCharset);
                    charset = Charset.forName(providedCharset); // it works..
                } catch (Exception e) {
                    logger.debug("Got invalid _charset_ in form: " + providedCharset);
                    // lets just use the default one..
                }
            }

            // We're ready to decode the params
            Map<String, String[]> decodedParams = new LinkedHashMap<String, String[]>(params.size());
            for (Map.Entry<String, String[]> e : params.entrySet()) {
                String key = e.getKey();
                try {
                    key = Codec.decodeUrl(e.getKey(), charset);
                } catch (Throwable z) {
                    // Nothing we can do about, ignore
                }
                for (String value : e.getValue()) {
                    try {
                        MapUtil.mergeValueInMap(decodedParams, key, (value == null ? null : Codec.decodeUrl(value, charset)));
                    } catch (Throwable z) {
                        // Nothing we can do about, lets fill in with the non decoded value
                        MapUtil.mergeValueInMap(decodedParams, key, value);
                    }
                }
            }

            // add the complete body as a parameters
            if (!forQueryString) {
                decodedParams.put(ActionContext.REQ_BODY, new String[]{data});
            }

            return decodedParams;
        } catch (Result s) {
            // just pass it along
            throw s;
        } catch (Exception e) {
            throw new UnexpectedException(e);
        }
    }

}
