/*
 * 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.Closeable;
import java.io.OutputStream;
import java.nio.ByteBuffer;

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

/** Low-level class for writing binary STL content.
 */
public class BinaryStlWriter implements Closeable {

    /** Output stream to write to. */
    private final OutputStream out;

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

    /** Construct a new instance for writing to the given output.
     * @param out output stream to write to
     */
    public BinaryStlWriter(final OutputStream out) {
        this.out = out;
    }

    /** Write binary STL header content. If {@code headerContent} is null, the written header
     * will consist entirely of zeros. Otherwise, up to 80 bytes from {@code headerContent}
     * are written to the header, with any remaining bytes of the header filled with zeros.
     * @param headerContent bytes to include in the header; may be null
     * @param triangleCount number of triangles to be included in the content
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    public void writeHeader(final byte[] headerContent, final int triangleCount) {
        writeHeader(headerContent, triangleCount, out);
    }

    /** Write a triangle to the output using a default attribute value of 0.
     * Callers are responsible for ensuring that the number of triangles written
     * matches the number given in the header.
     *
     * <p>If a normal is given, the vertices are ordered using the right-hand rule,
     * meaning that they will be in a counter-clockwise orientation when looking down
     * the normal. Thus, the given point ordering may not be the ordering used in
     * the written content.</p>
     * @param p1 first point
     * @param p2 second point
     * @param p3 third point
     * @param normal triangle normal; may be null
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D p3,
            final Vector3D normal) {
        writeTriangle(p1, p2, p3, normal, 0);
    }

    /** Write a triangle to the output. Callers are responsible for ensuring
     * that the number of triangles written matches the number given in the header.
     *
     * <p>If a non-zero normal is given, the vertices are ordered using the right-hand rule,
     * meaning that they will be in a counter-clockwise orientation when looking down
     * the normal. If no normal is given, or the given value cannot be normalized, a normal
     * is computed from the triangle vertices, also using the right-hand rule. If this also
     * fails (for example, if the triangle vertices do not define a plane), then the
     * zero vector is used.</p>
     * @param p1 first point
     * @param p2 second point
     * @param p3 third point
     * @param normal triangle normal; may be null
     * @param attributeValue 2-byte STL triangle attribute value
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    public void writeTriangle(final Vector3D p1, final Vector3D p2, final Vector3D p3,
            final Vector3D normal, final int attributeValue) {
        triangleBuffer.rewind();

        putVector(StlUtils.determineNormal(p1, p2, p3, normal));
        putVector(p1);

        if (StlUtils.pointsAreCounterClockwise(p1, p2, p3, normal)) {
            putVector(p2);
            putVector(p3);
        } else {
            putVector(p3);
            putVector(p2);
        }

        triangleBuffer.putShort((short) attributeValue);

        GeometryIOUtils.acceptUnchecked(out::write, triangleBuffer.array());
    }

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

    /** Put all double components of {@code vec} into the internal buffer.
     * @param vec vector to place into the buffer
     */
    private void putVector(final Vector3D vec) {
        triangleBuffer.putFloat((float) vec.getX());
        triangleBuffer.putFloat((float) vec.getY());
        triangleBuffer.putFloat((float) vec.getZ());
    }

    /** Write binary STL header content to the given output stream. If {@code headerContent}
     * is null, the written header will consist entirely of zeros. Otherwise, up to 80 bytes
     * from {@code headerContent} are written to the header, with any remaining bytes of the
     * header filled with zeros.
     * @param headerContent
     * @param triangleCount
     * @param out
     * @throws java.io.UncheckedIOException if an I/O error occurs
     */
    static void writeHeader(final byte[] headerContent, final int triangleCount, final OutputStream out) {
        // write the header
        final byte[] bytes = new byte[StlConstants.BINARY_HEADER_BYTES];
        if (headerContent != null) {
            System.arraycopy(
                    headerContent, 0,
                    bytes, 0,
                    Math.min(headerContent.length, StlConstants.BINARY_HEADER_BYTES));
        }

        GeometryIOUtils.acceptUnchecked(out::write, bytes);

        // write the triangle count number
        ByteBuffer countBuffer = StlUtils.byteBuffer(Integer.BYTES);
        countBuffer.putInt(triangleCount);
        countBuffer.flip();

        GeometryIOUtils.acceptUnchecked(out::write, countBuffer.array());
    }
}
