/*
 * Copyright 2020 Yurii Serhiichuk
 *
 * Licensed 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 com.github.xsavikx.androidscreencast.api.recording;

import com.github.xsavikx.androidscreencast.api.recording.atom.AtomType;
import com.github.xsavikx.androidscreencast.exception.IORuntimeException;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

public final class DataAtomOutputStream extends FilterOutputStream {

    private static final long MAC_TIMESTAMP_EPOCH = new GregorianCalendar(1904, Calendar.JANUARY, 1).getTimeInMillis();
    /**
     * The number of bytes written to the data output stream so far. If this counter overflows, it will be wrapped to Integer.MAX_VALUE.
     */
    private long written;

    public DataAtomOutputStream(OutputStream out) {
        super(out);
    }

    /**
     * Increases the written counter by the specified value until it reaches Long.MAX_VALUE.
     */
    private void incCount(int value) {
        long temp = written + value;
        if (temp < 0) {
            temp = Long.MAX_VALUE;
        }
        written = temp;
    }

    /**
     * Returns the current value of the counter <code>written</code>, the number of bytes written to this data output stream so far. If the counter
     * overflows, it will be wrapped to Integer.MAX_VALUE.
     *
     * @return the value of the <code>written</code> field.
     * @see java.io.DataOutputStream#written
     */
    public final long size() {
        return written;
    }

    /**
     * Writes <code>len</code> bytes from the specified byte array starting at offset <code>off</code> to the underlying output stream. If no exception
     * is thrown, the counter <code>written</code> is incremented by <code>len</code> .
     *
     * @param b   the data.
     * @param off the start offset in the data.
     * @param len the number of bytes to write.
     * @throws IOException if an I/O error occurs.
     * @see java.io.FilterOutputStream#out
     */
    @Override
    public synchronized void write(byte[] b, int off, int len) throws IOException {
        out.write(b, off, len);
        incCount(len);
    }

    /**
     * Writes the specified byte (the low eight bits of the argument <code>b</code>) to the underlying output stream. If no exception is thrown, the
     * counter <code>written</code> is incremented by <code>1</code> .
     * <p>
     * Implements the <code>write</code> method of <code>OutputStream</code>.
     *
     * @param b the <code>byte</code> to be written.
     * @throws IOException if an I/O error occurs.
     * @see java.io.FilterOutputStream#out
     */
    @Override
    public synchronized void write(int b) throws IOException {
        out.write(b);
        incCount(1);
    }

    /**
     * Writes a <code>BCD2</code> to the underlying output stream.
     *
     * @param v an <code>int</code> to be written.
     * @throws IOException if an I/O error occurs.
     * @see java.io.FilterOutputStream#out
     */
    void writeBCD2(int v) throws IOException {
        out.write(((v % 100 / 10) << 4) | (v % 10));
        incCount(1);
    }

    /**
     * Writes a <code>BCD4</code> to the underlying output stream.
     *
     * @param v an <code>int</code> to be written.
     * @throws IOException if an I/O error occurs.
     * @see java.io.FilterOutputStream#out
     */
    void writeBCD4(int v) throws IOException {
        out.write(((v % 10000 / 1000) << 4) | (v % 1000 / 100));
        out.write(((v % 100 / 10) << 4) | (v % 10));
        incCount(2);
    }

    /**
     * Writes out a <code>byte</code> to the underlying output stream as a 1-byte value. If no exception is thrown, the counter <code>written</code> is
     * incremented by <code>1</code>.
     *
     * @param v a <code>byte</code> value to be written.
     * @throws IOException if an I/O error occurs.
     * @see java.io.FilterOutputStream#out
     */
    final void writeByte(int v) throws IOException {
        out.write(v);
        incCount(1);
    }

    /**
     * Writes 32-bit fixed-point number divided as 16.16.
     *
     * @param f an <code>int</code> to be written.
     * @throws IOException if an I/O error occurs.
     * @see java.io.FilterOutputStream#out
     */
    void writeFixed16D16(double f) throws IOException {
        double v = (f >= 0) ? f : -f;

        int wholePart = (int) v;
        int fractionPart = (int) ((v - wholePart) * 65536);
        int t = (wholePart << 16) + fractionPart;

        if (f < 0) {
            t = t - 1;
        }
        writeInt(t);
    }

    /**
     * Writes 32-bit fixed-point number divided as 2.30.
     *
     * @param f an <code>int</code> to be written.
     * @throws IOException if an I/O error occurs.
     * @see java.io.FilterOutputStream#out
     */
    void writeFixed2D30(double f) throws IOException {
        double v = (f >= 0) ? f : -f;

        int wholePart = (int) v;
        int fractionPart = (int) ((v - wholePart) * 1073741824);
        int t = (wholePart << 30) + fractionPart;

        if (f < 0) {
            t = t - 1;
        }
        writeInt(t);
    }

    /**
     * Writes 16-bit fixed-point number divided as 8.8.
     *
     * @param f an <code>int</code> to be written.
     * @throws IOException if an I/O error occurs.
     * @see java.io.FilterOutputStream#out
     */
    public void writeFixed8D8(float f) throws IOException {
        float v = (f >= 0) ? f : -f;

        int wholePart = (int) v;
        int fractionPart = (int) ((v - wholePart) * 256);
        int t = (wholePart << 8) + fractionPart;

        if (f < 0) {
            t = t - 1;
        }
        writeUShort(t);
    }

    /**
     * Writes an <code>int</code> to the underlying output stream as four bytes, high byte first. If no exception is thrown, the counter
     * <code>written</code> is incremented by <code>4</code>.
     *
     * @param v an <code>int</code> to be written.
     * @throws IOException if an I/O error occurs.
     * @see java.io.FilterOutputStream#out
     */
    public void writeInt(int v) throws IOException {
        out.write((v >>> 24) & 0xff);
        out.write((v >>> 16) & 0xff);
        out.write((v >>> 8) & 0xff);
        out.write((v >>> 0) & 0xff);
        incCount(4);
    }

    public void writeLong(long v) throws IOException {
        out.write((int) (v >>> 56) & 0xff);
        out.write((int) (v >>> 48) & 0xff);
        out.write((int) (v >>> 40) & 0xff);
        out.write((int) (v >>> 32) & 0xff);
        out.write((int) (v >>> 24) & 0xff);
        out.write((int) (v >>> 16) & 0xff);
        out.write((int) (v >>> 8) & 0xff);
        out.write((int) (v >>> 0) & 0xff);
        incCount(8);
    }

    /**
     * Writes a 32-bit Mac timestamp (seconds since 1902).
     *
     * @param date
     * @throws java.io.IOException
     */
    void writeMacTimestamp(Date date) throws IOException {
        long millis = date.getTime();
        long qtMillis = millis - MAC_TIMESTAMP_EPOCH;
        long qtSeconds = qtMillis / 1000;
        writeUInt(qtSeconds);
    }

    /**
     * Writes a Pascal String.
     *
     * @param s
     * @throws java.io.IOException
     */
    public void writePString(String s) throws IOException {
        if (s.length() > 0xffff) {
            throw new IllegalArgumentException("String too long for PString");
        }
        if (s.length() < 256) {
            out.write(s.length());
        } else {
            out.write(0);
            writeShort(s.length()); // increments +2
        }
        for (int i = 0; i < s.length(); i++) {
            out.write(s.charAt(i));
        }
        incCount(1 + s.length());
    }

    /**
     * Writes a Pascal String padded to the specified fixed size in bytes
     *
     * @param s
     * @param length the fixed size in bytes
     * @throws java.io.IOException
     */
    void writePString(String s, int length) throws IOException {
        if (s.length() > length) {
            throw new IllegalArgumentException("String too long for PString of length " + length);
        }
        if (s.length() < 256) {
            out.write(s.length());
        } else {
            out.write(0);
            writeShort(s.length()); // increments +2
        }
        for (int i = 0; i < s.length(); i++) {
            out.write(s.charAt(i));
        }

        // write pad bytes
        for (int i = 1 + s.length(); i < length; i++) {
            out.write(0);
        }

        incCount(length);
    }

    /**
     * Writes a signed 16 bit integer value.
     *
     * @param v The value
     * @throws java.io.IOException
     */
    void writeShort(int v) throws IOException {
        out.write((v >> 8) & 0xff);
        out.write((v >>> 0) & 0xff);
        incCount(2);
    }

    /**
     * Writes an Atom Type identifier (4 bytes).
     *
     * @param type A string with a length of 4 characters.
     */
    private void writeType(String type) throws IOException {
        checkArgument(type.length() == 4, "Type string must have exactly 4 characters. type=%s", type);
        try {
            out.write(type.getBytes(StandardCharsets.US_ASCII), 0, 4);
            incCount(4);
        } catch (UnsupportedEncodingException e) {
            throw new InternalError(e.toString());
        }
    }

    /**
     * Writes an Atom Type identifier (4 bytes).
     *
     * @param atomType AtomType to be written
     */
    public void writeType(AtomType atomType) throws IOException {
        checkNotNull(atomType, "AtomType should not be null");
        writeType(atomType.toStringRepresentation());
    }

    /**
     * Writes an unsigned 32 bit integer value.
     *
     * @param v The value
     * @throws java.io.IOException
     */
    public void writeUInt(long v) throws IOException {
        out.write((int) ((v >>> 24) & 0xff));
        out.write((int) ((v >>> 16) & 0xff));
        out.write((int) ((v >>> 8) & 0xff));
        out.write((int) ((v >>> 0) & 0xff));
        incCount(4);
    }

    void writeUShort(int v) throws IOException {
        out.write((v >> 8) & 0xff);
        out.write((v >>> 0) & 0xff);
        incCount(2);
    }

    @Override
    public void close() {
        try {
            flush();
        } catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }
}
