package com.sas.unravl.generators;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BinaryNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.sas.unravl.UnRAVL;
import com.sas.unravl.UnRAVLException;
import com.sas.unravl.util.Json;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * This class produces a binary byte stream from a JSON 'binary' specification.
 * 
 * <pre>
 * { "binary" : array-of-bytes } // the binary value is the result
 * { "binary" : "@fileOrUrl" } // read binary data from a File or URL
 * { "binary" : array-of-binary } // combine binary streams, each of which may be a array-of-bytes or a @file-or-url
 * </pre>
 * <p>
 * TODO: allow variable references <code>"varName"</code>; the value must be an
 * array of byte values.
 * </p>
 * 
 * @author David.Biesack@sas.com
 */
public class Binary {

    private static final int BUFSIZE = 256;
    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    private final UnRAVL script;

    /**
     * Create an instance from a JSON object; the value associated with the
     * field name defines how to create the binary data. For example, for the
     * ObjectNode <code>{ "binary" : [ 0, 2, 1, 3 ]}</code> the constructor new
     * Binary(object, "binary")
     * 
     * @param script
     *            the current UnRAVL script
     * @param node
     *            the JSON node for this scriptlet
     * @param fieldName
     *            the field name (normally "binary")
     * 
     * @throws IOException
     *             if an I/O exception occurs
     * @throws UnRAVLException
     *             if an other exception occurs, including invalid JSON
     *             specification.
     */
    public Binary(UnRAVL script, ObjectNode node, String fieldName)
            throws IOException, UnRAVLException {
        this(script, node.get(fieldName));
    }

    /**
     * Construct a Binary instance from the binary spec value.
     * 
     * @param script
     *            the current UnRAVL script
     * @param binarySpec
     *            either a TextNode with the value "@file-or-url", or a
     *            ArrayNode that contains integer byte values (0 to 255) or
     *            "@file-or-url" strings, or nested arrays.
     * 
     * @throws IOException
     *             an I/O error occurred
     * @throws UnRAVLException
     *             Some other exception occurred, including invalid JSON
     *             specification
     */
    public Binary(UnRAVL script, JsonNode binarySpec) throws IOException,
            UnRAVLException {
        this.script = script;
        build(binarySpec);
    }

    public void build(JsonNode node) throws IOException, UnRAVLException {
        if (node == null) {
        } else if (node.isTextual()) {
            build(node.textValue());
        } else if (node.isArray()) {
            for (JsonNode each : Json.array(node)) {
                build(each);
            }
        } else if (node.isInt()) {
            int b = node.asInt();
            if (b < 0 | b > 255)
                throw new UnRAVLException("Byte value " + b
                        + " not in [0..255] in binary element");
            bytes.write(b);
        } else if (node.isBinary()) {
            BinaryNode b = (BinaryNode) node;
            bytes.write(b.binaryValue());
        } else {
            throw new UnRAVLException("Invalid element " + node
                    + " in binary element.");
        }
    }

    private void build(String node) throws IOException, UnRAVLException {
        if (node.startsWith(UnRAVL.REDIRECT_PREFIX)) {
            String path = node.substring(UnRAVL.REDIRECT_PREFIX.length());
            path = script.expand(path);
            buildFromStream(path);
        } else {
            throw new UnRAVLException("Unrecognized element " + node
                    + " in 'binary' input.");
        }

    }

    private void buildFromStream(String fileOrURL) throws IOException {
        InputStream is = null;
        try {
            URL url = new URL(fileOrURL);
            is = url.openStream();
        } catch (MalformedURLException e) {
            File f = new File(fileOrURL);
            if (f.exists()) {
                is = new FileInputStream(f);
            } else {
                is = getClass().getResourceAsStream(fileOrURL);
            }
        }
        if (is == null) {
            throw new IOException("No such file or URL " + fileOrURL);
        }
        copy(is, bytes);
    }

    /**
     * Copy bytes from an input stream to an output stream.
     * 
     * @param in
     *            the input stream. This is closed when done.
     * @param out
     *            the output stream. This is <strong>not</strong> closed.
     * @throws IOException
     *             if there is an error reading from in or writing to out
     */
    public static void copy(InputStream in, OutputStream out)
            throws IOException {

        byte buffer[] = new byte[BUFSIZE];
        BufferedInputStream bis = new BufferedInputStream(in);
        BufferedOutputStream bos = new BufferedOutputStream(out);
        for (int n = bis.read(buffer, 0, BUFSIZE); n > 0; n = bis.read(buffer,
                0, BUFSIZE))
            bos.write(buffer, 0, n);
        bos.flush();
        bis.close();
    }

    public InputStream stream() {
        return new ByteArrayInputStream(bytes.toByteArray());
    }

    public int size() {
        return bytes.size();
    }

    @Override
    public boolean equals(Object other) {
        if (other == null)
            return false;
        else if (other instanceof Binary) {
            return bytes.equals(((Binary) other).bytes);
        } else
            return false;
    }

    public byte[] bytes() {
        return bytes.toByteArray();
    }

}
