/*
 * 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 org.apache.solr.ltr.model;

import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.Explanation;
import org.apache.solr.ltr.feature.Feature;
import org.apache.solr.ltr.norm.Normalizer;

/**
 * A scoring model that wraps the other model.
 *
 * <p>This model loads a model from an external resource during the initialization.
 * The way of fetching the wrapped model is depended on
 * the implementation of {@link WrapperModel#fetchModelMap()}.
 *
 * <p>This model doesn't hold the actual parameters of the wrapped model,
 * thus it can manage large models which are difficult to upload to ZooKeeper.
 *
 * <p>Example configuration:
 * <pre>{
    "class": "...",
    "name": "myModelName",
    "params": {
        ...
    }
 }</pre>
 *
 * <p>NOTE: no "features" are configured in the wrapper model
 * because the wrapped model's features will be used instead.
 * Also note that if a "store" is configured for the wrapper
 * model then it must match the "store" of the wrapped model.
 */
public abstract class WrapperModel extends AdapterModel {

  protected LTRScoringModel model;

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + ((model == null) ? 0 : model.hashCode());
    result = prime * result + ((solrResourceLoader == null) ? 0 : solrResourceLoader.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!super.equals(obj)) return false;
    if (getClass() != obj.getClass()) return false;
    WrapperModel other = (WrapperModel) obj;
    if (model == null) {
      if (other.model != null) return false;
    } else if (!model.equals(other.model)) return false;
    if (solrResourceLoader == null) {
      if (other.solrResourceLoader != null) return false;
    } else if (!solrResourceLoader.equals(other.solrResourceLoader)) return false;
    return true;
  }

  public WrapperModel(String name, List<Feature> features, List<Normalizer> norms, String featureStoreName,
                      List<Feature> allFeatures, Map<String, Object> params) {
    super(name, features, norms, featureStoreName, allFeatures, params);
  }

  @Override
  protected void validate() throws ModelException {
    if (!features.isEmpty()) {
      throw new ModelException("features must be empty for the wrapper model " + name);
    }
    if (!norms.isEmpty()) {
      throw new ModelException("norms must be empty for the wrapper model " + name);
    }

    if (model != null) {
      super.validate();
      model.validate();
      // check feature store names match
      final String wrappedFeatureStoreName = model.getFeatureStoreName();
      if (wrappedFeatureStoreName == null || !wrappedFeatureStoreName.equals(this.getFeatureStoreName())) {
        throw new ModelException("wrapper feature store name ("+this.getFeatureStoreName() +")"
            + " must match the "
            + "wrapped feature store name ("+wrappedFeatureStoreName+")");
      }
    }
  }

  public void updateModel(LTRScoringModel model) {
    this.model = model;
    validate();
  }

  /*
   * The child classes must implement how to fetch the definition of the wrapped model.
   */
  public abstract Map<String, Object> fetchModelMap() throws ModelException;

  @Override
  public List<Normalizer> getNorms() {
    return model.getNorms();
  }

  @Override
  public List<Feature> getFeatures() {
    return model.getFeatures();
  }

  @Override
  public Collection<Feature> getAllFeatures() {
    return model.getAllFeatures();
  }

  @Override
  public float score(float[] modelFeatureValuesNormalized) {
    return model.score(modelFeatureValuesNormalized);
  }

  @Override
  public Explanation explain(LeafReaderContext context, int doc, float finalScore,
                             List<Explanation> featureExplanations) {
    return model.explain(context, doc, finalScore, featureExplanations);
  }

  @Override
  public void normalizeFeaturesInPlace(float[] modelFeatureValues) {
    model.normalizeFeaturesInPlace(modelFeatureValues);
  }

  @Override
  public Explanation getNormalizerExplanation(Explanation e, int idx) {
    return model.getNormalizerExplanation(e, idx);
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder(getClass().getSimpleName());
    sb.append("(name=").append(getName());
    sb.append(",model=(").append(model.toString()).append(")");

    return sb.toString();
  }

}
