package de.duehl.basics.io;

/*
 * Copyright 2020 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import de.duehl.basics.io.exceptions.IORuntimeException;
import de.duehl.basics.text.Text;

/**
 * Diese Klasse öffnet, liest aus und schließt Dateien. Ehemals 'OpenDataFile', dann 'FileReader'.
 *
 * @version 1.05     2020-09-29
 * @author Christian Dühl
 */

public class FineFileReader implements Reader {

    /** Die Datei, die geöffnet und gelesen werden soll. */
    private final String filename;

    /** Das LineNumberReader-Objekt. */
    private final LineNumberReader lineReader;

    /** Hält fest, ob der Reader geschlossen wurde. */
    private boolean closed;

    /** Zeichensatz der einzulesenden Datei (z.B. "ISO-8859-1", "UTF-8"). */
    private final Charset charset;

    /**
     * Der Konstruktor aus einem File.
     *
     * @param file
     *            Zu öffnende Datei.
     * @throws IORuntimeException
     *             Falls die angegebene Datei nicht existiert, keine normale Datei ist oder eine
     *             unbekannte Kodierung vorliegt.
     */
    public FineFileReader(File file) {
        this(file.getAbsolutePath());
    }

    /**
     * Der Konstruktor aus einem Dateinamen.
     *
     * @param filename
     *            Name der zu öffnenden Datei.
     * @throws IORuntimeException
     *             Falls die angegebene Datei nicht existiert, keine normale Datei ist oder eine
     *             unbekannte Kodierung vorliegt.
     */
    public FineFileReader(String filename) {
        this(filename, Charset.ISO_8859_1);
    }

    /**
     * Der Konstruktor aus einem File und einem Charset.
     *
     * @param file
     *            Zu öffnende Datei.
     * @param charset
     *            Zeichensatz der einzulesenden Datei.
     * @throws IORuntimeException
     *             Falls die angegebene Datei nicht existiert, keine normale Datei ist oder eine
     *             unbekannte Kodierung vorliegt.
     */
    public FineFileReader(File file, Charset charset) {
        this(file.getAbsolutePath(), charset);
    }

    /**
     * Der Konstruktor aus einem Dateinamen und einem Charset.
     *
     * @param filename
     *            Name der zu öffnenden Datei.
     * @param charset
     *            Zeichensatz der einzulesenden Datei.
     * @throws IORuntimeException
     *             Falls die angegebene Datei nicht existiert, keine normale Datei ist oder eine
     *             unbekannte Kodierung vorliegt.
     */
    public FineFileReader(String filename, Charset charset) {
        this.filename = filename;
        this.charset = charset;
        lineReader = openReader();
        closed = false;
    }

    protected FineFileReader(String filename, Charset charset, LineNumberReader lineReader) {
        this.filename = filename;
        this.charset = charset;
        this.lineReader = lineReader;
        closed = false;
    }

    /**
     * Bildet das Reader-Objekt. Hier werden die Ausnahmen behandelt und in Laufzeitausnahmen
     * umgewandelt.
     *
     * @return Das Reader-Objekt.
     * @throws IORuntimeException
     *             Wenn die Datei nicht gefunden wurde oder eine unbekannte Kodierung vorliegt.
     */
    private LineNumberReader openReader() {
        try {
            return tryToOpenReader();
        }
        catch (FileNotFoundException e) {
            throw new IORuntimeException("Die Eingabedatei '"
                    + filename + "' wurde nicht gefunden. ("
                    + e.getMessage() + ")");
        }
        catch (UnsupportedEncodingException e) {
            throw new IORuntimeException("Bei der Erzeugung des "
                    + "InputStreamReaders für die Eingabedatei '"
                    + filename
                    + "' wurde eine nicht unterstützte Kodierung verwendet. ("
                    + e.getMessage() + ")");
        }
    }

    /**
     * Bildet das Reader-Objekt.
     *
     * @return Das Reader-Objekt.
     * @throws FileNotFoundException
     *             Wenn die angegebene Datei nicht gefunden wurde.
     * @throws UnsupportedEncodingException
     *             Wenn eine unbekannte Kodierung für den InputStreamReader verwendet wurde.
     */
    private LineNumberReader tryToOpenReader() throws FileNotFoundException,
            UnsupportedEncodingException {
        InputStream inputStream = new FileInputStream(filename);
        InputStreamReader reader = new InputStreamReader(inputStream, charset.getCharsetAsString());
        return new LineNumberReader(reader);
    }

    /** Erzeugt eine Stringrepräsentation des Objekts. */
    @Override
    public String toString() {
        return "FineFileReader[fileName:" + filename + ", charset:" + charset.getCharsetAsString()
                + "]";
    }

    /**
     * Liest die nächste Zeile aus der Datei ein.
     *
     * @return Die nächste Zeile oder null, falls das Ende der Datei erreicht wurde.
     * @throws IORuntimeException
     *             Falls die Zeile nicht eingelesen werden konnte.
     */
    @Override
    public String readNextLine() {
        if (closed) {
            throw new IORuntimeException("Es wurde versucht, aus der geschlossenen Datei '"
                    + filename + "' zu lesen");
        }
        else {
            try {
                return lineReader.readLine();
            }
            catch (IOException e) {
                throw new IORuntimeException("Es trat ein Fehler beim Einlesen der nächsten Zeile "
                        + "aus der Datei '" + filename + "' auf. (" + e.getMessage() + ")");
            }
        }
    }

    /** Liefert die aktuelle Zeilennummer zurück. */
    @Override
    public int getLineNumber() {
        return lineReader.getLineNumber();
    }

    /**
     * Schließt den Reader.
     *
     * @throws IORuntimeException
     *             Wenn es beim Schließen zu Problemen kam.
     */
    @Override
    public void close() {
        if (closed) {
            throw new IORuntimeException("Es trat ein Fehler beim Schließen der Datei '"
                    + filename + "' auf.");
        }
        else {
            tryToClose();
            closed = true;
        }
    }

    /**
     * Schließt den Reader.
     *
     * @throws IORuntimeException
     *             Wenn es beim Schließen zu Problemen kam.
     */
    private void tryToClose() {
        try {
            lineReader.close();
        }
        catch (IOException e) {
            throw new IORuntimeException("Es trat ein Fehler beim Schließen der Datei '"
                    + filename + "' auf. (" + e.getMessage() + ")");
        }
    }

    /**
     * Gibt den Dateiinhalt (falls schon gelesen wurde: ab der momentanen Stelle) als Liste von
     * Strings zurück.
     *
     * @return Liste mit dem Dateiinhalt als Strings.
     */
    @Override
    public List<String> readFileToListOfStrings() {
        List<String> list = new ArrayList<>();

        String line = readNextLine();
        while (line != null) {
            list.add(line);
            line = readNextLine();
        }

        return list;
    }

    /**
     * Gibt den Dateiinhalt (falls schon gelesen wurde: ab der momentanen Stelle) als Liste von
     * Listen von tabgetrennten Werten zurück.
     *
     * @return Liste mit dem Dateiinhalt als Strings.
     */
    @Override
    public List<List<String>> readFileToListOfFieldLines() {
        List<List<String>> listOfListOfFields = new ArrayList<>();

        List<String> listOfFields = readFieldsLine();
        while (listOfFields != null) {
            listOfListOfFields.add(listOfFields);
            listOfFields = readFieldsLine();
        }

        return listOfListOfFields;
    }

    /**
     * Gibt den Dateiinhalt (falls schon gelesen wurde: ab der momentanen Stelle) als einen String
     * zurück.
     *
     * @return String mit dem Dateiinhalt.
     */
    @Override
    public String readFileToString() {
        StringBuilder builder = new StringBuilder();
        String lineSeparator = System.getProperty("line.separator");

        List<String> list = readFileToListOfStrings();
        for (String line : list) {
            builder.append(line);
            builder.append(lineSeparator);
        }

        return builder.toString();
    }

    /** Liest eine tabgetrennte Zeile ein. Gibt am Dateiende null zurück. */
    @Override
    public List<String> readFieldsLine() {
        String line = readNextLine();
        if (null == line) {
            return null;
        }
        return Text.splitByTabulatorNotConsumingWhitespace(line);
        //List<String> fields = new ArrayList<>(Arrays.asList(line.split("\t", -1)));
        //return fields;
    }

    /** Getter für den Namen der Eingabedatei. */
    @Override
    public String getFileName() {
        return filename;
    }

}

