package act.view.rythm;

/*-
 * #%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 static org.rythmengine.conf.RythmConfigurationKey.*;

import act.Act;
import act.app.App;
import act.conf.AppConfig;
import act.view.Template;
import act.view.VarDef;
import act.view.View;
import org.osgl.util.C;
import org.osgl.util.S;
import org.rythmengine.Rythm;
import org.rythmengine.RythmEngine;
import org.rythmengine.extension.IFormatter;
import org.rythmengine.extension.ISourceCodeEnhancer;
import org.rythmengine.extension.Transformer;
import org.rythmengine.resource.ClasspathResourceLoader;
import org.rythmengine.template.ITemplate;
import org.rythmengine.utils.RawData;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Implement a view with Rythm Template Engine
 */
public class RythmView extends View {

    public static final String ID = "rythm";

    ConcurrentMap<App, RythmEngine> engines = new ConcurrentHashMap<>();
    ConcurrentMap<String, Template> templates = new ConcurrentHashMap<>();
    ConcurrentMap<String, String> missings = new ConcurrentHashMap<>();

    private boolean isDev;

    public RythmView() {
        isDev = Act.isDev();
    }

    @Override
    public String name() {
        return ID;
    }

    @Override
    protected Template loadTemplate(String resourcePath) {
        App app = Act.app();
        if (isDev) {
            return loadTemplateFromResource(resourcePath, app);
        }
        if (missings.containsKey(resourcePath)) {
            return null;
        }
        Template template = templates.get(resourcePath);
        if (null == template) {
            Template newTemplate = loadTemplateFromResource(resourcePath, app);
            if (null != newTemplate) {
                template = templates.putIfAbsent(resourcePath, newTemplate);
                if (null == template) {
                    template = newTemplate;
                }
            } else {
                missings.put(resourcePath, resourcePath);
            }
        }
        return template;
    }

    @Override
    protected Template loadInlineTemplate(String content) {
        RythmEngine engine = getEngine(Act.app());
        return new RythmTemplate(engine, content, true);
    }

    public RythmEngine getEngine(App app) {
        RythmEngine engine = engines.get(app);
        if (null == engine) {
            RythmEngine newEngine = createEngine(app);
            engine = engines.putIfAbsent(app, newEngine);
            if (null == engine) {
                engine = newEngine;
            } else {
                newEngine.shutdown();
            }
        }
        return engine;
    }

    private Template loadTemplateFromResource(String resourcePath, App app) {
        RythmEngine engine = getEngine(app);
        return RythmTemplate.find(engine, resourcePath);
    }

    private RythmEngine createEngine(App app) {
        AppConfig config = app.config();
        Properties p = new Properties();

        p.put(ENGINE_MODE.getKey(), Act.mode().isDev() ? Rythm.Mode.dev : Rythm.Mode.prod);
        p.put(ENGINE_PLUGIN_VERSION.getKey(), Act.VERSION.getVersion());
        p.put(ENGINE_CLASS_LOADER_PARENT_IMPL.getKey(), app.classLoader());
        p.put(HOME_TMP.getKey(), createTempHome(app));
        p.put(I18N_LOCALE.getKey(), config.locale());

        Map map = config.rawConfiguration();
        for (Object k : map.keySet()) {
            String key = k.toString();
            if (key.startsWith("rythm.")) {
                p.put(key, map.get(key));
            }
        }

        String appRestricted = p.getProperty("rythm.sandbox.restricted_classes", "");
        appRestricted += ";act.*";
        p.put(SANDBOX_RESTRICTED_CLASS.getKey(), appRestricted);

        p.put(HOME_TEMPLATE.getKey(), templateRootDir());

        p.put(CODEGEN_SOURCE_CODE_ENHANCER.getKey(), new ISourceCodeEnhancer() {
            @Override
            public List<String> imports() {
                return C.list();
            }

            @Override
            public String sourceCode() {
                return "";
            }

            @Override
            public Map<String, ?> getRenderArgDescriptions() {
                Map<String, String> map = new HashMap<>();
                for (VarDef var : Act.viewManager().implicitActionViewVariables()) {
                    map.put(var.name(), var.type());
                }
                return map;
            }

            @Override
            public void setRenderArgs(ITemplate iTemplate) {
                // no need to set render args here as
                // it's all done at TemplateBase#exposeImplicitVariables
            }
        });

        RythmEngine engine = new RythmEngine(p);
        engine.setDateFormatFactory(new DateFormatFactory(config));
        engine.resourceManager().prependResourceLoader(new ClasspathResourceLoader(engine, ID));
        if (config.defaultView() == this) {
            String home = config.templateHome();
            if (S.neq("default", home) && S.neq(ID, home)) {
                engine.resourceManager().prependResourceLoader(new ClasspathResourceLoader(engine, home));
            }
        }
        engine.registerTransformer("rythm", "", RythmView.class);

        Tags tags = app.getInstance(Tags.class);
        tags.register(engine);

        return engine;
    }

    public void registerTransformer(App app, Class<?> clazz) {
        getEngine(app).registerTransformer(clazz);
    }

    public void registerBuiltInTransformer(App app, Class<?> clazz) {
        getEngine(app).registerTransformer("rythm", "([^a-zA-Z0-9_]s\\(\\)|^s\\(\\))", clazz);
    }

    public void registerFormatter(App app, IFormatter formatter) {
        getEngine(app).extensionManager().registerFormatter(formatter);
    }

    @Override
    protected void reload(App app) {
        engines.remove(app);
        super.reload(app);
    }

    private File createTempHome(App app) {
        String tmp = System.getProperty("java.io.tmpdir");
        File f =  new File(tmp, "__rythm_" + app.name());
        if (!f.exists() && !f.mkdirs()) {
            f = new File(tmp);
        }
        return f;
    }

    @Transformer
    public static RawData errorLine(String data, int errorColumn) {
        String line = S.string(data);
        if (errorColumn > -1 && errorColumn < line.length()) {
            String a = line.substring(0, errorColumn);
            char c = line.charAt(errorColumn);
            String b = line.substring(errorColumn + 1);
            line = S.concat(a, "<span_class='error-column'>", c, "</span>", b);
            line = S.replace(" ").with("&nbsp;").in(line);
            line = S.replace("span_class").with("span class").in(line);
        } else {
            line = S.replace(" ").with("&nbsp;").in(line);
        }
        return new RawData(line);
    }

    @Transformer
    public static RawData errorLine(RawData data, int errorColumn) {
        String line = S.string(data);
        if (errorColumn > -1 && errorColumn < line.length()) {
            String a = line.substring(0, errorColumn);
            char c = line.charAt(errorColumn);
            String b = line.substring(errorColumn + 1);
            line = S.concat(a, "<span_class='error-column'>", c, "</span>", b);
            line = S.replace(" ").with("&nbsp;").in(line);
            line = S.replace("span_class").with("span class").in(line);
        } else {
            line = S.replace(" ").with("&nbsp;").in(line);
        }
        return new RawData(line);
    }

}
