/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 freemarker.core;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import freemarker.template.MalformedTemplateNameException;
import freemarker.template.SimpleHash;
import freemarker.template.Template;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateHashModelEx2.KeyValuePair;
import freemarker.template.TemplateHashModelEx2.KeyValuePairIterator;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateScalarModel;
import freemarker.template._TemplateAPI;
import freemarker.template.utility.TemplateModelUtils;

/**
 * Implements {@code .get_optional_template(name, options)}.
 */
class GetOptionalTemplateMethod implements TemplateMethodModelEx {

    static final GetOptionalTemplateMethod INSTANCE = new GetOptionalTemplateMethod(
            BuiltinVariable.GET_OPTIONAL_TEMPLATE);
    static final GetOptionalTemplateMethod INSTANCE_CC = new GetOptionalTemplateMethod(
            BuiltinVariable.GET_OPTIONAL_TEMPLATE_CC);
    
    private static final String OPTION_ENCODING = "encoding";
    private static final String OPTION_PARSE = "parse";

    private static final String RESULT_INCLUDE = "include";
    private static final String RESULT_IMPORT = "import";
    private static final String RESULT_EXISTS = "exists";
   
    /** Used in error messages */
    private final String methodName;

    private GetOptionalTemplateMethod(String builtInVarName) {
        this.methodName = "." + builtInVarName;
    }

    @Override
    public Object exec(List args) throws TemplateModelException {
        final int argCnt = args.size();
        if (argCnt < 1 || argCnt > 2) {
            throw _MessageUtil.newArgCntError(methodName, argCnt, 1, 2);
        }

        final Environment env = Environment.getCurrentEnvironment();
        if (env == null) {
            throw new IllegalStateException("No freemarer.core.Environment is associated to the current thread.");
        }
        
        final String absTemplateName;
        {
            TemplateModel arg = (TemplateModel) args.get(0);
            if (!(arg instanceof TemplateScalarModel)) {
                throw _MessageUtil.newMethodArgMustBeStringException(methodName, 0, arg);
            }
            String templateName  = EvalUtil.modelToString((TemplateScalarModel) arg, null, env);
            
            try {
                absTemplateName = env.toFullTemplateName(env.getCurrentTemplate().getName(), templateName);
            } catch (MalformedTemplateNameException e) {
                throw new _TemplateModelException(
                        e, "Failed to convert template path to full path; see cause exception.");
            }
        }
        
        final TemplateHashModelEx options;
        if (argCnt > 1) {
            TemplateModel arg = (TemplateModel) args.get(1);
            if (!(arg instanceof TemplateHashModelEx)) {
                throw _MessageUtil.newMethodArgMustBeExtendedHashException(methodName, 1, arg);
            }
            options = (TemplateHashModelEx) arg;
        } else {
            options = null;
        }
        
        String encoding = null;
        boolean parse = true;
        if (options != null) {
            final KeyValuePairIterator kvpi = TemplateModelUtils.getKeyValuePairIterator(options);
            while (kvpi.hasNext()) {
                final KeyValuePair kvp = kvpi.next();
                
                final String optName;
                {
                    TemplateModel optNameTM = kvp.getKey();
                    if (!(optNameTM instanceof TemplateScalarModel)) {
                        throw _MessageUtil.newMethodArgInvalidValueException(methodName, 1,
                                "All keys in the options hash must be strings, but found ",
                                new _DelayedAOrAn(new _DelayedFTLTypeDescription(optNameTM)));
                    }
                    optName = ((TemplateScalarModel) optNameTM).getAsString();
                }
                
                final TemplateModel optValue = kvp.getValue();
                
                if (OPTION_ENCODING.equals(optName)) {
                    encoding = getStringOption(OPTION_ENCODING, optValue); 
                } else if (OPTION_PARSE.equals(optName)) {
                    parse = getBooleanOption(OPTION_PARSE, optValue); 
                } else {
                    throw _MessageUtil.newMethodArgInvalidValueException(methodName, 1,
                            "Unsupported option ", new _DelayedJQuote(optName), "; valid names are: ",
                            new _DelayedJQuote(OPTION_ENCODING), ", ", new _DelayedJQuote(OPTION_PARSE), ".");
                }
            }
        }

        final Template template;
        try {
            template = env.getTemplateForInclusion(absTemplateName, encoding, parse, true);
        } catch (IOException e) {
            throw new _TemplateModelException(
                    e, "I/O error when trying to load optional template ", new _DelayedJQuote(absTemplateName),
                        "; see cause exception");
        }
        
        SimpleHash result = new SimpleHash(_TemplateAPI.SAFE_OBJECT_WRAPPER);
        result.put(RESULT_EXISTS, template != null);
        // If the template is missing, result.include and such will be missing too, so that a default can be
        // conveniently provided like in <@optTemp.include!myDefaultMacro />.
        if (template != null) {
            result.put(RESULT_INCLUDE, new TemplateDirectiveModel() {
                @Override
                public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
                        throws TemplateException, IOException {
                    if (!params.isEmpty()) {
                        throw new TemplateException("This directive supports no parameters.", env);
                    }
                    if (loopVars.length != 0) {
                        throw new TemplateException("This directive supports no loop variables.", env);
                    }
                    if (body != null) {
                        throw new TemplateException("This directive supports no nested content.", env);
                    }
                    
                    env.include(template);
                }
            });
            result.put(RESULT_IMPORT, new TemplateMethodModelEx() {
                @Override
                public Object exec(List args) throws TemplateModelException {
                    if (!args.isEmpty()) {
                        throw new TemplateModelException("This method supports no parameters.");
                    }
                    
                    try {
                        return env.importLib(template, null);
                    } catch (IOException | TemplateException e) {
                        throw new _TemplateModelException(e, "Failed to import loaded template; see cause exception");
                    }
                }
            });
        }
        return result;
    }

    private boolean getBooleanOption(String optionName, TemplateModel value) throws TemplateModelException {
        if (!(value instanceof TemplateBooleanModel)) {
            throw _MessageUtil.newMethodArgInvalidValueException(methodName, 1,
                    "The value of the ", new _DelayedJQuote(optionName), " option must be a boolean, but it was ",
                    new _DelayedAOrAn(new _DelayedFTLTypeDescription(value)), ".");
        }
        return ((TemplateBooleanModel) value).getAsBoolean();
    }

    private String getStringOption(String optionName, TemplateModel value) throws TemplateModelException {
        if (!(value instanceof TemplateScalarModel)) {
            throw _MessageUtil.newMethodArgInvalidValueException(methodName, 1,
                    "The value of the ", new _DelayedJQuote(optionName), " option must be a string, but it was ",
                    new _DelayedAOrAn(new _DelayedFTLTypeDescription(value)), ".");
        }
        return EvalUtil.modelToString((TemplateScalarModel) value, null, null);
    }

}