/*
This class is part of the YAGO project at the Max Planck Institute
for Informatics/Germany and Télécom ParisTech University/France:
http://yago-knowledge.org

This class is copyright 2016 Fabian M. Suchanek.

YAGO is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published
by the Free Software Foundation, either version 3 of the License,
or (at your option) any later version.

YAGO is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
License for more details.

You should have received a copy of the GNU General Public License
along with YAGO.  If not, see <http://www.gnu.org/licenses/>.
*/

package utils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import basics.Fact;
import basics.FactComponent;
import basics.FactSource;
import basics.FactWriter;
import basics.YAGO;
import javatools.administrative.Announce;
import javatools.administrative.D;

/**
 * This class represents a theme. If a theme is assigned to a file, it is a file
 * fact source, i.e., it can iterate over the facts in the file. It can also
 * write facts.
*/
public class Theme extends FactSource.FileFactSource implements Comparable<Theme> {

  /** Types of Theme */
  public enum ThemeGroup {
    TAXONOMY, SIMPLETAX, CORE, GEONAMES, META, MULTILINGUAL, LINK, OTHER, INTERNAL, WIKIPEDIA;

    public static ThemeGroup of(String s) {
      try {
        return (valueOf(s));
      } catch (Exception e) {
      }
      ;
      return (null);
    }
  }

  /** Types of my theme */
  public final ThemeGroup themeGroup;

  /** Name of the theme */
  public final String name;

  /** Description of the theme */
  public final String description;

  /** maps the names to themes */
  protected static Map<String, Theme> name2theme = new TreeMap<>();

  public Theme(String name, String description) {
    this(name, description, name.startsWith("yago") ? ThemeGroup.OTHER : ThemeGroup.INTERNAL);
  }

  public Theme(String name, String description, ThemeGroup group) {
    super(null);
    this.name = name;
    this.description = description;
    if (name2theme.containsKey(name)) throw new RuntimeException("Duplicate Theme: " + name);
    name2theme.put(this.name, this);
    themeGroup = group;
  }

  public static synchronized Theme getOrCreate(String name, String description, ThemeGroup group) {
    Theme result = name2theme.get(name);
    if (result == null) result = new Theme(name, description, group);
    return (result);
  }

  public Theme(String name, String language, String description, ThemeGroup group) {
    this(name + "_" + language, description, group);
  }

  public Theme(String name, String language, String description) {
    this(name + "_" + language, description);
  }

  /** Returns the language of a theme (or NULL) */
  public String language() {
    int pos = name.lastIndexOf('_');
    if (name.length() < 3 || pos == -1 || pos < name.length() - 4) return (null);
    return (name.substring(pos + 1));
  }

  /** TRUE if the language is English or undefined */
  public boolean isEnglishOrDefault() {
    String lan = language();
    return (lan == null || FactComponent.isEnglish(lan));
  }

  /** Returns this theme as a YAGO entity */
  public String asYagoEntity() {
    return (FactComponent.forYagoEntity("yagoTheme_" + this));
  }

  /** TRUE for export-ready themes */
  public boolean isFinal() {
    return (name.startsWith("yago"));
  }

  @Override
  public int compareTo(Theme o) {
    return name.compareTo(o.name);
  }

  @Override
  public boolean equals(Object obj) {
    return (obj instanceof Theme) && ((Theme) obj).name.equals(name);
  }

  @Override
  public String toString() {
    return name;
  }

  @Override
  public int hashCode() {
    return name.hashCode();
  }

  /** Returns all available themes */
  public static Collection<Theme> all() {
    return (name2theme.values());
  }

  /** Removes all known themes */
  public static void forgetAllFiles() {
    for (Theme t : all())
      t.forgetFile();
  }

  /**
   * Returns the file name of this theme in the given folder, in either TTL or
   * TSV. Gives preference to TSV. Returns null if not found.
   */
  public File findFileInFolder(File folder) {
    File tsv = new File(folder, name + ".tsv");
    File ttl = new File(folder, name + ".ttl");
    if (tsv.exists()) {
      if (ttl.exists()) Announce.warning("Theme", this, "exists as ttl and tsv in", folder, ". Using tsv.");
      return (tsv);
    }
    if (ttl.exists()) return (ttl);
    return (null);
  }

  /** Fact writer for writing the theme */
  protected FactWriter factWriter;

  /** Caching the theme */
  protected FactCollection cache = null;

  /** Opens the theme for writing */
  public synchronized void openForWritingInFolder(File folder, String header) throws Exception {
    if (factWriter != null) throw new RuntimeException("Already writing into Theme " + this);
    if (file != null) throw new RuntimeException("Theme " + this + " already written to " + file);
    file = new File(folder, name + ".tsv");
    factWriter = FactWriter.from(file, header);
    cache = null;
  }

  /** Flush the theme */
  public void flush() throws IOException {
    if (factWriter != null) factWriter.flush();
  }

  /** Closes the theme for writing */
  public void close() throws IOException {
    if (factWriter == null) throw new IOException("Theme " + this + " cannot be closed because it was not open");
    factWriter.writeComment("end of file " + name);
    factWriter.close();
    factWriter = null;
  }

  /** Assigns the theme to a file (to use data that is already there) */
  public synchronized Theme assignToFolder(File folder) throws IOException {
    File f = findFileInFolder(folder);
    if (f == null) throw new FileNotFoundException("Cannot find theme " + this + " in " + folder.getCanonicalPath());
    if (file != null) {
      if (file.equals(f)) return (this);
      else throw new IOException("Theme " + this + " is already assigned to file " + file + ", cannot assign it to " + f);
    }
    file = f;
    cache = null;
    return (this);
  }

  /** Writes a fact */
  public void write(Fact f) throws IOException {
    if (factWriter == null)
      throw new RuntimeException("Theme " + this + " is not open for writing. Maybe you forgot to declare it as the output of the extractor?");
    factWriter.write(f);
  }

  /** True if the facts can be read from this source */
  public boolean isAvailableForReading() {
    return file != null && factWriter == null;
  }

  /** Forgets the file */
  public void forgetFile() {
    if (factWriter != null) throw new RuntimeException(this + " cannot forget a file while writing to it: " + this.file);
    file = null;
    cache = null;
  }

  /** Returns the file of this theme (or null) */
  public File file() {
    return file;
  }

  @Override
  public Iterator<Fact> iterator() {
    if (file == null) throw new RuntimeException(
        "Theme " + this + " has not yet been assigned to a file.\nMaybe the theme was not declared as an input to an extractor?");
    if (factWriter != null) throw new RuntimeException("Theme " + this + " is currently being written");
    return (super.iterator());
  }

  /** returns the cache, or creates a cache */
  public synchronized FactCollection factCollection() throws IOException {
    if (factWriter != null) throw new IOException("Theme " + this + " is currently being written");
    if (file == null)
      throw new IOException("Theme " + this + " has not yet been assigned to a file.\n" + "Maybe it was not declared as input to an extractor?");
    if (cache == null) cache = new FactCollection(file, true);
    return (cache);
  }

  /** TRUE if is cached */
  public boolean isCached() {
    return (cache != null);
  }

  /** Removes the cache */
  public void killCache() {
    if (cache != null) {
      D.p("Killing cache", this);
      cache = null;
    }
  }

  /** Returns a dictionary from subjects to objects
   * @throws IOException */
  public Map<String, String> dictionary() throws IOException {
    // Force the loading
    factCollection();
    return (new Map<String, String>() {

      @Override
      public void clear() {
        throw new UnsupportedOperationException("Clear dictionary");
      }

      @Override
      public boolean containsKey(Object arg0) {
        if (cache == null) throw new RuntimeException("Cache of " + this + " was killed");
        return cache.containsSubjectWithRelation(arg0.toString(), YAGO.hasTranslation);
      }

      @Override
      public boolean containsValue(Object arg0) {
        throw new UnsupportedOperationException("ContainsValue() on dictionary");
      }

      @Override
      public Set<java.util.Map.Entry<String, String>> entrySet() {
        throw new UnsupportedOperationException("entrySet() on dictionary");
      }

      @Override
      public String get(Object arg0) {
        if (cache == null) throw new RuntimeException("Cache of " + this + " was killed");
        return (cache.getObject((String) arg0, YAGO.hasTranslation));
        /*Map<String, List<Fact>> map = cache.index.get(arg0);
        if (map == null) return null;
        List<Fact> objects = map.get(YAGO.hasTranslation);
        if (objects == null || objects.isEmpty()) return (null);
        return (objects.get(0).getObject());*/
      }

      @Override
      public boolean isEmpty() {
        if (cache == null) throw new RuntimeException("Cache of " + this + " was killed");
        return cache.isEmpty();
      }

      @Override
      public Set<String> keySet() {
        throw new UnsupportedOperationException("keySet() on dictionary");
      }

      @Override
      public String put(String arg0, String arg1) {
        throw new UnsupportedOperationException("put() on dictionary");
      }

      @Override
      public void putAll(Map<? extends String, ? extends String> arg0) {
        throw new UnsupportedOperationException("putAll() on dictionary");
      }

      @Override
      public String remove(Object arg0) {
        throw new UnsupportedOperationException("remove() on dictionary");
      }

      @Override
      public int size() {
        if (cache == null) throw new RuntimeException("Cache of " + this + " was killed");
        return cache.size();
      }

      @Override
      public Collection<String> values() {
        throw new UnsupportedOperationException("values() on dictionary");
      }
    });
    /*
    Map<String, String> result = new HashMap<>();
    Announce.doing("Loading dictionary", this);
    for (Fact f : this) {
    	if (!f.getRelation().equals("<_hasTranslation>"))
    		continue;
    	result.put(f.getSubject(), f.getObject());
    }
    Announce.done();
    return (result);*/
  }

}