/*
 * 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.uima.analysis_engine.impl;

import java.util.ArrayList;

import org.apache.uima.cas.text.Language;

//@formatter:off
/**
 * A set of languages, each represented by a canonical string object
 * The set is stored without any subsumed elements
 * 
 * Instances of this class are shareable
 * Duplicate-on-update strategy
 *   Requires that all update operations to it return the
 *     possibly new RsLangs object, and that calls are always of the form
 *       rsLangInstance = rsLangInstance.[some-update-operation]
 *   Requires that all copy operations set the shared bit:
 *     copiedInstance = origInstance.setShared(); 
 *  
 * A instance marked isShared == true is immutable
 *   Updates cause duplication.
 *   
 * Users store x-unspecified as null for the rsLangs instance
 *   Because of this, users use static methods, passing in as the first argument,
 *   the value of rsLangs, and getting an updated value of rsLangs.
 *     This allows the passed-in value to be null.
 *  
 * Languages kept in canonical form:
 *   duplicates removed
 *   subsumed languages removed
 *   language strings mapped to unique strings (allowing == comparisons)
 * Languages kept in array list, to allow for expansion
 *   Languages not removed, only added (for a given tof)
 */
//@formatter:on
public class RsLangs {

  private ArrayList<String> languages; // set of languages; null means x-unspecified
  private boolean isShared = false; // support copy on update for languages

  private RsLangs() {
  }

  // for instance used to represent x-unspec inside compiled forms, where null cant be used
  static RsLangs createSharableEmpty() {
    RsLangs rsl = new RsLangs();
    rsl.setShared();
    return rsl;
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append(this.getClass().getSimpleName() + " [languages=");
    if (languages != null) {
      for (String l : languages) {
        builder.append(l).append(',');
      }
    }
    builder.append(", isShared=");
    builder.append(isShared);
    builder.append(']');
    return builder.toString();
  }

  static RsLangs createOrNull(String[] languages) {
    return replaceAll(null, languages);
  }

  void setShared() {
    isShared = true;
  }

  static RsLangs createOrNull(RsLangs rsl) {
    if (null == rsl || rsl.languages == null) {
      return null;
    }
    rsl.setShared();
    return rsl;
  }

  // make a copy when needed
  private RsLangs(RsLangs original) {
    languages = (null == original.languages) ? null : new ArrayList<>(original.languages);
  }

  static boolean isEmpty(RsLangs rsl) {
    return rsl == null || rsl.languages == null || rsl.languages.size() == 0;
  }

  /**
   * 
   * @param rsl
   *          may be null (means x-unspec, subsumes all)
   * @param lang
   * @return true if rsl subsumes lang
   */
  static boolean subsumes(RsLangs rsl, String lang) {
    return subsumesCanonical(rsl, RsLang.getCanonicalLanguageString(lang));
  }

  /**
   * 
   * @param lang
   * @return true if any of the rsLangs subsumes the param lang
   */

  // @edu.umd.cs.findbugs.annotations.SuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
  static boolean subsumesCanonical(RsLangs rsl, String lang) {
    if (null == rsl || null == rsl.languages) { // don't test for size() == 0 - that's used by
                                                // replace to indicate empty, not x-unspec
      return true; // x-unspecified subsumes all
    }
    if (null == lang || lang == Language.UNSPECIFIED_LANGUAGE) {
      return false; // x-unspec not subsumed by anything (other than x-unspec)
    }
    String baseLang = getBaseLanguage(lang);
    for (String rsLang : rsl.languages) {
      if (subsumesCanonical(rsLang, lang, baseLang)) {
        return true;
      }
    }
    return false;
  }

  /**
   * 
   * @param rsl
   *          assumed to be not null, not x-unspec
   * @param lang
   *          assumed to be not null, not x-unspec
   * @param baseLang
   * @return true if any of the rsl languages is equal to the lang or the base lang
   */
  private static boolean subsumesCanonical(RsLangs rsl, String lang, String baseLang) {
    for (String rsLang : rsl.languages) {
      if (subsumesCanonical(rsLang, lang, baseLang)) {
        return true;
      }
    }
    return false;
  }

  // @edu.umd.cs.findbugs.annotations.SuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ")
  private static boolean subsumesCanonical(String containingLang, String langToTest,
          String langToTestBase) {
    return containingLang == langToTest || containingLang == langToTestBase;
  }

  /**
   * 
   * @param language
   *          (must not be null)
   * @return the same == language or the base form of the language
   */
  private static String getBaseLanguage(String language) {
    String baseLanguage = language;
    int index = language.indexOf(Language.CANONICAL_LANG_SEPARATOR);
    if (index > -1) {
      baseLanguage = RsLang.getCanonicalLanguageString(language.substring(0, index));
    }
    return baseLanguage;
  }

  /**
   * 
   * @param rsl
   *          could be null meaning current is x-unspecified
   * @param langs
   *          null means x-unspecified
   * @return null (meaning x-unspecified, or an instance of RsLangs
   */
  static RsLangs replaceAll(RsLangs rsl, String[] langs) {
    if (rsl == null || rsl.languages == null) {
      if (langs == null || (langs.length == 0)) { // UIMA-2212
        return null;
      }
      if (rsl == null || rsl.isShared) {
        rsl = new RsLangs();
      }
      rsl.languages = new ArrayList<>(0); // special form means empty, not x-unspec
    }
    return addAll(rsl, langs);
  }

  static RsLangs addAll(RsLangs rsl, String[] langs) {
    if (null == langs || null == rsl || null == rsl.languages) { // because x-unspec subsumes all
      return rsl;
    } else {
      for (String lang : langs) {
        rsl = add(rsl, lang);
      }
      return rsl;
    }
  }

  static RsLangs addAll(RsLangs rsl, RsLangs rsLangs) {
    if (null == rsLangs || null == rsLangs.languages || null == rsl || null == rsl.languages) { // because
                                                                                                // x-unspec
                                                                                                // subsumes
                                                                                                // all
      return rsl;
    }
    for (String lang : rsLangs.languages) {
      rsl = add(rsl, lang);
    }
    return rsl;
  }

  /**
   * add language unless it's subsumed by existing one remove any languages the newly added one
   * subsumes store x-unspec as null
   * 
   * @param rsl
   *          - is not null and has non-null languages array (may be empty)
   * @param lang
   *          - may be null or x-unspec
   */
  static RsLangs add(RsLangs rsl, String lang) {
    lang = RsLang.getCanonicalLanguageString(lang);
    if (lang == Language.UNSPECIFIED_LANGUAGE) {
      return null;
    }
    String baseLang = getBaseLanguage(lang);
    if (!subsumesCanonical(rsl, lang, baseLang)) {
      if (rsl.isShared) {
        rsl = new RsLangs(rsl);
      }
      rsl.removeSubsumedLanguages(lang, baseLang); // remove subsumed lang, but leave as empty list
                                                   // if all removed
      rsl.languages.add(lang);
    }
    return rsl;
  }

  /**
   * Remove languages that are subsumed by the argument If all removed, keep as empty list
   * 
   * @param canonicalLang
   */
  private void removeSubsumedLanguages(String canonicalLang, String baseLang) {
    languages.removeIf(s -> subsumesCanonical(s, canonicalLang, baseLang));
  }

  /**
   * 
   * @param other
   * @return null for empty intersection (null doesn't mean x-unspecified here)
   */
  RsLangs intersect(RsLangs other) {
    if (null == other) {
      return null;
    }

    if (null == languages) { // means x-unspecified, so return the other
      return other;
    }
    if (null == other.languages) { // means x-unspecified, so return the first
      return this;
    }

    RsLangs r = new RsLangs();
    r.languages = new ArrayList<>(0); // creates an empty, not null arraylist

    for (String lang : languages) {
      if (subsumesCanonical(other, lang)) {
        r = add(r, lang); // add langs in other that are subsumed by this
      }
    }
    for (String lang : other.languages) {
      if (subsumesCanonical(this, lang)) { // add langs in this that are subsumed by other
        r = add(r, lang);
      }
    }
    if (r.languages.size() == 0) {
      return null;
    }
    return r;
  }

  static String[] toArray(RsLangs rsl) {
    return (isEmpty(rsl)) ? null : rsl.languages.toArray(new String[rsl.languages.size()]);
  }

  /**
   * Must return the same hashcode regardless of the value of isShared, and treating the values as a
   * set
   */
  @Override
  public int hashCode() {
    int result = 31;
    for (String lang : languages) {
      result += lang.hashCode(); // non-standard, gives same answer regardless of order
    }
    return result;
  }

  /**
   * This must return true ignoring the value of isShared, and treating the lists as a set
   */
  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if ((obj == null) || (getClass() != obj.getClass())) {
      return false;
    }
    RsLangs other = (RsLangs) obj;
    if (languages == null) {
      if (other.languages != null) {
        return false;
      }
    } else {
      if (languages.size() != other.languages.size()) {
        return false;
      }
      for (String lang : languages) {
        if (!other.languages.contains(lang)) {
          return false;
        }
      }
    }
    return true;
  }
}
