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

import static org.apache.uima.util.Level.SEVERE;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import org.antlr.runtime.CommonToken;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.uima.UIMAFramework;
import org.apache.uima.UimaContext;
import org.apache.uima.cas.CAS;
import org.apache.uima.cas.CASException;
import org.apache.uima.cas.Type;
import org.apache.uima.cas.TypeSystem;
import org.apache.uima.cas.text.AnnotationFS;
import org.apache.uima.fit.factory.TypeSystemDescriptionFactory;
import org.apache.uima.fit.internal.ResourceManagerFactory;
import org.apache.uima.resource.ResourceAccessException;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.resource.ResourceManager;
import org.apache.uima.resource.metadata.TypeDescription;
import org.apache.uima.resource.metadata.TypeSystemDescription;
import org.apache.uima.ruta.action.AbstractRutaAction;
import org.apache.uima.ruta.block.RutaBlock;
import org.apache.uima.ruta.condition.AbstractRutaCondition;
import org.apache.uima.ruta.engine.RutaEngine;
import org.apache.uima.ruta.expression.IRutaExpression;
import org.apache.uima.ruta.expression.annotation.IAnnotationExpression;
import org.apache.uima.ruta.expression.bool.IBooleanExpression;
import org.apache.uima.ruta.expression.bool.SimpleBooleanListExpression;
import org.apache.uima.ruta.expression.list.ListExpression;
import org.apache.uima.ruta.expression.number.INumberExpression;
import org.apache.uima.ruta.expression.number.SimpleNumberListExpression;
import org.apache.uima.ruta.expression.resource.LiteralWordListExpression;
import org.apache.uima.ruta.expression.resource.LiteralWordTableExpression;
import org.apache.uima.ruta.expression.resource.StringWordListExpression;
import org.apache.uima.ruta.expression.resource.StringWordTableExpression;
import org.apache.uima.ruta.expression.resource.WordListExpression;
import org.apache.uima.ruta.expression.resource.WordTableExpression;
import org.apache.uima.ruta.expression.string.IStringExpression;
import org.apache.uima.ruta.expression.string.SimpleStringListExpression;
import org.apache.uima.ruta.expression.type.ITypeExpression;
import org.apache.uima.ruta.expression.type.SimpleTypeExpression;
import org.apache.uima.ruta.expression.type.SimpleTypeListExpression;
import org.apache.uima.ruta.resource.CSVTable;
import org.apache.uima.ruta.resource.MultiTreeWordList;
import org.apache.uima.ruta.resource.RutaResourceLoader;
import org.apache.uima.ruta.resource.RutaTable;
import org.apache.uima.ruta.resource.RutaWordList;
import org.apache.uima.ruta.resource.TreeWordList;
import org.apache.uima.ruta.rule.MatchContext;
import org.apache.uima.ruta.rule.RuleElement;
import org.apache.uima.ruta.rule.RuleMatch;
import org.apache.uima.ruta.verbalize.RutaVerbalizer;
import org.apache.uima.util.InvalidXMLException;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

public class RutaEnvironment {

  private static final String DOCUMENT = "Document";

  private final Object annotationTypeDummy = new Object();

  private Map<String, Type> types;

  private Map<String, RutaWordList> wordLists;

  private Map<String, RutaTable> tables;

  private RutaBlock owner;

  /**
   * Mapping from short type name (e.g. {@code W}) to their disambiguated long type names (e.g.
   * {@code org.apache.uima.ruta.type.W}).
   */
  private Map<String, String> namespaces;

  /**
   * Mapping from ambiguous short type names to all their possible long type names.
   */
  private Map<String, Set<String>> ambiguousTypeAlias;

  /**
   * Set of imported typesystems.
   */
  private Set<String> typesystems;

  /**
   * Set of imported scripts.
   */
  private Set<String> scripts;

  /**
   * An alias from a long to a short name.
   */
  private static class Alias {
    final String longName;

    final String shortName;

    Alias(String longName, String shortName) {
      this.longName = longName;
      this.shortName = shortName;
    }
  }

  /**
   * Types that are imported in the environment. Keys are type system descriptors and values are
   * aliased types.
   */
  private Map<String, List<Alias>> typeImports;

  /**
   * Packages that are imported in the environment without a typesystem specification.
   *
   * Keys are package names and values are aliases. An empty string as alias means that all types
   * from the package should be imported in the default namespace.
   */
  private Map<String, List<String>> packageImports;

  /**
   * Set of types that are declared in the script.
   */
  private Set<String> declaredAnnotationTypes;

  private Map<String, Object> variableValues;

  private Map<String, Object> tempVariableValues;

  private Map<String, Class<?>> variableTypes;

  private Map<String, Class<?>> availableTypes;

  private Map<String, Class<?>> variableGenericTypes;

  private Map<String, Class<?>> availableListTypes;

  private Map<String, Triple<Map<String, String>, List<AbstractRutaCondition>, Set<String>>> macroConditions;

  private Map<String, Triple<Map<String, String>, List<AbstractRutaAction>, Set<String>>> macroActions;

  private String[] resourcePaths = null;

  private CAS cas;

  private Map<String, Object> initializedVariables;

  private ResourceManager resourceManager;

  private Map<String, String> variableAliases;

  private RutaVerbalizer verbalizer = new RutaVerbalizer();

  private Pattern typeIgnorePattern;

  public RutaEnvironment(RutaBlock owner) {
    this.owner = owner;

    types = new HashMap<>();
    namespaces = new HashMap<>();
    ambiguousTypeAlias = new HashMap<>();
    typesystems = new HashSet<>();
    scripts = new HashSet<>();
    typeImports = new HashMap<>();
    packageImports = new HashMap<>();
    declaredAnnotationTypes = new HashSet<>();
    wordLists = new HashMap<>();
    tables = new HashMap<>();
    variableValues = new HashMap<>();
    tempVariableValues = new HashMap<>();
    variableTypes = new HashMap<>();
    variableGenericTypes = new HashMap<>();
    macroConditions = new HashMap<>();
    macroActions = new HashMap<>();
    availableTypes = new HashMap<>();
    availableTypes.put(RutaConstants.RUTA_VARIABLE_ANNOTATION, AnnotationFS.class);
    availableTypes.put("INT", Integer.class);
    availableTypes.put("STRING", String.class);
    availableTypes.put("DOUBLE", Double.class);
    availableTypes.put("FLOAT", Float.class);
    availableTypes.put("BOOLEAN", Boolean.class);
    availableTypes.put("TYPE", Type.class);
    availableTypes.put("CONDITION", AbstractRutaCondition.class);
    availableTypes.put("ACTION", AbstractRutaAction.class);
    availableTypes.put("WORDLIST", RutaWordList.class);
    availableTypes.put("WORDTABLE", RutaTable.class);
    availableTypes.put(RutaConstants.RUTA_VARIABLE_ANNOTATION_LIST, List.class);
    availableTypes.put("BOOLEANLIST", List.class);
    availableTypes.put("INTLIST", List.class);
    availableTypes.put("DOUBLELIST", List.class);
    availableTypes.put("FLOATLIST", List.class);
    availableTypes.put("STRINGLIST", List.class);
    availableTypes.put(RutaConstants.RUTA_VARIABLE_TYPE_LIST, List.class);
    availableListTypes = new HashMap<>();
    availableListTypes.put(RutaConstants.RUTA_VARIABLE_ANNOTATION_LIST, AnnotationFS.class);
    availableListTypes.put("BOOLEANLIST", Boolean.class);
    availableListTypes.put("INTLIST", Integer.class);
    availableListTypes.put("DOUBLELIST", Double.class);
    availableListTypes.put("FLOATLIST", Float.class);
    availableListTypes.put("STRINGLIST", String.class);
    availableListTypes.put(RutaConstants.RUTA_VARIABLE_TYPE_LIST, Type.class);
    resourcePaths = getResourcePaths();
    initializedVariables = new HashMap<>();
    variableAliases = new HashMap<>();

    // Always import BasicTypeSystem
    addTypeSystem("org.apache.uima.ruta.engine.BasicTypeSystem");
  }

  /**
   * Import short type names.
   *
   * @param cas
   *          Cas to initialize the types for.
   * @param strictImport
   *          Specify whether all types should be imported (false) or only types
   */
  public void initializeTypes(CAS cas, boolean strictImport) {
    this.cas = cas;
    try {
      if (strictImport) {
        importDeclaredTypes(cas.getTypeSystem());
        importDeclaredTypesystems(cas.getTypeSystem());
        importTypeAliases(cas.getTypeSystem());
        importPackageAliases(cas.getTypeSystem());
        importDeclaredScripts(cas.getTypeSystem());
      } else {
        // import all types known to the cas
        importAllTypes(cas.getTypeSystem());
        importTypeAliases(cas.getTypeSystem());
        importPackageAliases(cas.getTypeSystem());
      }

      ambiguousTypeAlias.remove(DOCUMENT);
      namespaces.remove(DOCUMENT);
      Type documentType = cas.getTypeSystem().getType(CAS.TYPE_NAME_DOCUMENT_ANNOTATION);
      addType(DOCUMENT, documentType);
      addType(documentType.getShortName(), documentType);

      Type annotationType = cas.getJCas().getCasType(org.apache.uima.jcas.tcas.Annotation.type);
      addType("Annotation", annotationType);
    } catch (CASException e) {
      UIMAFramework.getLogger(getClass()).log(SEVERE, "Cannot initialize types.", e);
    } catch (InvalidXMLException e) {
      UIMAFramework.getLogger(getClass()).log(SEVERE, "Cannot initialize types.", e);
    }

  }

  /**
   * Imports all types that are known to a type system.
   *
   * @param ts
   *          Type system to import.
   * @throws CASException
   */
  private void importAllTypes(TypeSystem ts) throws CASException {
    Type topType = ts.getTopType();
    if (topType != null) {
      List<Type> list = ts.getProperlySubsumedTypes(topType);
      for (Type type : list) {
        addType(type);
      }
    }
  }

  /**
   * Import all types that are declared by the script.
   *
   * @param casTS
   *          Type system containing all known types.
   * @throws InvalidXMLException
   *           When import cannot be resolved.
   */
  private void importDeclaredTypes(TypeSystem casTS) throws InvalidXMLException {
    for (String name : declaredAnnotationTypes) {
      Type type = casTS.getType(name);
      if (type != null) {
        addType(type);
      } else {
        throw new RuntimeException("Type '" + name + "' declared in script not found in CAS");
      }
    }
  }

  /**
   * Import all typesystems that are imported in the script.
   *
   * @param casTS
   *          Type system containing all known types.
   * @throws InvalidXMLException
   *           When import cannot be resolved.
   */
  private void importDeclaredTypesystems(TypeSystem casTS) throws InvalidXMLException {
    String[] descriptors = typesystems.toArray(new String[typesystems.size()]);
    TypeSystemDescription ts = TypeSystemDescriptionFactory
            .createTypeSystemDescription(descriptors);
    ts.resolveImports(getResourceManager());
    for (TypeDescription td : ts.getTypes()) {
      Type type = casTS.getType(td.getName());
      if (type != null) {
        addType(type);
      } else {
        throw new RuntimeException(
                "Type '" + td.getName() + "' from imported type system not found in CAS");
      }
    }
  }

  /**
   * Import all already initialized types of imported scripts.
   *
   * @param casTS
   *          Type system containing all known types.
   * @throws InvalidXMLException
   *           When import cannot be resolved.
   */
  private void importDeclaredScripts(TypeSystem casTS) throws InvalidXMLException {

    RutaModule script = owner.getScript();
    for (String eachImportedScript : scripts) {
      RutaModule importedModule = script.getScript(eachImportedScript);
      RutaEnvironment importedEnvironment = importedModule.getRootBlock().getEnvironment();
      Map<String, Type> importedTypeMap = importedEnvironment.getTypes();
      Map<String, String> importedNamespaces = importedEnvironment.getNamespaces();
      Set<Entry<String, String>> entrySet = importedNamespaces.entrySet();
      for (Entry<String, String> entry : entrySet) {
        if (!ownsType(entry.getValue()) && !StringUtils.equals(entry.getKey(), DOCUMENT)) {
          Type type = importedTypeMap.get(entry.getValue());
          addType(entry.getKey(), type);
        }
      }
      // TODO import also wordlists and variables?
    }
  }

  /**
   * Imports all type aliases.
   *
   * @param casTS
   *          Cas type system.
   */
  private void importTypeAliases(TypeSystem casTS) {
    for (List<Alias> aliases : typeImports.values()) {
      for (Alias alias : aliases) {
        Type type = casTS.getType(alias.longName);
        if (type == null) {
          throw new RuntimeException("Type '" + alias.longName + "' not found");
        }
        addType(alias.shortName, casTS.getType(alias.longName));
      }
    }
  }

  /**
   * Import all packages that are imported by the script.
   *
   * @param casTS
   *          Type system containing all known types.
   */
  private void importPackageAliases(TypeSystem casTS) {
    Iterator<Type> iter = casTS.getTypeIterator();
    while (iter.hasNext()) {
      Type type = iter.next();
      String name = type.getName();
      String pkg = name.substring(0, Math.max(name.lastIndexOf('.'), 0));
      List<String> aliases = packageImports.get(pkg);
      if (aliases != null) {
        for (String alias : aliases) {
          if (alias.isEmpty()) {
            addType(type);
          } else {
            addType(alias + "." + type.getShortName(), type);
          }
        }
      }
    }
  }

  public String[] getResourcePaths() {
    if (resourcePaths == null) {
      RutaBlock parent = owner.getParent();
      if (parent != null) {
        return parent.getEnvironment().getResourcePaths();
      }
    }
    return resourcePaths;
  }

  public void setResourcePaths(String[] resourcePaths) {
    this.resourcePaths = resourcePaths;
  }

  public boolean ownsType(String match) {
    match = expand(match);
    return types.keySet().contains(match);
  }

  private String expand(String string) {
    String complete = namespaces.get(string);
    if (complete == null) {
      if (!string.contains(".")) {
        complete = namespaces.get(string);
        if (complete == null) {
          complete = string;
        }
      } else {
        complete = string;
      }
    }
    return complete;
  }

  /**
   * Resolves an annotation type.
   *
   * @param match
   *          Annotation type to resolve.
   * @return Resolved annotation type or null if match is unknown.
   * @throws IllegalArgumentException
   *           When {@code match} is ambiguous.
   */
  public Type getType(String match) {
    // make sure that match is not ambiguous
    Set<String> ambiguousTargets = ambiguousTypeAlias.get(match);
    if (ambiguousTargets != null) {
      StringBuilder message = new StringBuilder(match);
      message.append(" is ambiguous, use one of the following instead : ");
      for (String target : ambiguousTargets) {
        message.append(target).append(' ');
      }
      throw new IllegalArgumentException(message.toString());
    }

    // try to resolve match
    String expanded = expand(match);
    Type type = types.get(expanded);
    if (type == null) {
      RutaBlock parent = owner.getParent();
      if (parent != null) {
        type = parent.getEnvironment().getType(match);
      }
    }
    return type;
  }

  public void addType(String shortName, Type type) {
    String name = type.getName();

    if (ignoreType(name)) {
      return;
    }

    importType(name, shortName);
    types.put(name, type);
  }

  private boolean ignoreType(String name) {
    if (typeIgnorePattern == null) {
      return false;
    }
    return typeIgnorePattern.matcher(name).matches();
  }

  public void addType(Type type) {
    addType(type.getShortName(), type);
  }

  public void declareType(String name) {
    declaredAnnotationTypes.add(name);
  }

  /**
   * Add a typesystem to the script.
   *
   * @param descriptor
   *          Type system's descriptor path.
   */
  public void addTypeSystem(String descriptor) {
    typesystems.add(descriptor);
  }

  /**
   * Add a script to the script.
   *
   * @param script
   *          the script's full name.
   */
  public void addScript(String script) {
    scripts.add(script);
  }

  /**
   * Import a type in the current namespace.
   *
   * @param longName
   *          Complete type name.
   * @param shortName
   *          Short type name (without namespace).
   */
  private void importType(String longName, String shortName) {
    Set<String> targets = ambiguousTypeAlias.get(shortName);
    if (targets != null) {
      // shortName is already ambiguous, add longName to its list of
      // possible targets
      targets.add(longName);
    } else {
      String existing = namespaces.put(shortName, longName);

      if (existing != null && !existing.equals(longName)) {
        // shortName can now be resolved to "existing" or "longName"
        targets = new HashSet<>(2);
        targets.add(existing);
        targets.add(longName);

        // add existing mapping and longName to its list of possible
        // targets
        ambiguousTypeAlias.put(shortName, targets);

        // remove shortName from the namespace because it is ambiguous
        namespaces.remove(shortName);
      }
    }
  }

  /**
   * Import a type from a type system.
   *
   * @param typesystem
   *          Typesystem from which to import the type or null.
   * @param longName
   *          Type to import.
   * @param shortName
   *          Short name to use for this type.
   */
  public void importTypeFromTypeSystem(String typesystem, String longName, String shortName) {
    String key = typesystem != null ? typesystem : "";
    List<Alias> aliases = typeImports.get(key);

    if (aliases == null) {
      aliases = new ArrayList<>();
      typeImports.put(key, aliases);
    }

    aliases.add(new Alias(longName, shortName));
  }

  /**
   * Import a type from a type system.
   *
   * The type is aliased by its unqualified name.
   *
   * @param typesystem
   *          Typesystem from which to import the type or null.
   * @param longName
   *          Type to import.
   */
  public void importTypeFromTypeSystem(String typesystem, String longName) {
    importTypeFromTypeSystem(typesystem, longName,
            longName.substring(longName.lastIndexOf('.') + 1));
  }

  /**
   * Import all the types from a package.
   *
   * @param typesystem
   *          Type system describing the package to load.
   * @param packageName
   *          Package to load or null to load all packages.
   * @param alias
   *          Alias of the package. Null or empty string to use no alias.
   */
  public void importPackageFromTypeSystem(String typesystem, String packageName, String alias) {
    TypeSystemDescription tsd = TypeSystemDescriptionFactory
            .createTypeSystemDescription(typesystem);
    try {
      tsd.resolveImports(getResourceManager());
    } catch (InvalidXMLException e) {
      throw new RuntimeException("Cannot resolve imports in " + typesystem, e);
    }

    for (TypeDescription td : tsd.getTypes()) {
      String qname = td.getName();
      if (packageName == null || (qname.startsWith(packageName)
              && qname.indexOf('.', packageName.length() + 1) == -1)) {
        // td is in packageName
        if (alias != null) {
          String shortName = alias + "." + qname.substring(qname.lastIndexOf('.') + 1);
          importTypeFromTypeSystem(typesystem, qname, shortName);
        } else {
          importTypeFromTypeSystem(typesystem, qname);
        }
      }
    }
  }

  /**
   * Imports all the packages from the specified type system.
   *
   * @param typesystem
   *          Typesystem to load.
   * @param alias
   *          Alias for all the packages.
   */
  public void importAllPackagesFromTypeSystem(String typesystem, String alias) {
    importPackageFromTypeSystem(typesystem, null, alias);
  }

  /**
   * Import all the types from a package that are available at runtime.
   *
   * @param packageName
   *          Package to load.
   * @param alias
   *          Alias of the package. Null or empty string to use no alias.
   */
  public void importPackage(String packageName, String alias) {
    List<String> aliases = packageImports.get(packageName);
    if (aliases == null) {
      aliases = new ArrayList<>(1);
      packageImports.put(packageName, aliases);
    }

    aliases.add(alias == null ? "" : alias);
  }

  public RutaWordList getWordList(String list) {

    if (list == null) {
      return null;
    }

    RutaWordList result = wordLists.get(list);
    UimaContext context = owner.getContext();
    Boolean dictRemoveWS = false;
    if (context != null) {
      dictRemoveWS = (Boolean) context.getConfigParameterValue(RutaEngine.PARAM_DICT_REMOVE_WS);
      if (dictRemoveWS == null) {
        dictRemoveWS = false;
      }
    }
    if (result == null) {
      if (list.endsWith("txt") || list.endsWith("twl") || list.endsWith("mtwl")) {
        ResourceLoader resourceLoader = new RutaResourceLoader(getResourcePaths(),
                getResourceManager().getExtensionClassLoader());
        Resource resource = resourceLoader.getResource(list);
        if (resource.exists()) {
          try {
            if (list.endsWith("mtwl")) {
              wordLists.put(list, new MultiTreeWordList(resource));
            } else {
              wordLists.put(list, new TreeWordList(resource, dictRemoveWS));
            }
          } catch (IOException e) {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE,
                    "Error reading word list" + list, e);
          }
        } else {
          Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "Can't find " + list + "!");
        }
      } else {
        try {
          RutaWordList rutaList = (RutaWordList) context.getResourceObject(list);
          if (rutaList != null) {
            wordLists.put(list, rutaList);
          } else {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE,
                    "Can't find external resource list" + list);
          }
        } catch (ResourceAccessException e) {
          Logger.getLogger(this.getClass().getName()).log(Level.SEVERE,
                  "Can't find external resource list" + list, e);
        }
      }
    }

    return wordLists.get(list);
  }

  public RutaTable getWordTable(String table) {
    UimaContext context = owner.getContext();

    RutaTable result = tables.get(table);
    if (result == null) {
      if (table.endsWith("csv") || table.endsWith("txt") || table.endsWith("tsv")) {
        var resourceLoader = new RutaResourceLoader(getResourceManager(), getResourcePaths(),
                getResourceManager().getExtensionClassLoader());
        Resource resource = resourceLoader.getResource(table);
        if (resource.exists()) {
          try {
            tables.put(table, new CSVTable(resource, getCsvSeparator(context)));
          } catch (IOException e) {
            Logger.getLogger(this.getClass().getName()).log(Level.SEVERE,
                    "Error reading csv table " + table, e);
          }
        } else {
          Logger.getLogger(this.getClass().getName()).log(Level.SEVERE,
                  "Can't find " + table + "!");
        }
      } else {
        try {
          RutaTable rutaTable = (RutaTable) context.getResourceObject(table);
          tables.put(table, rutaTable);
        } catch (ResourceAccessException e) {
          Logger.getLogger(this.getClass().getName()).log(Level.SEVERE,
                  "Can't find external resource table" + table, e);
        }
      }
    }

    return tables.get(table);
  }

  private String getCsvSeparator(UimaContext context) {
    if (context != null) {
      String cvsSeparator = (String) context
              .getConfigParameterValue(RutaEngine.PARAM_CSV_SEPARATOR);
      if (cvsSeparator != null) {
        return cvsSeparator;
      }
    }
    return CSVTable.DEFAULT_CSV_SEPARATOR;
  }

  private void addVariable(String name, Class<?> type, Class<?> generic) {
    variableTypes.put(name, type);
    if (generic != null) {
      variableGenericTypes.put(name, generic);
    }
    variableValues.put(name, getInitialValue(name, type));
  }

  @SuppressWarnings("unchecked")
  private Object getInitialValue(String name, Class<?> type) {
    Object init = initializedVariables.get(name);
    if (init != null) {
      if (init instanceof List) {
        ArrayList<Object> list = new ArrayList<>();
        list.addAll((Collection<? extends Object>) init);
        return list;
      }
      return init;
    }
    if (Integer.class.equals(type)) {
      return 0;
    } else if (Double.class.equals(type)) {
      return 0d;
    } else if (Float.class.equals(type)) {
      return 0f;
    } else if (String.class.equals(type)) {
      return null;
    } else if (Boolean.class.equals(type)) {
      return false;
    } else if (Type.class.equals(type)) {
      return null;
    } else if (List.class.equals(type)) {
      return new ArrayList<>();
    }
    return null;
  }

  public void addVariable(String name, String type) {
    addVariable(name, availableTypes.get(type), availableListTypes.get(type));
  }

  public void removeVariable(String name) {
    variableTypes.remove(name);
    variableGenericTypes.remove(name);
    variableValues.remove(name);
    tempVariableValues.remove(name);
  }

  public boolean ownsVariable(String name) {
    return variableTypes.containsKey(name);
  }

  public boolean ownsVariableOfType(String name, String type) {
    if (variableAliases.containsKey(name)) {
      name = variableAliases.get(name);
    }
    Class<?> varclass = variableTypes.get(name);
    Class<?> aclass = availableTypes.get(type);
    boolean list = true;
    if (aclass.equals(List.class)) {
      Class<?> vt = variableGenericTypes.get(name);
      Class<?> at = availableListTypes.get(type);
      list = vt != null && vt.equals(at);
    }
    return list && varclass != null && varclass.equals(aclass);
  }

  public boolean isVariable(String name) {
    if (variableAliases.containsKey(name)) {
      name = variableAliases.get(name);
    }
    if (ownsVariable(name)) {
      return true;
    }
    if (owner != null && owner.getParent() != null) {
      return owner.getParent().getEnvironment().isVariable(name);
    }
    return false;
  }

  public boolean isVariableOfType(String name, String type) {
    return ownsVariableOfType(name, type) || (owner.getParent() != null
            && owner.getParent().getEnvironment().isVariableOfType(name, type));
  }

  public Class<?> getVariableType(String name) {
    if (variableAliases.containsKey(name)) {
      name = variableAliases.get(name);
    }
    Class<?> result = variableTypes.get(name);
    if (result != null) {
      return result;
    } else if (owner.getParent() != null) {
      return owner.getParent().getEnvironment().getVariableType(name);
    }
    return null;
  }

  public Class<?> getVariableGenericType(String name) {
    Class<?> result = variableGenericTypes.get(name);
    if (result != null) {
      return result;
    } else if (owner.getParent() != null) {
      return owner.getParent().getEnvironment().getVariableGenericType(name);
    }
    return null;
  }

  public <T> T getVariableValue(String name, Class<T> type, RutaStream stream) {
    if (variableAliases.containsKey(name)) {
      name = variableAliases.get(name);
    }
    Object result = null;
    boolean containsKey = variableValues.containsKey(name);
    result = tempVariableValues.get(name);
    if (result == null) {
      result = variableValues.get(name);
    }

    if (result instanceof String && type.equals(Type.class)) {
      // "cast" string to type, because initial values were set when there
      // was no cas/type system
      // yet
      String stringValue = (String) result;
      result = types.get(stringValue);
      if (result == null) {
        // try to resolve short names
        result = getType(stringValue);
      }
    }

    if (containsKey && result == null) {
      // TODO find the problem with the null values!
      // this might now work for word lists in another env.
      Object initialValue = getInitialValue(name, type);
      if (initialValue instanceof Type) {
        return type.cast(initialValue);
      } else if (initialValue != null) {
        throw new IllegalArgumentException("Variable " + name + " of type " + type
                + " is not correctly initialized! It is not a Type, but " + initialValue);
      }
    }
    if (result == annotationTypeDummy) {
      return type.cast(cas.getAnnotationType());
    }
    if (result != null) {
      MatchContext context = new MatchContext(owner);
      if (RutaWordList.class.isAssignableFrom(type) && result instanceof WordListExpression) {
        WordListExpression wle = (WordListExpression) result;
        RutaWordList list = wle.getList(context, stream);
        return type.cast(list);
      } else if (RutaTable.class.isAssignableFrom(type) && result instanceof WordTableExpression) {
        WordTableExpression wte = (WordTableExpression) result;
        RutaTable table = wte.getTable(context, stream);
        return type.cast(table);
      } else {
        return type.cast(result);
      }
    } else if (owner.getParent() != null) {
      return owner.getParent().getEnvironment().getVariableValue(name, type, stream);
    }
    return null;
  }

  public Object getVariableValue(String name, RutaStream stream) {
    return getVariableValue(name, Object.class, stream);
  }

  @SuppressWarnings("rawtypes")
  public Object getLiteralValue(String var, Object value) {
    if (ownsVariable(var)) {
      MatchContext context = new MatchContext(owner);
      Class<?> clazz = variableTypes.get(var);
      if (value instanceof INumberExpression) {
        INumberExpression ne = (INumberExpression) value;
        if (clazz.equals(Integer.class)) {
          return ne.getIntegerValue(context, null);
        } else if (clazz.equals(Double.class)) {
          return ne.getDoubleValue(context, null);
        } else if (clazz.equals(Float.class)) {
          return ne.getFloatValue(context, null);
        } else if (clazz.equals(String.class)) {
          return ne.getStringValue(context, null);
        }
      } else if (clazz.equals(String.class) && value instanceof IStringExpression) {
        IStringExpression se = (IStringExpression) value;
        return se.getStringValue(context, null);
      } else if (clazz.equals(Boolean.class) && value instanceof IBooleanExpression) {
        IBooleanExpression be = (IBooleanExpression) value;
        return be.getBooleanValue(context, null);
      }
      if (clazz.equals(RutaWordList.class) && value instanceof LiteralWordListExpression) {
        return value;
      } else if (clazz.equals(RutaWordList.class) && value instanceof StringWordListExpression) {
        return value;
      } else if (clazz.equals(RutaWordList.class) && value instanceof String) {
        return value;
      } else if (clazz.equals(RutaTable.class) && value instanceof LiteralWordTableExpression) {
        return value;
      } else if (clazz.equals(RutaTable.class) && value instanceof StringWordTableExpression) {
        return value;
      } else if (clazz.equals(RutaTable.class) && value instanceof String) {
        return value;
      } else if (clazz.equals(List.class) && value instanceof ListExpression) {
        List list = getList((ListExpression) value);
        return list;
      } else if (clazz.equals(Type.class) && value instanceof CommonToken) {
        String typeName = ((CommonToken) value).getText();
        return typeName;
      } else if (clazz.equals(Type.class) && value instanceof SimpleTypeExpression) {
        String typeName = ((SimpleTypeExpression) value).getTypeString();
        return typeName;
      }

      return null;
    } else {
      return owner.getParent().getEnvironment().getLiteralValue(var, value);
    }
  }

  @SuppressWarnings("unchecked")
  public void setInitialVariableValue(String var, Object value) {
    if (ownsVariable(var)) {
      if (value instanceof List) {
        List<Object> initValue = new ArrayList<>();
        initValue.addAll((Collection<? extends Object>) value);
        initializedVariables.put(var, initValue);
      } else {
        initializedVariables.put(var, value);
      }
      setVariableValue(var, value);
    } else if (owner.getParent() != null) {
      owner.getParent().getEnvironment().setInitialVariableValue(var, value);
    }
  }

  public void setVariableValue(String name, Object value) {
    if (variableAliases.containsKey(name)) {
      name = variableAliases.get(name);
    }
    if (ownsVariable(name)) {
      Class<?> clazz = variableTypes.get(name);
      if (value == null) {
        value = getInitialValue(name, clazz);
      }
      variableValues.put(name, value);
    } else if (owner.getParent() != null) {
      owner.getParent().getEnvironment().setVariableValue(name, value);
    }
  }

  public void setTempVariableValue(String name, Object value) {
    if (variableAliases.containsKey(name)) {
      name = variableAliases.get(name);
    }
    tempVariableValues.put(name, value);
  }

  @SuppressWarnings("rawtypes")
  private List getList(ListExpression value) {
    if (value instanceof SimpleBooleanListExpression) {
      SimpleBooleanListExpression e = (SimpleBooleanListExpression) value;
      return e.getList();
    } else if (value instanceof SimpleNumberListExpression) {
      SimpleNumberListExpression e = (SimpleNumberListExpression) value;
      return e.getList();
    } else if (value instanceof SimpleStringListExpression) {
      SimpleStringListExpression e = (SimpleStringListExpression) value;
      return e.getList();
    } else if (value instanceof SimpleTypeListExpression) {
      SimpleTypeListExpression e = (SimpleTypeListExpression) value;
      return e.getList();
    }
    return null;
  }

  public void reset(CAS cas) {
    this.cas = cas;
    Set<Entry<String, Object>> entrySet = variableValues.entrySet();
    for (Entry<String, Object> entry : entrySet) {
      String key = entry.getKey();
      Class<?> variableType = variableTypes.get(key);
      Object initialValue = getInitialValue(key, variableTypes.get(key));
      if ((!variableType.equals(RutaTable.class) && !variableType.equals(RutaWordList.class))
              || initialValue != null) {
        // not for word lists
        entry.setValue(initialValue);
      }
    }
  }

  public ResourceManager getResourceManager() {
    if (resourceManager != null) {
      return resourceManager;
    } else {
      RutaBlock parent = owner.getParent();
      if (parent != null) {
        return parent.getEnvironment().getResourceManager();
      }
    }
    // at least return default resource manager
    try {
      resourceManager = ResourceManagerFactory.newResourceManager();
      return resourceManager;
    } catch (ResourceInitializationException e) {
      UIMAFramework.getLogger(getClass()).log(SEVERE,
              "Failed to initialize fallback ResourceManager.", e);
    }
    return UIMAFramework.newDefaultResourceManager();
  }

  public void setResourceManager(ResourceManager resourceManager) {
    this.resourceManager = resourceManager;
  }

  public void addMacroAction(String name, Map<String, String> def, Set<String> vars,
          List<AbstractRutaAction> actions) {
    macroActions.put(name, new ImmutableTriple<>(def, actions, vars));
  }

  public void addMacroCondition(String name, Map<String, String> def, Set<String> vars,
          List<AbstractRutaCondition> conditions) {
    macroConditions.put(name, new ImmutableTriple<>(def, conditions, vars));
  }

  public boolean isMacroAction(String name) {
    boolean isDefined = macroActions.keySet().contains(name);
    if (isDefined) {
      return true;
    } else if (owner != null && owner.getParent() != null) {
      return owner.getParent().getEnvironment().isMacroAction(name);
    }
    return false;
  }

  public boolean isMacroCondition(String name) {
    boolean isDefined = macroConditions.keySet().contains(name);
    if (isDefined) {
      return true;
    } else if (owner != null && owner.getParent() != null) {
      return owner.getParent().getEnvironment().isMacroCondition(name);
    }
    return false;
  }

  public Triple<Map<String, String>, List<AbstractRutaAction>, Set<String>> getMacroAction(
          String name) {
    Triple<Map<String, String>, List<AbstractRutaAction>, Set<String>> definition = macroActions
            .get(name);
    if (definition != null) {
      return definition;
    } else if (owner != null && owner.getParent() != null) {
      return owner.getParent().getEnvironment().getMacroAction(name);
    }
    return null;
  }

  public Triple<Map<String, String>, List<AbstractRutaCondition>, Set<String>> getMacroCondition(
          String name) {
    Triple<Map<String, String>, List<AbstractRutaCondition>, Set<String>> definition = macroConditions
            .get(name);
    if (definition != null) {
      return definition;
    } else if (owner != null && owner.getParent() != null) {
      return owner.getParent().getEnvironment().getMacroCondition(name);
    }
    return null;
  }

  public void addAliasVariable(String name, String var) {
    variableAliases.put(name, var);
  }

  public void removeAliasVariable(String name) {
    variableAliases.remove(name);
  }

  public String getVariableNameOfExpression(IRutaExpression expression) {
    String verbalize = verbalizer.verbalize(expression);
    return verbalize;
  }

  public Map<String, Type> getTypes() {
    return types;
  }

  public Set<String> getDeclaredAnnotationTypes() {
    return declaredAnnotationTypes;
  }

  public Set<String> getTypesystems() {
    return typesystems;
  }

  public Map<String, String> getNamespaces() {
    return namespaces;
  }

  public void addMatchToVariable(RuleMatch ruleMatch, RuleElement element, MatchContext context,
          RutaStream stream) {
    String var = element.getLabel();
    if (StringUtils.isBlank(var)) {
      return;
    }
    List<AnnotationFS> annotations = ruleMatch.getMatchedAnnotationsOfElement(element);
    addAnnotationsToVariable(annotations, var, context);
  }

  public void removeVariableValue(String var, MatchContext context) {
    if (StringUtils.isBlank(var)) {
      return;
    }
    setTempVariableValue(var, null);
  }

  public void addAnnotationsToVariable(List<AnnotationFS> annotations, String var,
          MatchContext context) {
    if (StringUtils.isBlank(var)) {
      return;
    }

    Class<?> variableType = getVariableType(var);
    if (List.class.equals(variableType) && AnnotationFS.class.equals(getVariableGenericType(var))) {
      setTempVariableValue(var, annotations);
    } else if (AnnotationFS.class.equals(variableType)) {
      if (context.getDirection()) {
        AnnotationFS annotation = null;
        if (annotations != null) {
          if (context.getDirection()) {
            annotation = annotations.get(annotations.size() - 1);
          } else {
            annotation = annotations.get(0);
          }
        }
        setTempVariableValue(var, annotation);
      }
    }
  }

  public void acceptTempVariableValues(Collection<String> localVariables) {
    for (String variableName : localVariables) {
      if (tempVariableValues.containsKey(variableName)) {
        Object value = tempVariableValues.get(variableName);
        setVariableValue(variableName, value);
      }
    }

    clearTempVariables(localVariables);
  }

  public void clearTempVariables(Collection<String> localVariables) {
    for (String variableName : localVariables) {
      tempVariableValues.remove(variableName);
    }
  }

  public void ensureMaterializedInitialValues(MatchContext matchContext, RutaStream stream) {

    for (Entry<String, Object> entry : new ArrayList<>(initializedVariables.entrySet())) {
      String var = entry.getKey();
      Object value = entry.getValue();

      if (value instanceof IAnnotationExpression || value instanceof IBooleanExpression
              || value instanceof INumberExpression || value instanceof IStringExpression
              || value instanceof ITypeExpression) {
        Class<?> clazz = variableTypes.get(var);
        Object expressionValue = stream.getExpressionValue(clazz, (IRutaExpression) value,
                matchContext);
        initializedVariables.put(var, expressionValue);
        variableValues.put(var, expressionValue);
      } else if (value instanceof List) {
        List<?> list = (List<?>) value;
        List<Object> newList = new ArrayList<>(list.size());
        Class<?> clazz = variableGenericTypes.get(var);
        for (Object each : list) {
          if (each instanceof IAnnotationExpression || each instanceof IBooleanExpression
                  || each instanceof INumberExpression || each instanceof IStringExpression
                  || each instanceof ITypeExpression) {
            Object expressionValue = stream.getExpressionValue(clazz, (IRutaExpression) each,
                    matchContext);
            newList.add(expressionValue);
          } else {
            newList.add(each);
          }
        }
        initializedVariables.put(var, newList);
        variableValues.put(var, new ArrayList<>(newList));
      }
    }
  }

  public void setTypeIgnorePattern(Pattern pattern) {
    typeIgnorePattern = pattern;
  }
}
