/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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 org.elasticsearch.script;

import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParseFieldMatcher;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.xcontent.ToXContent.Params;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.script.Script.ScriptParseException;
import org.elasticsearch.script.ScriptService.ScriptType;

import java.io.IOException;
import java.util.*;

public class ScriptParameterParser {

    public static final String FILE_SUFFIX = "_file";
    public static final String INDEXED_SUFFIX = "_id";

    private final Map<String, ScriptParameterValue> parameterValues = new HashMap<>();
    private Set<ParseField> inlineParameters;
    private Set<ParseField> fileParameters;
    private Set<ParseField> indexedParameters;
    private String lang = null;
    private final DeprecationLogger deprecationLogger;

    public ScriptParameterParser() {
        this(null);
    }

    public ScriptParameterParser(Set<String> parameterNames) {
        ESLogger logger = Loggers.getLogger(getClass());
        deprecationLogger = new DeprecationLogger(logger);
        if (parameterNames == null || parameterNames.isEmpty()) {
            inlineParameters = Collections.singleton(ScriptService.SCRIPT_INLINE);
            fileParameters = Collections.singleton(ScriptService.SCRIPT_FILE);
            indexedParameters = Collections.singleton(ScriptService.SCRIPT_ID);
        } else {
            inlineParameters = new HashSet<>();
            fileParameters = new HashSet<>();
            indexedParameters = new HashSet<>();
            for (String parameterName : parameterNames) {
                if (ParseFieldMatcher.EMPTY.match(parameterName, ScriptService.SCRIPT_LANG)) {
                    throw new IllegalArgumentException("lang is reserved and cannot be used as a parameter name");
                }
                inlineParameters.add(new ParseField(parameterName));
                fileParameters.add(new ParseField(parameterName + FILE_SUFFIX));
                indexedParameters.add(new ParseField(parameterName + INDEXED_SUFFIX));
            }
        }
    }

    public boolean token(String currentFieldName, XContentParser.Token token, XContentParser parser, ParseFieldMatcher parseFieldMatcher) throws IOException {
        if (token == XContentParser.Token.VALUE_STRING) {
            if (parseFieldMatcher.match(currentFieldName, ScriptService.SCRIPT_LANG)) {
                lang  = parser.text();
                return true;
            } else {
                for (ParseField parameter : inlineParameters) {
                    if (parseFieldMatcher.match(currentFieldName, parameter)) {
                        String coreParameterName = parameter.getPreferredName();
                        putParameterValue(coreParameterName, parser.textOrNull(), ScriptType.INLINE);
                        return true;
                    }
                }
                for (ParseField parameter : fileParameters) {
                    if (parseFieldMatcher.match(currentFieldName, parameter)) {
                        String coreParameterName = parameter.getPreferredName().replace(FILE_SUFFIX, "");
                        putParameterValue(coreParameterName, parser.textOrNull(), ScriptType.FILE);
                        return true;
                    }
                }
                for (ParseField parameter : indexedParameters) {
                    if (parseFieldMatcher.match(currentFieldName, parameter)) {
                        String coreParameterName = parameter.getPreferredName().replace(INDEXED_SUFFIX, "");
                        putParameterValue(coreParameterName, parser.textOrNull(), ScriptType.INDEXED);
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public void parseConfig(Map<String, Object> config, boolean removeMatchedEntries, ParseFieldMatcher parseFieldMatcher) {
        for (Iterator<Map.Entry<String, Object>> itr = config.entrySet().iterator(); itr.hasNext();) {
            Map.Entry<String, Object> entry = itr.next();
            String parameterName = entry.getKey();
            Object parameterValue = entry.getValue();
            if (parseFieldMatcher.match(parameterName, ScriptService.SCRIPT_LANG)) {
                if (parameterValue instanceof String || parameterValue == null) {
                    lang = (String) parameterValue;
                    if (removeMatchedEntries) {
                        itr.remove();
                    }
                } else {
                    throw new ScriptParseException("Value must be of type String: [" + parameterName + "]");
                }
            } else {
                for (ParseField parameter : inlineParameters) {
                    if (parseFieldMatcher.match(parameterName, parameter)) {
                        String coreParameterName = parameter.getPreferredName();
                        String stringValue;
                        if (parameterValue instanceof String) {
                            stringValue = (String) parameterValue;
                        } else {
                            throw new ScriptParseException("Value must be of type String: [" + parameterName + "]");
                        }
                        putParameterValue(coreParameterName, stringValue, ScriptType.INLINE);
                        if (removeMatchedEntries) {
                            itr.remove();
                        }
                    }
                }
                for (ParseField parameter : fileParameters) {
                    if (parseFieldMatcher.match(parameterName, parameter)) {
                        String coreParameterName = parameter.getPreferredName().replace(FILE_SUFFIX, "");;
                        String stringValue;
                        if (parameterValue instanceof String) {
                            stringValue = (String) parameterValue;
                        } else {
                            throw new ScriptParseException("Value must be of type String: [" + parameterName + "]");
                        }
                        putParameterValue(coreParameterName, stringValue, ScriptType.FILE);
                        if (removeMatchedEntries) {
                            itr.remove();
                        }
                    }
                }
                for (ParseField parameter : indexedParameters) {
                    if (parseFieldMatcher.match(parameterName, parameter)) {
                        String coreParameterName = parameter.getPreferredName().replace(INDEXED_SUFFIX, "");
                        String stringValue = null;
                        if (parameterValue instanceof String) {
                            stringValue = (String) parameterValue;
                        } else {
                            throw new ScriptParseException("Value must be of type String: [" + parameterName + "]");
                        }
                        putParameterValue(coreParameterName, stringValue, ScriptType.INDEXED);
                        if (removeMatchedEntries) {
                            itr.remove();
                        }
                    }
                }
            }
        }
    }

    private void putParameterValue(String coreParameterName, String script, ScriptType scriptType) {
        if (parameterValues.get(coreParameterName) == null) {
            parameterValues.put(coreParameterName, new ScriptParameterValue(script, scriptType));
        } else {
            throw new ScriptParseException("Only one of [" + coreParameterName + ", " + coreParameterName
                    + FILE_SUFFIX + ", " + coreParameterName + INDEXED_SUFFIX + "] is allowed.");
        }
    }

    public void parseParams(Params params) {
        lang = params.param(ScriptService.SCRIPT_LANG.getPreferredName());
        for (ParseField parameter : inlineParameters) {
            String value = params.param(parameter.getPreferredName());
            if (value != null) {
                String coreParameterName = parameter.getPreferredName();
                putParameterValue(coreParameterName, value, ScriptType.INLINE);
                
            }
        }
        for (ParseField parameter : fileParameters) {
            String value = params.param(parameter.getPreferredName());
            if (value != null) {
                String coreParameterName = parameter.getPreferredName().replace(FILE_SUFFIX, "");
                putParameterValue(coreParameterName, value, ScriptType.FILE);
                
            }
        }
        for (ParseField parameter : indexedParameters) {
            String value = params.param(parameter.getPreferredName());
            if (value != null) {
                String coreParameterName = parameter.getPreferredName().replace(INDEXED_SUFFIX, "");
                putParameterValue(coreParameterName, value, ScriptType.INDEXED);
                
            }
        }
    }

    public ScriptParameterValue getDefaultScriptParameterValue() {
        return getScriptParameterValue(ScriptService.SCRIPT_INLINE.getPreferredName());
    }

    public ScriptParameterValue getScriptParameterValue(String parameterName) {
        ScriptParameterValue scriptParameterValue = parameterValues.get(parameterName);
        if (scriptParameterValue != null) {
            deprecationLogger.deprecated("this form of the script api is deprecated and will be removed from the java api in the next "
                    + "major version. See scripting section of the documentation for the new syntax");
        }
        return scriptParameterValue;
    }

    public String lang() {
        return lang;
    }

    public static class ScriptParameterValue {
        private final String script;
        private final ScriptType scriptType;

        public ScriptParameterValue(String script, ScriptType scriptType) {
            this.script = script;
            this.scriptType = scriptType;
        }

        public String script() {
            return script;
        }

        public ScriptType scriptType() {
            return scriptType;
        }
    }
}