/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * 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.android.cts.verifier.audioquality;

import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioTrack;
import android.os.Environment;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * File and data utilities for the Audio Verifier.
 */
public class Utils {
    public static final String TAG = "AudioQualityVerifier";
    public static final ByteOrder BYTE_ORDER = ByteOrder.LITTLE_ENDIAN;

    /**
     *  Time delay.
     *
     *  @param ms time in milliseconds to pause for
     */
    public static void delay(int ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {}
    }

    public static String getExternalDir(Context context, Object exp) {
        checkExternalStorageAvailable();
        // API level 8:
        // return context.getExternalFilesDir(null).getAbsolutePath();
        // API level < 8:
        String dir = Environment.getExternalStorageDirectory().getAbsolutePath();
        dir += "/Android/data/" + exp.getClass().getPackage().getName() + "/files";
        checkMakeDir(dir);
        return dir;
    }

    private static void checkExternalStorageAvailable() {
        String state = Environment.getExternalStorageState();
        if (!Environment.MEDIA_MOUNTED.equals(state)) {
            // TODO: Raise a Toast and supply internal storage instead
        }
    }

    private static void checkMakeDir(String dir) {
        File f = new File(dir);
        if (!f.exists()) {
            f.mkdirs();
        }
    }

    /**
     * Convert a string (e.g. the name of an experiment) to something more suitable
     * for use as a filename.
     *
     * @param s the string to be cleaned
     * @return a string which is similar (not necessarily unique) and safe for filename use
     */
    public static String cleanString(String s) {
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            if (Character.isWhitespace(c)) sb.append('_');
            else if (Character.isLetterOrDigit(c)) sb.append(c);
        }
        return sb.toString();
    }

    /**
     * Convert a sub-array from bytes to shorts.
     *
     * @param data array of bytes to be converted
     * @param start first index to convert (should be even)
     * @param len number of bytes to convert (should be even)
     * @return an array of half the length, containing shorts
     */
    public static short[] byteToShortArray(byte[] data, int start, int len) {
        short[] samples = new short[len / 2];
        ByteBuffer bb = ByteBuffer.wrap(data, start, len);
        bb.order(BYTE_ORDER);
        for (int i = 0; i < len / 2; i++) {
            samples[i] = bb.getShort();
        }
        return samples;
    }

    /**
     * Convert a byte array to an array of shorts (suitable for the phone test
     * native library's audio sample data).
     *
     * @param data array of bytes to be converted
     * @return an array of half the length, containing shorts
     */
    public static short[] byteToShortArray(byte[] data) {
        int len = data.length / 2;
        short[] samples = new short[len];
        ByteBuffer bb = ByteBuffer.wrap(data);
        bb.order(BYTE_ORDER);
        for (int i = 0; i < len; i++) {
            samples[i] = bb.getShort();
        }
        return samples;
    }

    /**
     * Convert a short array (as returned by the phone test native library)
     * to an array of bytes.
     *
     * @param samples array of shorts to be converted
     * @return an array of twice the length, broken out into bytes
     */
    public static byte[] shortToByteArray(short[] samples) {
        int len = samples.length;
        byte[] data = new byte[len * 2];
        ByteBuffer bb = ByteBuffer.wrap(data);
        bb.order(BYTE_ORDER);
        for (int i = 0; i < len; i++) {
            bb.putShort(samples[i]);
        }
        return data;
    }

    /**
     * Scale the amplitude of an array of samples.
     *
     * @param samples to be scaled
     * @param db decibels to scale up by (may be negative)
     * @return the scaled samples
     */
    public static short[] scale(short[] samples, float db) {
        short[] scaled = new short[samples.length];
        // Convert decibels to a linear ratio:
        double ratio = Math.pow(10.0, db / 20.0);
        for (int i = 0; i < samples.length; i++) {
            scaled[i] = (short) (samples[i] * ratio);
        }
        return scaled;
    }

    /**
     * Read an entire file into memory.
     *
     * @param filename to be opened
     * @return the file data, or null in case of error
     */
    private static byte[] readFile(String filename) {
        FileInputStream fis;
        try {
            fis = new FileInputStream(filename);
        } catch (FileNotFoundException e1) {
            return null;
        }

        File file = new File(filename);
        int len = (int) file.length();
        byte[] data = new byte[len];

        int pos = 0;
        int bytes = 0;
        int count;
        while (pos < len) {
            try {
                count = fis.read(data, pos, len - pos);
            } catch (IOException e) {
                return null;
            }
            if (count < 1) return null;
            pos += count;
        }

        try {
            fis.close();
        } catch (IOException e) {}
        return data;
    }

    /**
     * Read an entire file from an InputStream.
     * Useful as AssetManager returns these.
     *
     * @param stream to read file contents from
     * @return file data
     */
    public static byte[] readFile(InputStream stream) {
        final int CHUNK_SIZE = 10000;
        ByteArrayBuilder bab = new ByteArrayBuilder();
        byte[] buf = new byte[CHUNK_SIZE];
        int count;
        while (true) {
            try {
                count = stream.read(buf, 0, CHUNK_SIZE);
            } catch (IOException e) {
                return null;
            }
            if (count == -1) break; // EOF
            bab.append(buf, count);
        }
        return bab.toByteArray();
    }

    /**
     * Save binary (audio) data to a file.
     *
     * @param filename to be written
     * @param data contents
     */
    public static void saveFile(String filename, byte[] data) {
        try {
            FileOutputStream fos = new FileOutputStream(filename);
            fos.write(data);
            fos.close();
        } catch (IOException e) {
            Log.e(TAG, "Error writing to file " + filename);
        }
    }

    /**
     * Push an entire array of audio data to an AudioTrack.
     *
     * @param at destination
     * @param data to be written
     * @return true if successful, or false on error
     */
    public static boolean writeAudio(AudioTrack at, byte[] data) {
        int pos = 0;
        int len = data.length;
        int count;

        while (pos < len) {
            count = at.write(data, pos, len - pos);
            if (count < 0) return false;
            pos += count;
        }
        at.flush();
        return true;
    }

    /**
     * Determine the number of audio samples in a file
     *
     * @param filename file containing audio data
     * @return number of samples in file, or 0 if file does not exist
     */
    public static int duration(String filename) {
        File file = new File(filename);
        int len = (int) file.length();
        return len / AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
    }

    /**
     * Determine the number of audio samples in a stimulus asset
     *
     * @param context to look up stimulus
     * @param stimNum index number of this stimulus
     * @return number of samples in stimulus
     */
    public static int duration(Context context, int stimNum) {
        byte[] data = AudioAssets.getStim(context, stimNum);
        return data.length / AudioQualityVerifierActivity.BYTES_PER_SAMPLE;
    }

    public static void playRawFile(String filename) {
        byte[] data = readFile(filename);
        if (data == null) {
            Log.e(TAG, "Cannot read " + filename);
            return;
        }
        playRaw(data);
    }

    public static void playStim(Context context, int stimNum) {
        Utils.playRaw(getStim(context, stimNum));
    }

    public static byte[] getStim(Context context, int stimNum) {
        return AudioAssets.getStim(context, stimNum);
    }

    public static byte[] getPinkNoise(Context context, int ampl, int duration) {
        return AudioAssets.getPinkNoise(context, ampl, duration);
    }

    public static void playRaw(byte[] data) {
        Log.i(TAG, "Playing " + data.length + " bytes of pre-recorded audio");
        AudioTrack at = new AudioTrack(AudioQualityVerifierActivity.PLAYBACK_STREAM, AudioQualityVerifierActivity.SAMPLE_RATE,
                AudioFormat.CHANNEL_OUT_MONO, AudioQualityVerifierActivity.AUDIO_FORMAT,
                data.length, AudioTrack.MODE_STREAM);
        writeAudio(at, data);
        at.play();
    }

    /**
     * The equivalent of a simplified StringBuilder, but for bytes.
     */
    public static class ByteArrayBuilder {
        private byte[] buf;
        private int capacity, size;

        public ByteArrayBuilder() {
            capacity = 100;
            size = 0;
            buf = new byte[capacity];
        }

        public void append(byte[] b, int nBytes) {
            if (nBytes < 1) return;
            if (size + nBytes > capacity) expandCapacity(size + nBytes);
            System.arraycopy(b, 0, buf, size, nBytes);
            size += nBytes;
        }

        public byte[] toByteArray() {
            byte[] result = new byte[size];
            System.arraycopy(buf, 0, result, 0, size);
            return result;
        }

        private void expandCapacity(int min) {
            capacity *= 2;
            if (capacity < min) capacity = min;
            byte[] expanded = new byte[capacity];
            System.arraycopy(buf, 0, expanded, 0, size);
            buf = expanded;
        }
    }
}
