/*
 * @(#)StorableInput.java 5.1
 *
 */

package CH.ifa.draw.util;

import java.awt.Color;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;

import de.renew.draw.storables.ontology.Storable;
import de.renew.plugin.PluginManager;


/**
 * An input stream that can be used to resurrect Storable objects.
 * StorableInput preserves the object identity of the stored objects.
 * <br>
 * {@link Storable}
 * {@link StorableOutput}
 * @deprecated This class should no longer be used and is marked as internal.
 */
@Deprecated
public class StorableInput extends StorableInOut
    implements de.renew.draw.storables.ontology.StorableInput
{
    /**
     * Logger for the class StorableInput.
     * Only a single logger instance exists at any given time. If a logger for StorableInput
     * already exists it is returned or else a new instance is created.
     */
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(StorableInput.class);
    private StreamTokenizer _tokenizer;
    private Reader _reader;

    /**
     * The version Integer.MAX_VALUE denoted the most current
     * version. It is used when no version is explicitly given.
     */
    private int _version = Integer.MAX_VALUE;

    /**
     * Initializes a StorableInput from the file given by name.
     * useUTF specifies whether UTF or the platform default
     * character encoding is to be used.
     *
     * @param location URL of the file
     * @param useUTF <code>true</code>, if UTF is used for character encoding
     * @throws IOException if an I/O error occurs while reading from the input location
     * @deprecated This constructor is only for internal usage. Please use the method {@link de.renew.draw.storables.api.StorableApi#createStorableInput(URL, boolean)} instead.
     */
    @Deprecated
    public StorableInput(URL location, boolean useUTF) throws IOException {
        this(URI.create(location.toString()), location.openStream(), useUTF);
    }

    /**
     * Initializes a StorableInput from the file given by name.
     * useUTF specifies whether UTF or the platform default
     * character encoding is to be used.
     *
     * @param file the file the FileInputStream will be constructed with
     * @param useUTF <code>true</code>, if UTF is used for character encoding
     * @throws FileNotFoundException if file was not found
     * @deprecated This constructor is only for internal usage and will later be hidden.
     */
    @Deprecated
    public StorableInput(File file, boolean useUTF) throws FileNotFoundException {
        this(file.toURI(), new FileInputStream(file), useUTF);
    }

    /**
     * Initializes a StorableInput with the given input stream.
     * UTF character encoding is used by default.
     *
     * @param stream the stream the BufferedReader of this class will be initialized with
     * @deprecated This constructor is only for internal usage. Please use the method {@link de.renew.draw.storables.api.StorableApi#createStorableInput(InputStream)} instead.
     */
    @Deprecated
    // This constructor needs to remain public for the API to be able to use it.
    // The deprecation will be removed once the class is moved to another package that is hidden to other plugins.
    public StorableInput(InputStream stream) {
        this(null, stream, true);
    }

    /**
     * Initializes a StorableInput with the given input stream.
     * useUTF specifies whether UTF or the platform default
     * character encoding is to be used.
     *
     * @param stream the InputStream the BufferedReader will be initialized with
     * @param useUTF <code>true</code>, if UTF-8 is used
     * @deprecated This constructor is only for internal usage. Please use the method {@link de.renew.draw.storables.api.StorableApi#createStorableInput(InputStream, boolean)} instead.
     */
    @Deprecated
    public StorableInput(InputStream stream, boolean useUTF) {
        this(null, stream, useUTF);
    }

    private StorableInput(URI location, InputStream stream, boolean useUTF) {
        super(location);
        if (useUTF) {
            _reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
        }
        if (_reader == null) {
            _reader = new BufferedReader(new InputStreamReader(stream));
        }
        _tokenizer = new StreamTokenizer(_reader);
        _tokenizer.wordChars('_', '_'); /* '_' can be in a class name */
    }

    /**
     * Initializes a StorableInput with the given string.
     *
     * @param stringStream the string the StorableInput will be initialized with
     * @deprecated This constructor is only for internal usage. Please use the method {@link de.renew.draw.storables.api.StorableApi#createStorableInput(String)} instead.
     */
    @Deprecated
    // This constructor needs to remain public for the API to be able to use it.
    // The deprecation will be removed once the class is moved to another package that is hidden to other plugins.
    public StorableInput(String stringStream) {
        this(null, stringStream);
    }

    private StorableInput(URI location, String stringStream) {
        super(location);
        if (_reader == null) {
            _reader = new BufferedReader(new StringReader(stringStream));
        }
        _tokenizer = new StreamTokenizer(_reader);
        _tokenizer.wordChars('_', '_'); /* '_' can be in a class name */
    }

    /**
     * @deprecated This constructor is currently not used and will be removed in a later version.
     */
    @Deprecated
    public StorableInput() {}

    /**
     * Gets the version of the StorableInput.
     *
     * @return an <code>int</code>, which describes the used version
     */
    public int getVersion() {
        return _version;
    }

    /**
     * Sets the version of the StorableInput.
     *
     * @param version the new version of the StorableInput
     */
    public void setVersion(int version) {
        this._version = version;
    }

    /**
     * Reads and resurrects a Storable object from the input stream.
     *
     * @return the storable that has been read from the input stream or<br>
     *         <code>null</code> if it wasn't successful to read from the StreamTokenizer
     * @throws IOException if an I/O error occurs while reading from the input
     */
    public Storable readStorable() throws IOException {
        Storable storable;
        String s = readString();

        if (s.equals("NULL")) {
            return null;
        }

        if (s.equals("REF")) {
            int ref = readInt();
            return retrieve(ref);
        }

        storable = (Storable) makeInstance(s);
        map(storable);
        storable.read(this);
        return storable;
    }

    /**
     * Reads a string from the input stream.
     *
     * @return the next token from the StreamTokenizer as String
     * @throws IOException if the next token from the StreamTokenizer isn't a String
     */
    public String readString() throws IOException {
        int token = _tokenizer.nextToken();
        if (token == StreamTokenizer.TT_WORD || token == '"') {
            return _tokenizer.sval;
        }

        final String msg = "Expected String but found: " + _tokenizer;
        throw new IOException(msg);
    }

    /**
     * Determines whether an int could be read from the input stream.
     *
     * @return <code>true</code>, if int can be read successfully
     * @throws IOException if the next token from the StreamTokenizer isn't an int
     */
    public boolean canReadInt() throws IOException {
        int token = _tokenizer.nextToken();
        _tokenizer.pushBack();
        return token == StreamTokenizer.TT_NUMBER;
    }

    /**
     * Reads an int from the input stream.
     *
     * @return the int that has been read from the input stream
     * @throws IOException if the next token from the StreamTokenizer isn't an int
     */
    public int readInt() throws IOException {
        int token = _tokenizer.nextToken();
        if (token == StreamTokenizer.TT_NUMBER) {
            return (int) _tokenizer.nval;
        }

        final String msg = "Expected Integer but found: " + _tokenizer;
        throw new IOException(msg);
    }

    /**
     * Reads a color from the input stream.
     *
     * @return <code>color</code> from the input stream
     * @throws IOException if the next three tokens from the StreamTokenizer isn't an int
     */
    public Color readColor() throws IOException {
        return new Color(readInt(), readInt(), readInt());
    }

    /**
     * Reads a double from the input stream.
     *
     * @return the double read from the input stream
     * @throws IOException if the next token from the StreamTokenizer isn't a double
     */
    public double readDouble() throws IOException {
        int token = _tokenizer.nextToken();
        if (token == StreamTokenizer.TT_NUMBER) {
            return _tokenizer.nval;
        }

        final String msg = "Expected Double but found: " + _tokenizer;
        throw new IOException(msg);
    }

    /**
     * Reads a boolean from the input stream.
     *
     * @return the boolean from the input stream
     * @throws IOException if an I/O error occurs while reading from the input
     */
    public boolean readBoolean() throws IOException {
        int token = _tokenizer.nextToken();
        if (token == StreamTokenizer.TT_NUMBER) {
            return (int) _tokenizer.nval == 1;
        }

        final String msg = "Expected Integer but found: " + _tokenizer;
        throw new IOException(msg);
    }

    /**
     * Creates an object of the given class.
     *
     * @param className string of the class that will be instantiated
     * @return an <code>object</code> from the given class
     * @throws IOException if a method was called, which doesn't exist on the class or<br>
     *         if the given class doesn't have a <code>getInstance()</code> method or<br>
     *         if a method of the given class was called, which was forbidden
     * @throws UnknownTypeException if the given classname is not associated to a class
     */
    protected Object makeInstance(String className) throws IOException {
        try {
            ClassLoader classLoader = PluginManager.getInstance().getBottomClassLoader();
            Class<?> cl = Class.forName(className, true, classLoader);
            return cl.newInstance();
        } catch (NoSuchMethodError e) {
            throw new IOException(
                "Class " + className + " does not seem to have a no-arg constructor", e);
        } catch (ClassNotFoundException e) {
            throw new UnknownTypeException("No class: " + className, className);
        } catch (InstantiationException e) {
            throw new IOException("Cannot instantiate: " + className);
        } catch (IllegalAccessException e) {
            throw new IOException("Class (" + className + ") not accessible");
        }
    }

    /**
     * Closes a storable input stream.
     */
    public void close() {
        try {
            _reader.close();
        } catch (IOException e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * Put the token back.
     */
    public void putBack() {
        _tokenizer.pushBack();
    }
}