package zemberek.core.io;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

/**
 * SimpleTextReader can be used reading text sources with ease.
 */
public final class SimpleTextReader implements AutoCloseable {

  private final InputStream is;
  private final String encoding;
  private List<Filter<String>> filters = new ArrayList<>();
  private boolean trim = false;
  private Template template;

  SimpleTextReader(InputStream is, Template template) {
    this.template = template;
    this.is = is;
    this.encoding = template._encoding;

    List<Filter<String>> filterz = new ArrayList<>();
    if (template._ignoreWhiteSpaceLines) {
      filterz.add(StringFilters.PASS_ONLY_TEXT);
    }
    if (template._regexp != null) {
      filterz.add(StringFilters.newRegexpFilter(template._regexp));
    }
    if (template._ignorePrefix != null) {
      filterz.add(new IgnorePrefixFilter(template._ignorePrefix));
    }

    this.filters = filterz;

    this.trim = template._trim;
  }

  /**
   * Creates a FileReader using the File.
   *
   * @param file a file.
   * @throws NullPointerException if file is null.
   * @throws java.io.IOException if File does not exist
   */
  public SimpleTextReader(File file) throws IOException {
    checkNotNull(file, "File name cannot be null..");
    this.is = new BufferedInputStream(new FileInputStream(file));
    encoding = Charset.defaultCharset().name();
    this.template = new Template(encoding);
  }

  public SimpleTextReader(File file, String encoding) throws IOException {
    checkNotNull(file, "File name cannot be null..");
    this.is = new BufferedInputStream(new FileInputStream(file));
    if (encoding == null) {
      this.encoding = Charset.defaultCharset().name();
    } else {
      this.encoding = encoding;
    }
    this.template = new Template(encoding);
  }

  public SimpleTextReader(String fileName) throws IOException {
    checkNotNull(fileName, "File name cannot be null..");
    this.is = new BufferedInputStream(new FileInputStream(fileName));
    encoding = Charset.defaultCharset().name();
    this.template = new Template(encoding);
  }

  public SimpleTextReader(String fileName, String encoding) throws IOException {
    checkNotNull(fileName, "File name cannot be null..");
    this.is = new BufferedInputStream(new FileInputStream(fileName));
    if (encoding == null) {
      this.encoding = Charset.defaultCharset().name();
    } else {
      this.encoding = encoding;
    }
    this.template = new Template(encoding);
  }

  public SimpleTextReader(InputStream is) {
    checkNotNull(is, "Input Stream cannot be null..");
    this.is = is;
    encoding = Charset.defaultCharset().name();
    this.template = new Template(encoding);
  }

  public SimpleTextReader(InputStream is, String encoding) {
    checkNotNull(is, "Input Stream cannot be null..");
    this.is = is;
    if (encoding == null) {
      this.encoding = Charset.defaultCharset().name();
    } else {
      this.encoding = encoding;
    }
    this.template = new Template(encoding);
  }

  /**
   * Returns a new SimpleTextReader that skips the whitespace lines and trims lines.
   *
   * @param file file
   * @return a new SimpleTextReader
   * @throws java.io.IOException if a porblem occurs while accessing file.
   */
  public static SimpleTextReader trimmingUTF8Reader(File file) throws IOException {
    return new Builder(file).encoding("utf-8").trim().ignoreWhiteSpaceLines().build();
  }

  /**
   * Returns a new UTF-8 LineIterator that skips the whitespace lines and trims lines.
   *
   * @param file file
   * @return a new LineIterator
   * @throws java.io.IOException if a porblem occurs while accessing file.
   */
  public static LineIterator trimmingUTF8LineIterator(File file) throws IOException {
    return new Builder(file).encoding("utf-8").trim().ignoreWhiteSpaceLines().build()
        .getLineIterator();
  }

  /**
   * Returns a new UTF-8 IterableLineReader that skips the whitespace lines and trims lines.
   *
   * @param file file
   * @return a new IterableLineReader
   * @throws java.io.IOException if a porblem occurs while accessing file.
   */
  public static IterableLineReader trimmingUTF8IterableLineReader(File file) throws IOException {
    return new Builder(file).trim().ignoreWhiteSpaceLines().build().getIterableReader();
  }

  /**
   * Returns a new SimpleTextReader that skips the whitespace lines and trims lines.
   *
   * @param is input stream to read.
   * @param encoding character encoding. if null, default encoding is used.
   * @return a new SimpleTextReader
   * @throws java.io.IOException if a porblem occurs while accessing file.
   */
  public static SimpleTextReader trimmingReader(InputStream is, String encoding)
      throws IOException {
    return new Builder(is).encoding(encoding).trim().ignoreWhiteSpaceLines().build();
  }

  /**
   * Returns a new LineIterator that skips the whitespace lines and trims lines.
   *
   * @param is input stream to read.
   * @param encoding character encoding. if null, default encoding is used.
   * @return a new LineIterator
   * @throws java.io.IOException if a porblem occurs while accessing file.
   */
  public static LineIterator trimmingLineIterator(InputStream is, String encoding)
      throws IOException {
    return new Builder(is).encoding(encoding).trim().ignoreWhiteSpaceLines().build()
        .getLineIterator();
  }

  /**
   * Returns a new IterableLineReader that skips the whitespace lines and trims lines.
   *
   * @param is input stream to read.
   * @param encoding character encoding.
   * @return a new IterableLineReader
   * @throws java.io.IOException if a porblem occurs while accessing file.
   */
  public static IterableLineReader trimmingIterableLineReader(InputStream is, String encoding)
      throws IOException {
    return new Builder(is).encoding(encoding).trim().ignoreWhiteSpaceLines().build()
        .getIterableReader();
  }

  /**
   * returns the current encoding.
   *
   * @return current encoding.
   */
  public String getEncoding() {
    return encoding;
  }

  /**
   * it generates a new SimpleTextReader using this.
   *
   * @param is an input stream for reader.
   * @return a new SimpleTextReader having the same attribues of this one.
   * @throws java.io.IOException if there is an error while accessing the input stream.
   */
  public SimpleTextReader cloneForStream(InputStream is) throws IOException {
    return this.template.generateReader(is);
  }

  /**
   * it generates a new SimpleTextReader using this.
   *
   * @param file File for the new Reader.
   * @return a new SimpleTextReader having the same attribues of this one.
   * @throws java.io.IOException if there is an error while accessing the file.
   */
  public SimpleTextReader cloneForFile(File file) throws IOException {
    return this.template.generateReader(file);
  }

  /**
   * converts an input stream data to byte array. careful with memory usage here.
   *
   * @return a byte array representing the stream data.
   * @throws java.io.IOException if an error occurs during the read or write of the streams.
   * @throws NullPointerException if filename is null
   */
  public byte[] asByteArray() throws IOException {
    return IOs.readAsByteArray(is);
  }

  /**
   * Reads the entire file as a single string. Use with caution for big files.
   *
   * @return simgle string representation.
   * @throws java.io.IOException if an IO error occurs
   */
  public String asString() throws IOException {
    String res = IOs.readAsString(getReader());
    if (trim) {
      return res.trim();
    } else {
      return res;
    }
  }

  /**
   * Reads a reader as a list of strings. each item represents one line in the reader.
   *
   * @return a list of Strings from the reader.
   * @throws java.io.IOException if an io error occurs
   */
  public List<String> asStringList() throws IOException {
    return IOs.readAsStringList(getReader(), trim, filters.toArray(new Filter[0]));
  }

  /**
   * return a buffered reader for the file
   *
   * @return buffered reader
   * @throws RuntimeException if file does not exist
   * @throws java.io.IOException if file does not exist or encoding is not available
   */
  BufferedReader getReader() throws IOException {
    return IOs.getReader(is, encoding);

  }

  /**
   * returns an IterableLineReader. This is expecially useful to use in enhanced for loops. if all
   * the elements are consumed, the resources will be closed automatically.
   *
   * @return a new IterableLineReader instance.
   * @throws java.io.IOException if file does not exist, or encoding is not supported.
   */
  public IterableLineReader getIterableReader() throws IOException {
    return new IterableLineReader(getReader(), trim, filters);
  }

  /**
   * returns a LineIterator. it is suggested to close th iterator in a finally block.
   *
   * @return an IterableLineReader that can be iterated for lines.
   * @throws java.io.IOException if file does not exist, or encoding is not supported.
   */
  public LineIterator getLineIterator() throws IOException {
    return new LineIterator(getReader(), trim, filters);
  }

  /**
   * counts the lines. if there are constraints while creating the reader (eg: not reading empty
   * lines), it counts ONLY the lines that are allowed to be read.
   *
   * @return line count.
   * @throws java.io.IOException if there is a problem while accesing the file.
   */
  public long countLines() throws IOException {
    long i;
    try (LineIterator li = getLineIterator()) {
      i = 0;
      while (li.hasNext()) {
        i++;
        li.next();
      }
      return i;
    }
  }

  /**
   * Closes the stream silently.
   */
  public void close() throws IOException {
    IOs.closeSilently(is);
  }

  public static class Builder {

    private InputStream _is;
    private Template _template = new Template();

    public Builder(String fileName) throws IOException {
      checkNotNull(fileName, "File name cannot be null..");
      this._is = new BufferedInputStream(new FileInputStream(fileName));
    }

    public Builder(InputStream is) {
      checkNotNull(is, "File name cannot be null..");
      this._is = is;
    }

    public Builder(File file) throws IOException {
      checkNotNull(file, "File name cannot be null..");
      this._is = new BufferedInputStream(new FileInputStream(file));
    }

    public Builder encoding(String encoding) {
      if (encoding != null) {
        this._template._encoding = encoding;
      }
      return this;
    }

    public Builder ignoreWhiteSpaceLines() {
      this._template._ignoreWhiteSpaceLines = true;
      return this;
    }

    public Builder ignoreIfStartsWith(String... prefix) {
      this._template._ignorePrefix = prefix;
      return this;
    }

    public Builder allowMatchingRegexp(String regexp) {
      this._template._regexp = regexp;
      return this;
    }

    public SimpleTextReader build() {
      return new SimpleTextReader(_is, _template);
    }

    public Builder trim() {
      this._template._trim = true;
      return this;
    }
  }

  static class Template {

    private String _encoding;
    private boolean _trim = false;
    private boolean _ignoreWhiteSpaceLines = false;
    private String _regexp;
    private String[] _ignorePrefix;

    public Template() {
      _encoding = Charset.defaultCharset().name();
    }

    private Template(String encoding) {
      this._encoding = encoding;
    }

    public Template encoding(String encoding) {
      if (encoding == null) {
        this._encoding = Charset.defaultCharset().name();
      }
      this._encoding = encoding;
      return this;
    }

    public Template ignoreWhiteSpaceLines() {
      this._ignoreWhiteSpaceLines = true;
      return this;
    }

    public Template ignoreIfStartsWith(String... prefix) {
      this._ignorePrefix = prefix;
      return this;
    }

    public Template allowMatchingRegexp(String regexp) {
      this._regexp = regexp;
      return this;
    }

    public SimpleTextReader generateReader(InputStream is) throws IOException {
      return new SimpleTextReader(is, this);
    }

    public SimpleTextReader generateReader(String fileName) throws IOException {
      return new SimpleTextReader(new BufferedInputStream(new FileInputStream(fileName)), this);
    }

    public SimpleTextReader generateReader(File file) throws IOException {
      return new SimpleTextReader(new BufferedInputStream(new FileInputStream(file)), this);
    }

    public Template trim() {
      this._trim = true;
      return this;
    }
  }

  private class IgnorePrefixFilter implements Filter<String> {

    String[] tokens;

    private IgnorePrefixFilter(String... token) {
      checkNotNull(token, "Cannot initialize Filter with null string.");
      this.tokens = token;
    }

    public boolean canPass(String s) {
      for (String token : tokens) {
        if (s.startsWith(token)) {
          return false;
        }
      }
      return true;
    }
  }
}
