/*
 * 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 org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.DocValuesTermsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.loader.SettingsLoader;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.fielddata.IndexFieldData;
import org.elasticsearch.index.fielddata.plain.ParentChildIndexFieldData;
import org.elasticsearch.index.query.QueryShardContext;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static org.elasticsearch.common.xcontent.support.XContentMapValues.nodeMapValue;

public class ParentFieldMapper extends MetadataFieldMapper {

    public static final String NAME = "_parent";
    public static final String CONTENT_TYPE = "_parent";

    public static class Defaults {
        public static final String NAME = ParentFieldMapper.NAME;

        public static final ParentFieldType FIELD_TYPE = new ParentFieldType();

        static {
            FIELD_TYPE.setTokenized(false);
            FIELD_TYPE.setIndexOptions(IndexOptions.NONE);
            FIELD_TYPE.setHasDocValues(true);
            FIELD_TYPE.setDocValuesType(DocValuesType.SORTED);
            FIELD_TYPE.freeze();
        }
    }

    public static class Builder extends MetadataFieldMapper.Builder<Builder, ParentFieldMapper> {

        private String parentType;

        private final String documentType;

        public Builder(String documentType) {
            super(Defaults.NAME, new ParentFieldType(Defaults.FIELD_TYPE, documentType), Defaults.FIELD_TYPE);
            this.documentType = documentType;
            builder = this;
        }

        public Builder type(String type) {
            this.parentType = type;
            return builder;
        }

        public Builder eagerGlobalOrdinals(boolean eagerGlobalOrdinals) {
            fieldType().setEagerGlobalOrdinals(eagerGlobalOrdinals);
            return builder;
        }

        @Override
        public ParentFieldMapper build(BuilderContext context) {
            if (parentType == null) {
                throw new MapperParsingException("[_parent] field mapping must contain the [type] option");
            }
            name = joinField(parentType);
            setupFieldType(context);
            return new ParentFieldMapper(createParentJoinFieldMapper(documentType, context), fieldType, parentType, context.indexSettings());
        }
    }

    public static class TypeParser implements MetadataFieldMapper.TypeParser {
        private static final ParseField FIELDDATA = new ParseField("fielddata").withAllDeprecated("eager_global_ordinals");
        @Override
        public MetadataFieldMapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException {
            Builder builder = new Builder(parserContext.type());
            for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
                Map.Entry<String, Object> entry = iterator.next();
                String fieldName = entry.getKey();
                Object fieldNode = entry.getValue();
                if (fieldName.equals("type")) {
                    builder.type(fieldNode.toString());
                    iterator.remove();
                } else if (FIELDDATA.match(fieldName)) {
                    // for bw compat only
                    Map<String, String> fieldDataSettings = SettingsLoader.Helper.loadNestedFromMap(nodeMapValue(fieldNode, "fielddata"));
                    if (fieldDataSettings.containsKey("loading")) {
                        builder.eagerGlobalOrdinals("eager_global_ordinals".equals(fieldDataSettings.get("loading")));
                    }
                    iterator.remove();
                } else if (fieldName.equals("eager_global_ordinals")) {
                    builder.eagerGlobalOrdinals(XContentMapValues.nodeBooleanValue(fieldNode, "eager_global_ordinals"));
                    iterator.remove();
                }
            }
            return builder;
        }

        @Override
        public MetadataFieldMapper getDefault(MappedFieldType fieldType, ParserContext context) {
            final Settings indexSettings = context.mapperService().getIndexSettings().getSettings();
            final String typeName = context.type();
            KeywordFieldMapper parentJoinField = createParentJoinFieldMapper(typeName, new BuilderContext(indexSettings, new ContentPath(0)));
            MappedFieldType childJoinFieldType = new ParentFieldType(Defaults.FIELD_TYPE, typeName);
            childJoinFieldType.setName(ParentFieldMapper.NAME);
            return new ParentFieldMapper(parentJoinField, childJoinFieldType, null, indexSettings);
        }
    }

    static KeywordFieldMapper createParentJoinFieldMapper(String docType, BuilderContext context) {
        KeywordFieldMapper.Builder parentJoinField = new KeywordFieldMapper.Builder(joinField(docType));
        parentJoinField.indexOptions(IndexOptions.NONE);
        parentJoinField.docValues(true);
        parentJoinField.fieldType().setDocValuesType(DocValuesType.SORTED);
        return parentJoinField.build(context);
    }

    static final class ParentFieldType extends MappedFieldType {

        final String documentType;

        ParentFieldType() {
            documentType = null;
            setEagerGlobalOrdinals(true);
        }

        ParentFieldType(ParentFieldType ref, String documentType) {
            super(ref);
            this.documentType = documentType;
        }

        private ParentFieldType(ParentFieldType ref) {
            super(ref);
            this.documentType = ref.documentType;
        }

        @Override
        public MappedFieldType clone() {
            return new ParentFieldType(this);
        }

        @Override
        public String typeName() {
            return CONTENT_TYPE;
        }

        @Override
        public Query termQuery(Object value, @Nullable QueryShardContext context) {
            return termsQuery(Collections.singletonList(value), context);
        }

        @Override
        public Query termsQuery(List values, @Nullable QueryShardContext context) {
            BytesRef[] ids = new BytesRef[values.size()];
            for (int i = 0; i < ids.length; i++) {
                ids[i] = BytesRefs.toBytesRef(values.get(i));
            }
            BooleanQuery.Builder query = new BooleanQuery.Builder();
            query.add(new DocValuesTermsQuery(name(), ids), BooleanClause.Occur.MUST);
            query.add(new TermQuery(new Term(TypeFieldMapper.NAME, documentType)), BooleanClause.Occur.FILTER);
            return query.build();
        }

        @Override
        public IndexFieldData.Builder fielddataBuilder() {
            return new ParentChildIndexFieldData.Builder();
        }
    }

    private final String parentType;
    // has no impact of field data settings, is just here for creating a join field,
    // the parent field mapper in the child type pointing to this type determines the field data settings for this join field
    private final KeywordFieldMapper parentJoinField;

    private ParentFieldMapper(KeywordFieldMapper parentJoinField, MappedFieldType childJoinFieldType, String parentType, Settings indexSettings) {
        super(NAME, childJoinFieldType, Defaults.FIELD_TYPE, indexSettings);
        this.parentType = parentType;
        this.parentJoinField = parentJoinField;
    }

    public MappedFieldType getParentJoinFieldType() {
        return parentJoinField.fieldType();
    }

    public String type() {
        return parentType;
    }

    @Override
    public void preParse(ParseContext context) throws IOException {
    }

    @Override
    public void postParse(ParseContext context) throws IOException {
        parse(context);
    }

    @Override
    protected void parseCreateField(ParseContext context, List<IndexableField> fields) throws IOException {
        boolean parent = context.docMapper().isParent(context.sourceToParse().type());
        if (parent) {
            fields.add(new SortedDocValuesField(parentJoinField.fieldType().name(), new BytesRef(context.sourceToParse().id())));
        }

        if (!active()) {
            return;
        }

        if (context.parser().currentName() != null && context.parser().currentName().equals(Defaults.NAME)) {
            // we are in the parsing of _parent phase
            String parentId = context.parser().text();
            context.sourceToParse().parent(parentId);
            fields.add(new SortedDocValuesField(fieldType.name(), new BytesRef(parentId)));
        } else {
            // otherwise, we are running it post processing of the xcontent
            String parsedParentId = context.doc().get(Defaults.NAME);
            if (context.sourceToParse().parent() != null) {
                String parentId = context.sourceToParse().parent();
                if (parsedParentId == null) {
                    if (parentId == null) {
                        throw new MapperParsingException("No parent id provided, not within the document, and not externally");
                    }
                    // we did not add it in the parsing phase, add it now
                    fields.add(new SortedDocValuesField(fieldType.name(), new BytesRef(parentId)));
                } else if (parentId != null && !parsedParentId.equals(Uid.createUid(parentType, parentId))) {
                    throw new MapperParsingException("Parent id mismatch, document value is [" + Uid.createUid(parsedParentId).id() + "], while external value is [" + parentId + "]");
                }
            }
        }
        // we have parent mapping, yet no value was set, ignore it...
    }

    public static String joinField(String parentType) {
        return ParentFieldMapper.NAME + "#" + parentType;
    }

    @Override
    protected String contentType() {
        return CONTENT_TYPE;
    }

    @Override
    public Iterator<Mapper> iterator() {
        return Collections.<Mapper>singleton(parentJoinField).iterator();
    }

    @Override
    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
        if (!active()) {
            return builder;
        }
        boolean includeDefaults = params.paramAsBoolean("include_defaults", false);

        builder.startObject(CONTENT_TYPE);
        builder.field("type", parentType);
        if (includeDefaults || fieldType().eagerGlobalOrdinals() != defaultFieldType.eagerGlobalOrdinals()) {
            builder.field("eager_global_ordinals", fieldType().eagerGlobalOrdinals());
        }
        builder.endObject();
        return builder;
    }

    @Override
    protected void doMerge(Mapper mergeWith, boolean updateAllTypes) {
        super.doMerge(mergeWith, updateAllTypes);
        ParentFieldMapper fieldMergeWith = (ParentFieldMapper) mergeWith;
        if (Objects.equals(parentType, fieldMergeWith.parentType) == false) {
            throw new IllegalArgumentException("The _parent field's type option can't be changed: [" + parentType + "]->[" + fieldMergeWith.parentType + "]");
        }

        List<String> conflicts = new ArrayList<>();
        fieldType().checkCompatibility(fieldMergeWith.fieldType, conflicts, true);
        if (conflicts.isEmpty() == false) {
            throw new IllegalArgumentException("Merge conflicts: " + conflicts);
        }

        if (active()) {
            fieldType = fieldMergeWith.fieldType.clone();
        }
    }

    /**
     * @return Whether the _parent field is actually configured.
     */
    public boolean active() {
        return parentType != null;
    }

}
