/*
 * 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.index.mapper;

import com.carrotsearch.hppc.ObjectOpenHashSet;
import com.google.common.base.Charsets;
import com.google.common.collect.*;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.AnalyzerWrapper;
import org.apache.lucene.index.Term;
import org.apache.lucene.queries.FilterClause;
import org.apache.lucene.queries.TermFilter;
import org.apache.lucene.queries.TermsFilter;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.Filter;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.ElasticsearchGenerationException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.collect.ImmutableOpenMap;
import org.elasticsearch.common.compress.CompressedString;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.lucene.search.AndFilter;
import org.elasticsearch.common.lucene.search.NotFilter;
import org.elasticsearch.common.lucene.search.XBooleanFilter;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.env.FailedToResolveConfigException;
import org.elasticsearch.index.AbstractIndexComponent;
import org.elasticsearch.index.Index;
import org.elasticsearch.index.analysis.AnalysisService;
import org.elasticsearch.index.codec.docvaluesformat.DocValuesFormatService;
import org.elasticsearch.index.codec.postingsformat.PostingsFormatService;
import org.elasticsearch.index.fielddata.IndexFieldDataService;
import org.elasticsearch.index.mapper.internal.TypeFieldMapper;
import org.elasticsearch.index.mapper.object.ObjectMapper;
import org.elasticsearch.index.search.nested.NonNestedDocsFilter;
import org.elasticsearch.index.settings.IndexSettings;
import org.elasticsearch.index.similarity.SimilarityLookupService;
import org.elasticsearch.indices.InvalidTypeNameException;
import org.elasticsearch.indices.TypeMissingException;
import org.elasticsearch.percolator.PercolatorService;

import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

import static org.elasticsearch.common.collect.MapBuilder.newMapBuilder;
import static org.elasticsearch.index.mapper.DocumentMapper.MergeFlags.mergeFlags;

/**
 *
 */
public class MapperService extends AbstractIndexComponent implements Iterable<DocumentMapper> {

    public static final String DEFAULT_MAPPING = "_default_";
    private static ObjectOpenHashSet<String> META_FIELDS = ObjectOpenHashSet.from(
            "_uid", "_id", "_type", "_all", "_analyzer", "_boost", "_parent", "_routing", "_index",
            "_size", "_timestamp", "_ttl"
    );

    private final AnalysisService analysisService;
    private final IndexFieldDataService fieldDataService;

    /**
     * Will create types automatically if they do not exists in the mapping definition yet
     */
    private final boolean dynamic;

    private volatile String defaultMappingSource;
    private volatile String defaultPercolatorMappingSource;

    private volatile Map<String, DocumentMapper> mappers = ImmutableMap.of();

    private final Object typeMutex = new Object();
    private final Object mappersMutex = new Object();

    private final FieldMappersLookup fieldMappers = new FieldMappersLookup();
    private volatile ImmutableOpenMap<String, ObjectMappers> fullPathObjectMappers = ImmutableOpenMap.of();
    private boolean hasNested = false; // updated dynamically to true when a nested object is added

    private final DocumentMapperParser documentParser;

    private final InternalFieldMapperListener fieldMapperListener = new InternalFieldMapperListener();
    private final InternalObjectMapperListener objectMapperListener = new InternalObjectMapperListener();

    private final SmartIndexNameSearchAnalyzer searchAnalyzer;
    private final SmartIndexNameSearchQuoteAnalyzer searchQuoteAnalyzer;

    private final List<DocumentTypeListener> typeListeners = new CopyOnWriteArrayList<DocumentTypeListener>();

    @Inject
    public MapperService(Index index, @IndexSettings Settings indexSettings, Environment environment, AnalysisService analysisService, IndexFieldDataService fieldDataService,
                         PostingsFormatService postingsFormatService, DocValuesFormatService docValuesFormatService, SimilarityLookupService similarityLookupService) {
        super(index, indexSettings);
        this.analysisService = analysisService;
        this.fieldDataService = fieldDataService;
        this.documentParser = new DocumentMapperParser(index, indexSettings, analysisService, postingsFormatService, docValuesFormatService, similarityLookupService);
        this.searchAnalyzer = new SmartIndexNameSearchAnalyzer(analysisService.defaultSearchAnalyzer());
        this.searchQuoteAnalyzer = new SmartIndexNameSearchQuoteAnalyzer(analysisService.defaultSearchQuoteAnalyzer());

        this.dynamic = componentSettings.getAsBoolean("dynamic", true);
        String defaultMappingLocation = componentSettings.get("default_mapping_location");
        URL defaultMappingUrl;
        if (defaultMappingLocation == null) {
            try {
                defaultMappingUrl = environment.resolveConfig("default-mapping.json");
            } catch (FailedToResolveConfigException e) {
                // not there, default to the built in one
                defaultMappingUrl = indexSettings.getClassLoader().getResource("org/elasticsearch/index/mapper/default-mapping.json");
                if (defaultMappingUrl == null) {
                    defaultMappingUrl = MapperService.class.getClassLoader().getResource("org/elasticsearch/index/mapper/default-mapping.json");
                }
            }
        } else {
            try {
                defaultMappingUrl = environment.resolveConfig(defaultMappingLocation);
            } catch (FailedToResolveConfigException e) {
                // not there, default to the built in one
                try {
                    defaultMappingUrl = new File(defaultMappingLocation).toURI().toURL();
                } catch (MalformedURLException e1) {
                    throw new FailedToResolveConfigException("Failed to resolve dynamic mapping location [" + defaultMappingLocation + "]");
                }
            }
        }

        if (defaultMappingUrl == null) {
            logger.info("failed to find default-mapping.json in the classpath, using the default template");
            defaultMappingSource = "{\n" +
                    "    \"_default_\":{\n" +
                    "    }\n" +
                    "}";
        } else {
            try {
                defaultMappingSource = Streams.copyToString(new InputStreamReader(defaultMappingUrl.openStream(), Charsets.UTF_8));
            } catch (IOException e) {
                throw new MapperException("Failed to load default mapping source from [" + defaultMappingLocation + "]", e);
            }
        }

        String percolatorMappingLocation = componentSettings.get("default_percolator_mapping_location");
        URL percolatorMappingUrl = null;
        if (percolatorMappingLocation != null) {
            try {
                percolatorMappingUrl = environment.resolveConfig(percolatorMappingLocation);
            } catch (FailedToResolveConfigException e) {
                // not there, default to the built in one
                try {
                    percolatorMappingUrl = new File(percolatorMappingLocation).toURI().toURL();
                } catch (MalformedURLException e1) {
                    throw new FailedToResolveConfigException("Failed to resolve default percolator mapping location [" + percolatorMappingLocation + "]");
                }
            }
        }
        if (percolatorMappingUrl != null) {
            try {
                defaultPercolatorMappingSource = Streams.copyToString(new InputStreamReader(percolatorMappingUrl.openStream(), Charsets.UTF_8));
            } catch (IOException e) {
                throw new MapperException("Failed to load default percolator mapping source from [" + percolatorMappingUrl + "]", e);
            }
        } else {
            defaultPercolatorMappingSource = "{\n" +
                    //"    \"" + PercolatorService.TYPE_NAME + "\":{\n" +
                    "    \"" + "_default_" + "\":{\n" +
                    "        \"_id\" : {\"index\": \"not_analyzed\"}," +
                    "        \"properties\" : {\n" +
                    "            \"query\" : {\n" +
                    "                \"type\" : \"object\",\n" +
                    "                \"enabled\" : false\n" +
                    "            }\n" +
                    "        }\n" +
                    "    }\n" +
                    "}";
        }

        if (logger.isTraceEnabled()) {
            logger.trace("using dynamic[{}], default mapping: default_mapping_location[{}], loaded_from[{}] and source[{}], default percolator mapping: location[{}], loaded_from[{}] and source[{}]", dynamic, defaultMappingLocation, defaultMappingUrl, defaultMappingSource, percolatorMappingLocation, percolatorMappingUrl, defaultPercolatorMappingSource);
        } else if (logger.isDebugEnabled()) {
            logger.debug("using dynamic[{}], default mapping: default_mapping_location[{}], loaded_from[{}], default percolator mapping: location[{}], loaded_from[{}]", dynamic, defaultMappingLocation, defaultMappingUrl, percolatorMappingLocation, percolatorMappingUrl);
        }
    }

    public void close() {
        for (DocumentMapper documentMapper : mappers.values()) {
            documentMapper.close();
        }
    }

    public boolean hasNested() {
        return this.hasNested;
    }

    @Override
    public UnmodifiableIterator<DocumentMapper> iterator() {
        return Iterators.unmodifiableIterator(mappers.values().iterator());
    }

    public AnalysisService analysisService() {
        return this.analysisService;
    }

    public DocumentMapperParser documentMapperParser() {
        return this.documentParser;
    }

    public void addTypeListener(DocumentTypeListener listener) {
        typeListeners.add(listener);
    }

    public void removeTypeListener(DocumentTypeListener listener) {
        typeListeners.remove(listener);
    }

    public DocumentMapper merge(String type, CompressedString mappingSource, boolean applyDefault) {
        if (DEFAULT_MAPPING.equals(type)) {
            // verify we can parse it
            DocumentMapper mapper = documentParser.parseCompressed(type, mappingSource);
            // still add it as a document mapper so we have it registered and, for example, persisted back into
            // the cluster meta data if needed, or checked for existence
            synchronized (typeMutex) {
                mappers = newMapBuilder(mappers).put(type, mapper).map();
            }
            try {
                defaultMappingSource = mappingSource.string();
            } catch (IOException e) {
                throw new ElasticsearchGenerationException("failed to un-compress", e);
            }
            return mapper;
        } else {
            return merge(parse(type, mappingSource, applyDefault));
        }
    }

    // never expose this to the outside world, we need to reparse the doc mapper so we get fresh
    // instances of field mappers to properly remove existing doc mapper
    private DocumentMapper merge(DocumentMapper mapper) {
        synchronized (typeMutex) {
            if (mapper.type().length() == 0) {
                throw new InvalidTypeNameException("mapping type name is empty");
            }
            if (mapper.type().charAt(0) == '_') {
                throw new InvalidTypeNameException("mapping type name [" + mapper.type() + "] can't start with '_'");
            }
            if (mapper.type().contains("#")) {
                throw new InvalidTypeNameException("mapping type name [" + mapper.type() + "] should not include '#' in it");
            }
            if (mapper.type().contains(",")) {
                throw new InvalidTypeNameException("mapping type name [" + mapper.type() + "] should not include ',' in it");
            }
            if (mapper.type().contains(".") && !PercolatorService.TYPE_NAME.equals(mapper.type())) {
                logger.warn("Type [{}] contains a '.', it is recommended not to include it within a type name", mapper.type());
            }
            // we can add new field/object mappers while the old ones are there
            // since we get new instances of those, and when we remove, we remove
            // by instance equality
            DocumentMapper oldMapper = mappers.get(mapper.type());

            if (oldMapper != null) {
                DocumentMapper.MergeResult result = oldMapper.merge(mapper, mergeFlags().simulate(false));
                if (result.hasConflicts()) {
                    // TODO: What should we do???
                    if (logger.isDebugEnabled()) {
                        logger.debug("merging mapping for type [{}] resulted in conflicts: [{}]", mapper.type(), Arrays.toString(result.conflicts()));
                    }
                }
                fieldDataService.onMappingUpdate();
                return oldMapper;
            } else {
                FieldMapperListener.Aggregator fieldMappersAgg = new FieldMapperListener.Aggregator();
                mapper.traverse(fieldMappersAgg);
                addFieldMappers(fieldMappersAgg.mappers);
                mapper.addFieldMapperListener(fieldMapperListener, false);

                ObjectMapperListener.Aggregator objectMappersAgg = new ObjectMapperListener.Aggregator();
                mapper.traverse(objectMappersAgg);
                addObjectMappers(objectMappersAgg.mappers.toArray(new ObjectMapper[objectMappersAgg.mappers.size()]));
                mapper.addObjectMapperListener(objectMapperListener, false);

                for (DocumentTypeListener typeListener : typeListeners) {
                    typeListener.beforeCreate(mapper);
                }
                mappers = newMapBuilder(mappers).put(mapper.type(), mapper).map();
                return mapper;
            }
        }
    }

    private void addObjectMappers(ObjectMapper[] objectMappers) {
        synchronized (mappersMutex) {
            ImmutableOpenMap.Builder<String, ObjectMappers> fullPathObjectMappers = ImmutableOpenMap.builder(this.fullPathObjectMappers);
            for (ObjectMapper objectMapper : objectMappers) {
                ObjectMappers mappers = fullPathObjectMappers.get(objectMapper.fullPath());
                if (mappers == null) {
                    mappers = new ObjectMappers(objectMapper);
                } else {
                    mappers = mappers.concat(objectMapper);
                }
                fullPathObjectMappers.put(objectMapper.fullPath(), mappers);
                // update the hasNested flag
                if (objectMapper.nested().isNested()) {
                    hasNested = true;
                }
            }
            this.fullPathObjectMappers = fullPathObjectMappers.build();
        }
    }

    private void addFieldMappers(Iterable<FieldMapper>  fieldMappers) {
        synchronized (mappersMutex) {
            this.fieldMappers.addNewMappers(fieldMappers);
        }
    }

    public void remove(String type) {
        synchronized (typeMutex) {
            DocumentMapper docMapper = mappers.get(type);
            if (docMapper == null) {
                return;
            }
            docMapper.close();
            mappers = newMapBuilder(mappers).remove(type).map();
            removeObjectAndFieldMappers(docMapper);
            for (DocumentTypeListener typeListener : typeListeners) {
                typeListener.afterRemove(docMapper);
            }
        }
    }

    private void removeObjectAndFieldMappers(DocumentMapper docMapper) {
        synchronized (mappersMutex) {
            fieldMappers.removeMappers(docMapper.mappers());

            ImmutableOpenMap.Builder<String, ObjectMappers> fullPathObjectMappers = ImmutableOpenMap.builder(this.fullPathObjectMappers);
            for (ObjectMapper mapper : docMapper.objectMappers().values()) {
                ObjectMappers mappers = fullPathObjectMappers.get(mapper.fullPath());
                if (mappers != null) {
                    mappers = mappers.remove(mapper);
                    if (mappers.isEmpty()) {
                        fullPathObjectMappers.remove(mapper.fullPath());
                    } else {
                        fullPathObjectMappers.put(mapper.fullPath(), mappers);
                    }
                }
            }

            this.fullPathObjectMappers = fullPathObjectMappers.build();
        }
    }

    /**
     * Just parses and returns the mapper without adding it, while still applying default mapping.
     */
    public DocumentMapper parse(String mappingType, CompressedString mappingSource) throws MapperParsingException {
        return parse(mappingType, mappingSource, true);
    }

    public DocumentMapper parse(String mappingType, CompressedString mappingSource, boolean applyDefault) throws MapperParsingException {
        String defaultMappingSource;
        if (PercolatorService.TYPE_NAME.equals(mappingType)) {
            defaultMappingSource = this.defaultPercolatorMappingSource;
        } else {
            defaultMappingSource = this.defaultMappingSource;
        }
        return documentParser.parseCompressed(mappingType, mappingSource, applyDefault ? defaultMappingSource : null);
    }

    public boolean hasMapping(String mappingType) {
        return mappers.containsKey(mappingType);
    }

    public Collection<String> types() {
        return mappers.keySet();
    }

    public DocumentMapper documentMapper(String type) {
        return mappers.get(type);
    }

    public DocumentMapper documentMapperWithAutoCreate(String type) {
        DocumentMapper mapper = mappers.get(type);
        if (mapper != null) {
            return mapper;
        }
        if (!dynamic) {
            throw new TypeMissingException(index, type, "trying to auto create mapping, but dynamic mapping is disabled");
        }
        // go ahead and dynamically create it
        synchronized (typeMutex) {
            mapper = mappers.get(type);
            if (mapper != null) {
                return mapper;
            }
            merge(type, null, true);
            return mappers.get(type);
        }
    }

    /**
     * A filter for search. If a filter is required, will return it, otherwise, will return <tt>null</tt>.
     */
    @Nullable
    public Filter searchFilter(String... types) {
        boolean filterPercolateType = hasMapping(PercolatorService.TYPE_NAME);
        if (types != null && filterPercolateType) {
            for (String type : types) {
                if (PercolatorService.TYPE_NAME.equals(type)) {
                    filterPercolateType = false;
                    break;
                }
            }
        }
        Filter excludePercolatorType = null;
        if (filterPercolateType) {
            excludePercolatorType = new NotFilter(documentMapper(PercolatorService.TYPE_NAME).typeFilter());
        }

        if (types == null || types.length == 0) {
            if (hasNested && filterPercolateType) {
                return new AndFilter(ImmutableList.of(excludePercolatorType, NonNestedDocsFilter.INSTANCE));
            } else if (hasNested) {
                return NonNestedDocsFilter.INSTANCE;
            } else if (filterPercolateType) {
                return excludePercolatorType;
            } else {
                return null;
            }
        }
        // if we filter by types, we don't need to filter by non nested docs
        // since they have different types (starting with __)
        if (types.length == 1) {
            DocumentMapper docMapper = documentMapper(types[0]);
            if (docMapper == null) {
                return new TermFilter(new Term(types[0]));
            }
            return docMapper.typeFilter();
        }
        // see if we can use terms filter
        boolean useTermsFilter = true;
        for (String type : types) {
            DocumentMapper docMapper = documentMapper(type);
            if (docMapper == null) {
                useTermsFilter = false;
                break;
            }
            if (!docMapper.typeMapper().fieldType().indexed()) {
                useTermsFilter = false;
                break;
            }
        }

        if (useTermsFilter) {
            BytesRef[] typesBytes = new BytesRef[types.length];
            for (int i = 0; i < typesBytes.length; i++) {
                typesBytes[i] = new BytesRef(types[i]);
            }
            TermsFilter termsFilter = new TermsFilter(TypeFieldMapper.NAME, typesBytes);
            if (filterPercolateType) {
                return new AndFilter(ImmutableList.of(excludePercolatorType, termsFilter));
            } else {
                return termsFilter;
            }
        } else {
            // Current bool filter requires that at least one should clause matches, even with a must clause.
            XBooleanFilter bool = new XBooleanFilter();
            for (String type : types) {
                DocumentMapper docMapper = documentMapper(type);
                if (docMapper == null) {
                    bool.add(new FilterClause(new TermFilter(new Term(TypeFieldMapper.NAME, type)), BooleanClause.Occur.SHOULD));
                } else {
                    bool.add(new FilterClause(docMapper.typeFilter(), BooleanClause.Occur.SHOULD));
                }
            }
            if (filterPercolateType) {
                bool.add(excludePercolatorType, BooleanClause.Occur.MUST);
            }

            return bool;
        }
    }

    /**
     * Returns {@link FieldMappers} for all the {@link FieldMapper}s that are registered
     * under the given name across all the different {@link DocumentMapper} types.
     *
     * @param name The name to return all the {@link FieldMappers} for across all {@link DocumentMapper}s.
     * @return All the {@link FieldMappers} for across all {@link DocumentMapper}s
     */
    public FieldMappers name(String name) {
        return fieldMappers.name(name);
    }

    /**
     * Returns {@link FieldMappers} for all the {@link FieldMapper}s that are registered
     * under the given indexName across all the different {@link DocumentMapper} types.
     *
     * @param indexName The indexName to return all the {@link FieldMappers} for across all {@link DocumentMapper}s.
     * @return All the {@link FieldMappers} across all {@link DocumentMapper}s for the given indexName.
     */
    public FieldMappers indexName(String indexName) {
        return fieldMappers.indexName(indexName);
    }

    /**
     * Returns the {@link FieldMappers} of all the {@link FieldMapper}s that are
     * registered under the give fullName across all the different {@link DocumentMapper} types.
     *
     * @param fullName The full name
     * @return All teh {@link FieldMappers} across all the {@link DocumentMapper}s for the given fullName.
     */
    public FieldMappers fullName(String fullName) {
        return fieldMappers.fullName(fullName);
    }

    /**
     * Returns objects mappers based on the full path of the object.
     */
    public ObjectMappers objectMapper(String path) {
        return fullPathObjectMappers.get(path);
    }

    /**
     * Returns all the fields that match the given pattern, with an optional narrowing
     * based on a list of types.
     */
    public Set<String> simpleMatchToIndexNames(String pattern, @Nullable String[] types) {
        if (types == null || types.length == 0) {
            return simpleMatchToIndexNames(pattern);
        }
        if (types.length == 1 && types[0].equals("_all")) {
            return simpleMatchToIndexNames(pattern);
        }
        if (!Regex.isSimpleMatchPattern(pattern)) {
            return ImmutableSet.of(pattern);
        }
        Set<String> fields = Sets.newHashSet();
        for (String type : types) {
            DocumentMapper possibleDocMapper = mappers.get(type);
            if (possibleDocMapper != null) {
                for (String indexName : possibleDocMapper.mappers().simpleMatchToIndexNames(pattern)) {
                    fields.add(indexName);
                }
            }
        }
        return fields;
    }

    /**
     * Returns all the fields that match the given pattern. If the pattern is prefixed with a type
     * then the fields will be returned with a type prefix.
     */
    public Set<String> simpleMatchToIndexNames(String pattern) {
        if (!Regex.isSimpleMatchPattern(pattern)) {
            return ImmutableSet.of(pattern);
        }
        int dotIndex = pattern.indexOf('.');
        if (dotIndex != -1) {
            String possibleType = pattern.substring(0, dotIndex);
            DocumentMapper possibleDocMapper = mappers.get(possibleType);
            if (possibleDocMapper != null) {
                Set<String> typedFields = Sets.newHashSet();
                for (String indexName : possibleDocMapper.mappers().simpleMatchToIndexNames(pattern)) {
                    typedFields.add(possibleType + "." + indexName);
                }
                return typedFields;
            }
        }
        return fieldMappers.simpleMatchToIndexNames(pattern);
    }

    public SmartNameObjectMapper smartNameObjectMapper(String smartName, @Nullable String[] types) {
        if (types == null || types.length == 0) {
            return smartNameObjectMapper(smartName);
        }
        if (types.length == 1 && types[0].equals("_all")) {
            return smartNameObjectMapper(smartName);
        }
        for (String type : types) {
            DocumentMapper possibleDocMapper = mappers.get(type);
            if (possibleDocMapper != null) {
                ObjectMapper mapper = possibleDocMapper.objectMappers().get(smartName);
                if (mapper != null) {
                    return new SmartNameObjectMapper(mapper, possibleDocMapper);
                }
            }
        }
        // did not find one, see if its prefixed by type
        int dotIndex = smartName.indexOf('.');
        if (dotIndex != -1) {
            String possibleType = smartName.substring(0, dotIndex);
            DocumentMapper possibleDocMapper = mappers.get(possibleType);
            if (possibleDocMapper != null) {
                String possiblePath = smartName.substring(dotIndex + 1);
                ObjectMapper mapper = possibleDocMapper.objectMappers().get(possiblePath);
                if (mapper != null) {
                    return new SmartNameObjectMapper(mapper, possibleDocMapper);
                }
            }
        }
        // did not explicitly find one under the types provided, or prefixed by type...
        return null;
    }

    public SmartNameObjectMapper smartNameObjectMapper(String smartName) {
        int dotIndex = smartName.indexOf('.');
        if (dotIndex != -1) {
            String possibleType = smartName.substring(0, dotIndex);
            DocumentMapper possibleDocMapper = mappers.get(possibleType);
            if (possibleDocMapper != null) {
                String possiblePath = smartName.substring(dotIndex + 1);
                ObjectMapper mapper = possibleDocMapper.objectMappers().get(possiblePath);
                if (mapper != null) {
                    return new SmartNameObjectMapper(mapper, possibleDocMapper);
                }
            }
        }
        ObjectMappers mappers = objectMapper(smartName);
        if (mappers != null) {
            return new SmartNameObjectMapper(mappers.mapper(), null);
        }
        return null;
    }

    /**
     * Same as {@link #smartNameFieldMappers(String)} but returns the first field mapper for it. Returns
     * <tt>null</tt> if there is none.
     */
    public FieldMapper smartNameFieldMapper(String smartName) {
        FieldMappers fieldMappers = smartNameFieldMappers(smartName);
        if (fieldMappers != null) {
            return fieldMappers.mapper();
        }
        return null;
    }

    public FieldMapper smartNameFieldMapper(String smartName, @Nullable String[] types) {
        FieldMappers fieldMappers = smartNameFieldMappers(smartName, types);
        if (fieldMappers != null) {
            return fieldMappers.mapper();
        }
        return null;
    }

    public FieldMappers smartNameFieldMappers(String smartName, @Nullable String[] types) {
        if (types == null || types.length == 0) {
            return smartNameFieldMappers(smartName);
        }
        for (String type : types) {
            DocumentMapper documentMapper = mappers.get(type);
            // we found a mapper
            if (documentMapper != null) {
                // see if we find a field for it
                FieldMappers mappers = documentMapper.mappers().smartName(smartName);
                if (mappers != null) {
                    return mappers;
                }
            }
        }
        // did not find explicit field in the type provided, see if its prefixed with type
        int dotIndex = smartName.indexOf('.');
        if (dotIndex != -1) {
            String possibleType = smartName.substring(0, dotIndex);
            DocumentMapper possibleDocMapper = mappers.get(possibleType);
            if (possibleDocMapper != null) {
                String possibleName = smartName.substring(dotIndex + 1);
                FieldMappers mappers = possibleDocMapper.mappers().smartName(possibleName);
                if (mappers != null) {
                    return mappers;
                }
            }
        }
        // we did not find the field mapping in any of the types, so don't go and try to find
        // it in other types...
        return null;
    }

    /**
     * Same as {@link #smartName(String)}, except it returns just the field mappers.
     */
    public FieldMappers smartNameFieldMappers(String smartName) {
        int dotIndex = smartName.indexOf('.');
        if (dotIndex != -1) {
            String possibleType = smartName.substring(0, dotIndex);
            DocumentMapper possibleDocMapper = mappers.get(possibleType);
            if (possibleDocMapper != null) {
                String possibleName = smartName.substring(dotIndex + 1);
                FieldMappers mappers = possibleDocMapper.mappers().smartName(possibleName);
                if (mappers != null) {
                    return mappers;
                }
            }
        }
        FieldMappers mappers = fullName(smartName);
        if (mappers != null) {
            return mappers;
        }
        mappers = indexName(smartName);
        if (mappers != null) {
            return mappers;
        }
        return name(smartName);
    }

    public SmartNameFieldMappers smartName(String smartName, @Nullable String[] types) {
        if (types == null || types.length == 0) {
            return smartName(smartName);
        }
        if (types.length == 1 && types[0].equals("_all")) {
            return smartName(smartName);
        }
        for (String type : types) {
            DocumentMapper documentMapper = mappers.get(type);
            // we found a mapper
            if (documentMapper != null) {
                // see if we find a field for it
                FieldMappers mappers = documentMapper.mappers().smartName(smartName);
                if (mappers != null) {
                    return new SmartNameFieldMappers(this, mappers, documentMapper, false);
                }
            }
        }
        // did not find explicit field in the type provided, see if its prefixed with type
        int dotIndex = smartName.indexOf('.');
        if (dotIndex != -1) {
            String possibleType = smartName.substring(0, dotIndex);
            DocumentMapper possibleDocMapper = mappers.get(possibleType);
            if (possibleDocMapper != null) {
                String possibleName = smartName.substring(dotIndex + 1);
                FieldMappers mappers = possibleDocMapper.mappers().smartName(possibleName);
                if (mappers != null) {
                    return new SmartNameFieldMappers(this, mappers, possibleDocMapper, true);
                }
            }
        }
        // we did not find the field mapping in any of the types, so don't go and try to find
        // it in other types...
        return null;
    }

    /**
     * Returns smart field mappers based on a smart name. A smart name is one that can optioannly be prefixed
     * with a type (and then a '.'). If it is, then the {@link MapperService.SmartNameFieldMappers}
     * will have the doc mapper set.
     * <p/>
     * <p>It also (without the optional type prefix) try and find the {@link FieldMappers} for the specific
     * name. It will first try to find it based on the full name (with the dots if its a compound name). If
     * it is not found, will try and find it based on the indexName (which can be controlled in the mapping),
     * and last, will try it based no the name itself.
     * <p/>
     * <p>If nothing is found, returns null.
     */
    public SmartNameFieldMappers smartName(String smartName) {
        int dotIndex = smartName.indexOf('.');
        if (dotIndex != -1) {
            String possibleType = smartName.substring(0, dotIndex);
            DocumentMapper possibleDocMapper = mappers.get(possibleType);
            if (possibleDocMapper != null) {
                String possibleName = smartName.substring(dotIndex + 1);
                FieldMappers mappers = possibleDocMapper.mappers().smartName(possibleName);
                if (mappers != null) {
                    return new SmartNameFieldMappers(this, mappers, possibleDocMapper, true);
                }
            }
        }
        FieldMappers fieldMappers = fullName(smartName);
        if (fieldMappers != null) {
            return new SmartNameFieldMappers(this, fieldMappers, null, false);
        }
        fieldMappers = indexName(smartName);
        if (fieldMappers != null) {
            return new SmartNameFieldMappers(this, fieldMappers, null, false);
        }
        fieldMappers = name(smartName);
        if (fieldMappers != null) {
            return new SmartNameFieldMappers(this, fieldMappers, null, false);
        }
        return null;
    }

    public Analyzer searchAnalyzer() {
        return this.searchAnalyzer;
    }

    public Analyzer searchQuoteAnalyzer() {
        return this.searchQuoteAnalyzer;
    }

    public Analyzer fieldSearchAnalyzer(String field) {
        return this.searchAnalyzer.getWrappedAnalyzer(field);
    }

    public Analyzer fieldSearchQuoteAnalyzer(String field) {
        return this.searchQuoteAnalyzer.getWrappedAnalyzer(field);
    }


    /**
     * Resolves the closest inherited {@link ObjectMapper} that is nested.
     */
    public ObjectMapper resolveClosestNestedObjectMapper(String fieldName) {
        int indexOf = fieldName.lastIndexOf('.');
        if (indexOf == -1) {
            return null;
        } else {
            do {
                String objectPath = fieldName.substring(0, indexOf);
                ObjectMappers objectMappers = objectMapper(objectPath);
                if (objectMappers == null) {
                    return null;
                }

                if (objectMappers.hasNested()) {
                    for (ObjectMapper objectMapper : objectMappers) {
                        if (objectMapper.nested().isNested()) {
                            return objectMapper;
                        }
                    }
                }

                indexOf = objectPath.lastIndexOf('.');
            } while (indexOf != -1);
        }

        return null;
    }

    /**
     * @return Whether a field is a metadata field.
     */
    public static boolean isMetadataField(String fieldName) {
        return META_FIELDS.contains(fieldName);
    }

    public static class SmartNameObjectMapper {
        private final ObjectMapper mapper;
        private final DocumentMapper docMapper;

        public SmartNameObjectMapper(ObjectMapper mapper, @Nullable DocumentMapper docMapper) {
            this.mapper = mapper;
            this.docMapper = docMapper;
        }

        public boolean hasMapper() {
            return mapper != null;
        }

        public ObjectMapper mapper() {
            return mapper;
        }

        public boolean hasDocMapper() {
            return docMapper != null;
        }

        public DocumentMapper docMapper() {
            return docMapper;
        }
    }

    public static class SmartNameFieldMappers {
        private final MapperService mapperService;
        private final FieldMappers fieldMappers;
        private final DocumentMapper docMapper;
        private final boolean explicitTypeInName;

        public SmartNameFieldMappers(MapperService mapperService, FieldMappers fieldMappers, @Nullable DocumentMapper docMapper, boolean explicitTypeInName) {
            this.mapperService = mapperService;
            this.fieldMappers = fieldMappers;
            this.docMapper = docMapper;
            this.explicitTypeInName = explicitTypeInName;
        }

        /**
         * Has at least one mapper for the field.
         */
        public boolean hasMapper() {
            return !fieldMappers.isEmpty();
        }

        /**
         * The first mapper for the smart named field.
         */
        public FieldMapper mapper() {
            return fieldMappers.mapper();
        }

        /**
         * All the field mappers for the smart name field.
         */
        public FieldMappers fieldMappers() {
            return fieldMappers;
        }

        /**
         * If the smart name was a typed field, with a type that we resolved, will return
         * <tt>true</tt>.
         */
        public boolean hasDocMapper() {
            return docMapper != null;
        }

        /**
         * If the smart name was a typed field, with a type that we resolved, will return
         * the document mapper for it.
         */
        public DocumentMapper docMapper() {
            return docMapper;
        }

        /**
         * Returns <tt>true</tt> if the type is explicitly specified in the name.
         */
        public boolean explicitTypeInName() {
            return this.explicitTypeInName;
        }

        public boolean explicitTypeInNameWithDocMapper() {
            return explicitTypeInName && docMapper != null;
        }

        /**
         * The best effort search analyzer associated with this field.
         */
        public Analyzer searchAnalyzer() {
            if (hasMapper()) {
                Analyzer analyzer = mapper().searchAnalyzer();
                if (analyzer != null) {
                    return analyzer;
                }
            }
            if (docMapper != null && docMapper.searchAnalyzer() != null) {
                return docMapper.searchAnalyzer();
            }
            return mapperService.searchAnalyzer();
        }

        public Analyzer searchQuoteAnalyzer() {
            if (hasMapper()) {
                Analyzer analyzer = mapper().searchQuoteAnalyzer();
                if (analyzer != null) {
                    return analyzer;
                }
            }
            if (docMapper != null && docMapper.searchQuotedAnalyzer() != null) {
                return docMapper.searchQuotedAnalyzer();
            }
            return mapperService.searchQuoteAnalyzer();
        }
    }

    final class SmartIndexNameSearchAnalyzer extends AnalyzerWrapper {

        private final Analyzer defaultAnalyzer;

        SmartIndexNameSearchAnalyzer(Analyzer defaultAnalyzer) {
            this.defaultAnalyzer = defaultAnalyzer;
        }

        @Override
        protected Analyzer getWrappedAnalyzer(String fieldName) {
            int dotIndex = fieldName.indexOf('.');
            if (dotIndex != -1) {
                String possibleType = fieldName.substring(0, dotIndex);
                DocumentMapper possibleDocMapper = mappers.get(possibleType);
                if (possibleDocMapper != null) {
                    return possibleDocMapper.mappers().searchAnalyzer();
                }
            }
            FieldMappers mappers = fieldMappers.fullName(fieldName);
            if (mappers != null && mappers.mapper() != null && mappers.mapper().searchAnalyzer() != null) {
                return mappers.mapper().searchAnalyzer();
            }

            mappers = fieldMappers.indexName(fieldName);
            if (mappers != null && mappers.mapper() != null && mappers.mapper().searchAnalyzer() != null) {
                return mappers.mapper().searchAnalyzer();
            }
            return defaultAnalyzer;
        }

        @Override
        protected TokenStreamComponents wrapComponents(String fieldName, TokenStreamComponents components) {
            return components;
        }
    }

    final class SmartIndexNameSearchQuoteAnalyzer extends AnalyzerWrapper {

        private final Analyzer defaultAnalyzer;

        SmartIndexNameSearchQuoteAnalyzer(Analyzer defaultAnalyzer) {
            this.defaultAnalyzer = defaultAnalyzer;
        }

        @Override
        protected Analyzer getWrappedAnalyzer(String fieldName) {
            int dotIndex = fieldName.indexOf('.');
            if (dotIndex != -1) {
                String possibleType = fieldName.substring(0, dotIndex);
                DocumentMapper possibleDocMapper = mappers.get(possibleType);
                if (possibleDocMapper != null) {
                    return possibleDocMapper.mappers().searchQuoteAnalyzer();
                }
            }
            FieldMappers mappers = fieldMappers.fullName(fieldName);
            if (mappers != null && mappers.mapper() != null && mappers.mapper().searchQuoteAnalyzer() != null) {
                return mappers.mapper().searchQuoteAnalyzer();
            }

            mappers = fieldMappers.indexName(fieldName);
            if (mappers != null && mappers.mapper() != null && mappers.mapper().searchQuoteAnalyzer() != null) {
                return mappers.mapper().searchQuoteAnalyzer();
            }
            return defaultAnalyzer;
        }

        @Override
        protected TokenStreamComponents wrapComponents(String fieldName, TokenStreamComponents components) {
            return components;
        }
    }

    class InternalFieldMapperListener extends FieldMapperListener {
        @Override
        public void fieldMapper(FieldMapper fieldMapper) {
            addFieldMappers(Arrays.asList(fieldMapper));
        }

        @Override
        public void fieldMappers(Iterable<FieldMapper>  fieldMappers) {
            addFieldMappers(fieldMappers);
        }
    }

    class InternalObjectMapperListener extends ObjectMapperListener {
        @Override
        public void objectMapper(ObjectMapper objectMapper) {
            addObjectMappers(new ObjectMapper[]{objectMapper});
        }

        @Override
        public void objectMappers(ObjectMapper... objectMappers) {
            addObjectMappers(objectMappers);
        }
    }
}
