/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.geometry.io.euclidean.threed.stl;

import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;

import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
import org.apache.commons.geometry.io.euclidean.threed.FacetDefinitionReader;

/** Class used to read the binary form of the STL file format.
 * @see <a href="https://en.wikipedia.org/wiki/STL_(file_format)#Binary_STL">Binary STL</a>
 */
public class BinaryStlFacetDefinitionReader implements FacetDefinitionReader {

    /** Input stream to read from. */
    private final InputStream in;

    /** Buffer used to read triangle definitions. */
    private final ByteBuffer triangleBuffer = StlUtils.byteBuffer(StlConstants.BINARY_TRIANGLE_BYTES);

    /** Header content. */
    private ByteBuffer header = StlUtils.byteBuffer(StlConstants.BINARY_HEADER_BYTES);

    /** Total number of triangles declared to be present in the input. */
    private long triangleTotal;

    /** Number of triangles read so far. */
    private long trianglesRead;

    /** True when the header content has been read. */
    private boolean hasReadHeader;

    /** Construct a new instance that reads from the given input stream.
     * @param in input stream to read from.
     */
    public BinaryStlFacetDefinitionReader(final InputStream in) {
        this.in = in;
    }

    /** Get a read-only buffer containing the 80 bytes of the STL header. The header does not
     * include the 4-byte value indicating the total number of triangles in the STL file.
     * @return the STL header content
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    public ByteBuffer getHeader() {
        beginRead();
        return ByteBuffer.wrap(header.array().clone());
    }

    /** Return the header content as a string decoded using the UTF-8 charset. Control
     * characters (such as '\0') are not included in the result.
     * @return the header content decoded as a UTF-8 string
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    public String getHeaderAsString() {
        return getHeaderAsString(StlConstants.DEFAULT_CHARSET);
    }

    /** Return the header content as a string decoded using the given charset. Control
     * characters (such as '\0') are not included in the result.
     * @param charset charset to decode the header with
     * @return the header content decoded as a string
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    public String getHeaderAsString(final Charset charset) {
        // decode the entire header as characters in the given charset
        final String raw = charset.decode(getHeader()).toString();

        // strip out any control characters, such as '\0'
        final StringBuilder sb = new StringBuilder();
        for (char c : raw.toCharArray()) {
            if (!Character.isISOControl(c)) {
                sb.append(c);
            }
        }

        return sb.toString();
    }

    /** Get the total number of triangles (i.e. facets) declared to be present in the input.
     * @return total number of triangle in the input
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    public long getNumTriangles() {
        beginRead();
        return triangleTotal;
    }

    /** {@inheritDoc} */
    @Override
    public BinaryStlFacetDefinition readFacet() {
        beginRead();

        BinaryStlFacetDefinition facet = null;

        if (trianglesRead < triangleTotal) {
            facet = readFacetInternal();

            ++trianglesRead;
        }

        return facet;
    }

    /** {@inheritDoc} */
    @Override
    public void close() {
        GeometryIOUtils.closeUnchecked(in);
    }

    /** Read the file header content and triangle count.
     * @throws IllegalStateException is a parse error occurs
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    private void beginRead() {
        if (!hasReadHeader) {
            // read header content
            final int headerBytesRead = GeometryIOUtils.applyAsIntUnchecked(in::read, header.array());
            if (headerBytesRead < StlConstants.BINARY_HEADER_BYTES) {
                throw dataNotAvailable("header");
            }

            header.rewind();

            // read the triangle total
            final ByteBuffer triangleBuf = StlUtils.byteBuffer(Integer.BYTES);

            if (fill(triangleBuf) < triangleBuf.capacity()) {
                throw dataNotAvailable("triangle count");
            }

            triangleTotal = Integer.toUnsignedLong(triangleBuf.getInt());

            hasReadHeader = true;
        }
    }

    /** Internal method to read a single facet from the input.
     * @return facet read from the input
     */
    private BinaryStlFacetDefinition readFacetInternal() {
        if (fill(triangleBuffer) < triangleBuffer.capacity()) {
            throw dataNotAvailable("triangle at index " + trianglesRead);
        }

        final Vector3D normal = readVector(triangleBuffer);
        final Vector3D p1 = readVector(triangleBuffer);
        final Vector3D p2 = readVector(triangleBuffer);
        final Vector3D p3 = readVector(triangleBuffer);

        final int attr = Short.toUnsignedInt(triangleBuffer.getShort());

        return new BinaryStlFacetDefinition(Arrays.asList(p1, p2, p3), normal, attr);
    }

    /** Fill the buffer with data from the input stream. The buffer is then flipped and
     * made ready for reading.
     * @param buf buffer to fill
     * @return number of bytes read
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    private int fill(final ByteBuffer buf) {
        int read = GeometryIOUtils.applyAsIntUnchecked(in::read, buf.array());
        buf.rewind();

        return read;
    }

    /** Read a vector from the given byte buffer.
     * @param buf buffer to read from
     * @return vector containing the next 3 double values from the
     *      given buffer
     */
    private Vector3D readVector(final ByteBuffer buf) {
        final double x = buf.getFloat();
        final double y = buf.getFloat();
        final double z = buf.getFloat();

        return Vector3D.of(x, y, z);
    }

    /** Return an exception stating that data is not available for the file
     * component with the given name.
     * @param name name of the file component missing data
     * @return exception instance
     */
    private static IllegalStateException dataNotAvailable(final String name) {
        return GeometryIOUtils.parseError("Failed to read STL " + name + ": data not available");
    }
}
