/*
 * 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.search.suggest.context;

import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import org.apache.lucene.analysis.PrefixAnalyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.util.automaton.Automata;
import org.apache.lucene.util.automaton.Automaton;
import org.apache.lucene.util.automaton.Operations;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentParser.Token;
import org.elasticsearch.index.mapper.ParseContext;
import org.elasticsearch.index.mapper.ParseContext.Document;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * The {@link CategoryContextMapping} is used to define a {@link ContextMapping} that
 * references a field within a document. The value of the field in turn will be
 * used to setup the suggestions made by the completion suggester.
 */
public class CategoryContextMapping extends ContextMapping {

    protected static final String TYPE = "category";

    private static final String FIELD_FIELDNAME = "path";
    private static final String DEFAULT_FIELDNAME = "_type";

    private static final Iterable<? extends CharSequence> EMPTY_VALUES = Collections.emptyList();

    private final String fieldName;
    private final Iterable<? extends CharSequence> defaultValues;
    private final FieldConfig defaultConfig;

    /**
     * Create a new {@link CategoryContextMapping} with the default field
     * <code>[_type]</code>
     */
    public CategoryContextMapping(String name) {
        this(name, DEFAULT_FIELDNAME, EMPTY_VALUES);
    }

    /**
     * Create a new {@link CategoryContextMapping} with the default field
     * <code>[_type]</code>
     */
    public CategoryContextMapping(String name, String fieldName) {
        this(name, fieldName, EMPTY_VALUES);
    }

    /**
     * Create a new {@link CategoryContextMapping} with the default field
     * <code>[_type]</code>
     */
    public CategoryContextMapping(String name, Iterable<? extends CharSequence> defaultValues) {
        this(name, DEFAULT_FIELDNAME, defaultValues);
    }

    /**
     * Create a new {@link CategoryContextMapping} with the default field
     * <code>[_type]</code>
     */
    public CategoryContextMapping(String name, String fieldName, Iterable<? extends CharSequence> defaultValues) {
        super(TYPE, name);
        this.fieldName = fieldName;
        this.defaultValues = defaultValues;
        this.defaultConfig = new FieldConfig(fieldName, defaultValues, null);
    }

    /**
     * Name of the field used by this {@link CategoryContextMapping}
     */
    public String getFieldName() {
        return fieldName;
    }

    public Iterable<? extends CharSequence> getDefaultValues() {
        return defaultValues;
    }

    @Override
    public FieldConfig defaultConfig() {
        return defaultConfig;
    }

    /**
     * Load the specification of a {@link CategoryContextMapping}
     *
     * @param field
     *            name of the field to use. If <code>null</code> default field
     *            will be used
     * @return new {@link CategoryContextMapping}
     */
    protected static CategoryContextMapping load(String name, Map<String, Object> config) throws ElasticsearchParseException {
        CategoryContextMapping.Builder mapping = new CategoryContextMapping.Builder(name);

        Object fieldName = config.get(FIELD_FIELDNAME);
        Object defaultValues = config.get(FIELD_MISSING);

        if (fieldName != null) {
            mapping.fieldName(fieldName.toString());
            config.remove(FIELD_FIELDNAME);
        }

        if (defaultValues != null) {
            if (defaultValues instanceof Iterable) {
                for (Object value : (Iterable) defaultValues) {
                    mapping.addDefaultValue(value.toString());
                }
            } else {
                mapping.addDefaultValue(defaultValues.toString());
            }
            config.remove(FIELD_MISSING);
        }

        return mapping.build();
    }

    @Override
    protected XContentBuilder toInnerXContent(XContentBuilder builder, Params params) throws IOException {
        if (fieldName != null) {
            builder.field(FIELD_FIELDNAME, fieldName);
        }
        builder.startArray(FIELD_MISSING);
        for (CharSequence value : defaultValues) {
            builder.value(value);
        }
        builder.endArray();
        return builder;
    }

    @Override
    public ContextConfig parseContext(ParseContext parseContext, XContentParser parser) throws IOException, ElasticsearchParseException {
        Token token = parser.currentToken();
        if (token == Token.VALUE_NULL) {
            return new FieldConfig(fieldName, defaultValues, null);
        } else if (token == Token.VALUE_STRING) {
            return new FieldConfig(fieldName, null, Collections.singleton(parser.text()));
        } else if (token == Token.VALUE_NUMBER) {
            return new FieldConfig(fieldName, null, Collections.singleton(parser.text()));
        } else if (token == Token.VALUE_BOOLEAN) {
            return new FieldConfig(fieldName, null, Collections.singleton(parser.text()));
        } else if (token == Token.START_ARRAY) {
            ArrayList<String> values = new ArrayList<>();
            while((token = parser.nextToken()) != Token.END_ARRAY) {
                values.add(parser.text());
            }
            if(values.isEmpty()) {
                throw new ElasticsearchParseException("FieldConfig must contain a least one category");
            }
            return new FieldConfig(fieldName, null, values);
        } else {
            throw new ElasticsearchParseException("FieldConfig must be either [null], a string or a list of strings");
        }
    }

    @Override
    public FieldQuery parseQuery(String name, XContentParser parser) throws IOException, ElasticsearchParseException {
        Iterable<? extends CharSequence> values;
        Token token = parser.currentToken();
        if (token == Token.START_ARRAY) {
            ArrayList<String> list = new ArrayList<>();
            while ((token = parser.nextToken()) != Token.END_ARRAY) {
                list.add(parser.text());
            }
            values = list;
        } else if (token == Token.VALUE_NULL) {
            values = defaultValues;
        } else {
            values = Collections.singleton(parser.text());
        }

        return new FieldQuery(name, values);
    }

    public static FieldQuery query(String name, CharSequence... fieldvalues) {
        return query(name, Arrays.asList(fieldvalues));
    }

    public static FieldQuery query(String name, Iterable<? extends CharSequence> fieldvalues) {
        return new FieldQuery(name, fieldvalues);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof CategoryContextMapping) {
            CategoryContextMapping other = (CategoryContextMapping) obj;
            if (this.fieldName.equals(other.fieldName)) {
                return Iterables.elementsEqual(this.defaultValues, other.defaultValues);
            }
        }
        return false;
    }

    @Override
    public int hashCode() {
        int hashCode = fieldName.hashCode();
        for (CharSequence seq : defaultValues) {
            hashCode = 31 * hashCode + seq.hashCode();
        }
        return hashCode;
    }

    private static class FieldConfig extends ContextConfig {

        private final String fieldname;
        private final Iterable<? extends CharSequence> defaultValues;
        private final Iterable<? extends CharSequence> values;

        public FieldConfig(String fieldname, Iterable<? extends CharSequence> defaultValues, Iterable<? extends CharSequence> values) {
            this.fieldname = fieldname;
            this.defaultValues = defaultValues;
            this.values = values;
        }

        @Override
        protected TokenStream wrapTokenStream(Document doc, TokenStream stream) {
            if (values != null) {
                return new PrefixAnalyzer.PrefixTokenFilter(stream, ContextMapping.SEPARATOR, values);
            // if fieldname is default, BUT our default values are set, we take that one
            } else if ((doc.getFields(fieldname).length == 0 || fieldname.equals(DEFAULT_FIELDNAME)) && defaultValues.iterator().hasNext()) {
                return new PrefixAnalyzer.PrefixTokenFilter(stream, ContextMapping.SEPARATOR, defaultValues);
            } else {
                IndexableField[] fields = doc.getFields(fieldname);
                ArrayList<CharSequence> values = new ArrayList<>(fields.length);
                for (int i = 0; i < fields.length; i++) {
                    values.add(fields[i].stringValue());
                }

                return new PrefixAnalyzer.PrefixTokenFilter(stream, ContextMapping.SEPARATOR, values);
            }
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("FieldConfig(" + fieldname + " = [");
            if (this.values != null && this.values.iterator().hasNext()) {
                sb.append("(").append(Joiner.on(", ").join(this.values.iterator())).append(")");
            }
            if (this.defaultValues != null && this.defaultValues.iterator().hasNext()) {
                sb.append(" default(").append(Joiner.on(", ").join(this.defaultValues.iterator())).append(")");
            }
            return sb.append("])").toString();
        }

    }

    private static class FieldQuery extends ContextQuery {

        private final Iterable<? extends CharSequence> values;

        public FieldQuery(String name, Iterable<? extends CharSequence> values) {
            super(name);
            this.values = values;
        }

        @Override
        public Automaton toAutomaton() {
            List<Automaton> automatons = new ArrayList<>();
            for (CharSequence value : values) {
                automatons.add(Automata.makeString(value.toString()));
            }
            return Operations.union(automatons);
        }

        @Override
        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
            builder.startArray(name);
            for (CharSequence value : values) {
                builder.value(value);
            }
            builder.endArray();
            return builder;
        }
    }

    public static class Builder extends ContextBuilder<CategoryContextMapping> {

        private String fieldname;
        private List<CharSequence> defaultValues = new ArrayList<>();

        public Builder(String name) {
            this(name, DEFAULT_FIELDNAME);
        }

        public Builder(String name, String fieldname) {
            super(name);
            this.fieldname = fieldname;
        }

        /**
         * Set the name of the field to use
         */
        public Builder fieldName(String fieldname) {
            this.fieldname = fieldname;
            return this;
        }

        /**
         * Add value to the default values of the mapping
         */
        public Builder addDefaultValue(CharSequence defaultValue) {
            this.defaultValues.add(defaultValue);
            return this;
        }

        /**
         * Add set of default values to the mapping
         */
        public Builder addDefaultValues(CharSequence... defaultValues) {
            for (CharSequence defaultValue : defaultValues) {
                this.defaultValues.add(defaultValue);
            }
            return this;
        }

        /**
         * Add set of default values to the mapping
         */
        public Builder addDefaultValues(Iterable<? extends CharSequence> defaultValues) {
            for (CharSequence defaultValue : defaultValues) {
                this.defaultValues.add(defaultValue);
            }
            return this;
        }

        @Override
        public CategoryContextMapping build() {
            return new CategoryContextMapping(name, fieldname, defaultValues);
        }
    }
}