/*
 * 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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream;

import org.apache.commons.geometry.euclidean.internal.EuclideanUtils;
import org.apache.commons.geometry.euclidean.threed.BoundarySource3D;
import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
import org.apache.commons.geometry.euclidean.threed.Triangle3D;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
import org.apache.commons.geometry.io.core.GeometryFormat;
import org.apache.commons.geometry.io.core.internal.GeometryIOUtils;
import org.apache.commons.geometry.io.core.output.GeometryOutput;
import org.apache.commons.geometry.io.euclidean.threed.AbstractBoundaryWriteHandler3D;
import org.apache.commons.geometry.io.euclidean.threed.FacetDefinition;
import org.apache.commons.geometry.io.euclidean.threed.GeometryFormat3D;

/** {@link org.apache.commons.geometry.io.euclidean.threed.BoundaryWriteHandler3D BoundaryWriteHandler3D}
 * implementation for writing STL content. Because of its compact nature, all STL content is written in
 * binary format, as opposed the text (i.e. "ASCII") format. Callers should use the {@link TextStlWriter}
 * class directly in order to create text STL content.
 */
public class StlBoundaryWriteHandler3D extends AbstractBoundaryWriteHandler3D {

    /** Initial size of the data buffer. */
    private static final int DEFAULT_BUFFER_SIZE = 1024 * StlConstants.BINARY_TRIANGLE_BYTES;

    /** Initial size of data buffers used during write operations. */
    private int initialBufferSize = DEFAULT_BUFFER_SIZE;

    /** {@inheritDoc} */
    @Override
    public GeometryFormat getFormat() {
        return GeometryFormat3D.STL;
    }

    /** Get the initial size of the data buffers used by this instance.
     *
     * <p>The buffer is used in situations where it is not clear how many
     * triangles will ultimately be written to the output. In these cases, the
     * triangle data is first written to an internal buffer. Once all triangles are
     * written, the STL header containing the total triangle count is written
     * to the output, followed by the buffered triangle data.</p>
     * @return initial buffer size
     */
    public int getinitialBufferSize() {
        return initialBufferSize;
    }

    /** Set the initial size of the data buffers used by this instance.
     *
     * <p>The buffer is used in situations where it is not clear how many
     * triangles will ultimately be written to the output. In these cases, the
     * triangle data is first written to an internal buffer. Once all triangles are
     * written, the STL header containing the total triangle count is written
     * to the output, followed by the buffered triangle data.</p>
     * @param initialBufferSize initial buffer size
     */
    public void setInitialBufferSize(final int initialBufferSize) {
        if (initialBufferSize < 1) {
            throw new IllegalArgumentException("Buffer size must be greater than 0");
        }
        this.initialBufferSize = initialBufferSize;
    }

    /** {@inheritDoc} */
    @Override
    public void write(final BoundarySource3D src, final GeometryOutput out) {
        // handle cases where we know the number of triangles to be written up front
        // and do not need to buffer the content
        if (src instanceof TriangleMesh) {
            writeTriangleMesh((TriangleMesh) src, out);
        } else {
            // unknown number of triangles; proceed with a buffered write
            super.write(src, out);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void write(final Stream<? extends PlaneConvexSubset> boundaries, final GeometryOutput out) {

        // write the triangle data to a buffer and track how many we write
        int triangleCount = 0;
        final ByteArrayOutputStream triangleBuffer = new ByteArrayOutputStream(initialBufferSize);

        try (BinaryStlWriter stlWriter = new BinaryStlWriter(triangleBuffer)) {
            final Iterator<? extends PlaneConvexSubset> it = boundaries.iterator();

            while (it.hasNext()) {
                for (final Triangle3D tri : it.next().toTriangles()) {

                    stlWriter.writeTriangle(
                            tri.getPoint1(),
                            tri.getPoint2(),
                            tri.getPoint3(),
                            tri.getPlane().getNormal());

                    ++triangleCount;
                }
            }
        }

        // write the header and copy the data
        writeWithHeader(triangleBuffer, triangleCount, out);
    }

    /** {@inheritDoc} */
    @Override
    public void writeFacets(final Stream<? extends FacetDefinition> facets, final GeometryOutput out) {

        // write the triangle data to a buffer and track how many we write
        int triangleCount = 0;
        final ByteArrayOutputStream triangleBuffer = new ByteArrayOutputStream(initialBufferSize);

        try (BinaryStlWriter dataWriter = new BinaryStlWriter(triangleBuffer)) {
            final Iterator<? extends FacetDefinition> it = facets.iterator();

            FacetDefinition facet;
            int attributeValue;

            while (it.hasNext()) {
                facet = it.next();
                attributeValue = getFacetAttributeValue(facet);

                for (final List<Vector3D> tri :
                    EuclideanUtils.convexPolygonToTriangleFan(facet.getVertices(), t -> t)) {

                    dataWriter.writeTriangle(
                            tri.get(0),
                            tri.get(1),
                            tri.get(2),
                            facet.getNormal(),
                            attributeValue);

                    ++triangleCount;
                }
            }
        }

        // write the header and copy the data
        writeWithHeader(triangleBuffer, triangleCount, out);
    }

    /** Write the given triangle data prefixed with an STL header to the output stream from {@code out}.
     * @param triangleBuffer buffer containing STL triangle data
     * @param count number of triangles in {@code triangleBuffer}
     * @param out output to write to
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    private void writeWithHeader(final ByteArrayOutputStream triangleBuffer, final int count,
            final GeometryOutput out) {
        // write the header and copy the data
        try (OutputStream os = out.getOutputStream()) {
            BinaryStlWriter.writeHeader(null, count, os);
            triangleBuffer.writeTo(os);
        } catch (IOException exc) {
            throw GeometryIOUtils.createUnchecked(exc);
        }
    }

    /** Write all triangles in the given mesh to the output using the binary STL
     * format.
     * @param mesh mesh to write
     * @param output output to write to
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    private void writeTriangleMesh(final TriangleMesh mesh, final GeometryOutput output) {
        try (BinaryStlWriter stlWriter = new BinaryStlWriter(output.getOutputStream())) {
            // write the header
            stlWriter.writeHeader(null, mesh.getFaceCount());

            // write each triangle
            Triangle3D tri;
            for (final TriangleMesh.Face face : mesh.faces()) {
                tri = face.getPolygon();

                stlWriter.writeTriangle(
                        tri.getPoint1(),
                        tri.getPoint2(),
                        tri.getPoint3(),
                        tri.getPlane().getNormal());
            }
        }
    }

    /** Get the attribute value that should be used for the given facet.
     * @param facet facet to get the attribute value for
     * @return attribute value
     */
    private int getFacetAttributeValue(final FacetDefinition facet) {
        if (facet instanceof BinaryStlFacetDefinition) {
            return ((BinaryStlFacetDefinition) facet).getAttributeValue();
        }

        return 0;
    }
}
