package com.atlassian.stash.internal.web.soy.functions;

import com.atlassian.soy.renderer.JsExpression;
import com.atlassian.soy.renderer.SoyClientFunction;
import com.atlassian.soy.renderer.SoyServerFunction;
import com.atlassian.bitbucket.json.JsonRenderer;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableSet;

import java.util.Collections;
import java.util.Map;
import java.util.Set;

public class ToJsonFunction implements SoyServerFunction<String>, SoyClientFunction {
    private static final Set<Integer> VALID_ARG_SIZES = ImmutableSet.of(1, 2);

    /**
     * Unicode line and paragraph separators, which are not allowed in Javascript's String literals
     * @see <a href="http://timelessrepo.com/json-isnt-a-javascript-subset">JSON: The JavaScript subset that isn't</a>
     */
    private static final CharMatcher SEPARATORS = CharMatcher.anyOf("\u2028\u2029");

    private final JsonRenderer jsonRenderer;

    public ToJsonFunction(JsonRenderer jsonRenderer) {
        this.jsonRenderer = jsonRenderer;
    }

    @Override
    public JsExpression generate(JsExpression... args) {
        // The .replace() avoids XSS through document.write(</script>). I don't think we'll ever use document.write(), but
        // guarding just in case.
        // The replace turns </script> into <\/script> (in other words, the forward slash is "escaped" even though it has no special meaning
        // So it still ends up parsing back from \/ to just /
        return new JsExpression("JSON.stringify(" + args[0].getText() + ").replace(/\\//g, '\\\\/')");
    }

    @Override
    public String getName() {
        return "to_json";
    }

    @Override
    public String apply(Object... objects) {
        return safeJson(jsonRenderer.render(objects[0],
                objects.length == 2 ? (Map<String, Object>) objects[1] : Collections.<String, Object>emptyMap()));
    }

    private String safeJson(String json) {
        return SEPARATORS.replaceFrom(json, "\\n");
    }

    @Override
    public Set<Integer> validArgSizes() {
        return VALID_ARG_SIZES;
    }
}
