// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.components.runtime;

import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.FileUtil;
import android.media.MediaRecorder;
import android.media.MediaRecorder.OnErrorListener;
import android.media.MediaRecorder.OnInfoListener;
import android.os.Environment;
import android.util.Log;

import java.io.IOException;

/**
 * Multimedia component that records audio using
 * {@link android.media.MediaRecorder}.
 *
 */
@DesignerComponent(version = YaVersion.SOUND_RECORDER_COMPONENT_VERSION,
    description = "<p>Multimedia component that records audio.</p>",
    category = ComponentCategory.MEDIA,
    nonVisible = true,
    iconName = "images/soundRecorder.png")
@SimpleObject
@UsesPermissions(permissionNames = "android.permission.RECORD_AUDIO")
public final class SoundRecorder extends AndroidNonvisibleComponent
    implements Component, OnErrorListener, OnInfoListener {

  private static final String TAG = "SoundRecorder";

  // the path to the savedRecording
  // if it is the null string, the recorder will generate a path
  // note that this is also initialized to "" in the designer
  private String savedRecording = "";


  /**
   * This class encapsulates the required state during recording.
   */
  private class RecordingController {
    final MediaRecorder recorder;

    // file is the same as savedRecording, but we'll keep it local to the
    // RecordingController for future flexibility
     final String file;

    RecordingController(String savedRecording) throws IOException {
      // pick a pathname if none was specified
      file = (savedRecording.equals("")) ?
          FileUtil.getRecordingFile("3gp").getAbsolutePath() :
            savedRecording;

      recorder = new MediaRecorder();
      recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
      recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
      recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
      Log.i(TAG, "Setting output file to " + file);
      recorder.setOutputFile(file);
      Log.i(TAG, "preparing");
      recorder.prepare();
      recorder.setOnErrorListener(SoundRecorder.this);
      recorder.setOnInfoListener(SoundRecorder.this);
    }

    void start() throws IllegalStateException {
      Log.i(TAG, "starting");
      try {
      recorder.start();
      } catch (IllegalStateException e) {
        // This is the error produced when there are two recorders running.
        // There might be other causes, but we don't know them.
        // Using Log.e will log a stack trace, so we can investigate
        Log.e(TAG, "got IllegalStateException. Are there two recorders running?", e);
        // Pass back a message detail for dispatchErrorOccurred to
        // show at user level
        throw (new IllegalStateException("Is there another recording running?"));
      }
    }

    void stop() {
      recorder.setOnErrorListener(null);
      recorder.setOnInfoListener(null);
      recorder.stop();
      recorder.reset();
      recorder.release();
    }
  }

  /*
   * This is null when not recording, and contains the active RecordingState
   * when recording.
   */
  private RecordingController controller;

  public SoundRecorder(final ComponentContainer container) {
    super(container.$form());
  }


  /**
   * Returns the path to the saved recording
   *
   * @return  savedRecording path to recording
   */
  @SimpleProperty(
      description = "Specifies the path to the file where the recording is stored. " +
          "If this proprety is the empty string, then starting a recording will create a file in " +
          "an appropriate location.",
          category = PropertyCategory.BEHAVIOR)
  public String SavedRecording() {
    return savedRecording;
  }

  /**
   * Specifies the path to the saved recording displayed by the label.
   *
   * @param pathName  path to saved recording
   */
  @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING,
      defaultValue = "")
  @SimpleProperty
  public void SavedRecording(String pathName) {
    savedRecording = pathName;
  }

  /**
   * Starts recording.
   */
  @SimpleFunction
  public void Start() {
    if (controller != null) {
      Log.i(TAG, "Start() called, but already recording to " + controller.file);
      return;
    }
    Log.i(TAG, "Start() called");
    if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
      form.dispatchErrorOccurredEvent(
          this, "Start", ErrorMessages.ERROR_MEDIA_EXTERNAL_STORAGE_NOT_AVAILABLE);
      return;
    }
    try {
      controller = new RecordingController(savedRecording);
    } catch (Throwable t) {
      form.dispatchErrorOccurredEvent(
          this, "Start", ErrorMessages.ERROR_SOUND_RECORDER_CANNOT_CREATE, t.getMessage());
      return;
    }
    try {
      controller.start();
    } catch (Throwable t) {
      // I'm commenting the next line out because stop can throw an error, and
      // it's not clear to me how to handle that.
      // controller.stop();
      controller = null;
      form.dispatchErrorOccurredEvent(
          this, "Start", ErrorMessages.ERROR_SOUND_RECORDER_CANNOT_CREATE, t.getMessage());
      return;
    }
    StartedRecording();
  }

  @Override
  public void onError(MediaRecorder affectedRecorder, int what, int extra) {
    if (controller == null || affectedRecorder != controller.recorder) {
      Log.w(TAG, "onError called with wrong recorder. Ignoring.");
      return;
    }
    form.dispatchErrorOccurredEvent(this, "onError", ErrorMessages.ERROR_SOUND_RECORDER);
    try {
      controller.stop();
    } catch (Throwable e) {
      Log.w(TAG, e.getMessage());
    } finally {
      controller = null;
      StoppedRecording();
    }
  }

  @Override
  public void onInfo(MediaRecorder affectedRecorder, int what, int extra) {
    if (controller == null || affectedRecorder != controller.recorder) {
      Log.w(TAG, "onInfo called with wrong recorder. Ignoring.");
      return;
    }
    Log.i(TAG, "Recoverable condition while recording. Will attempt to stop normally.");
    controller.recorder.stop();
  }

  /**
   * Stops recording.
   */
  @SimpleFunction
  public void Stop() {
    if (controller == null) {
      Log.i(TAG, "Stop() called, but already stopped.");
      return;
    }
    try {
      Log.i(TAG, "Stop() called");
      Log.i(TAG, "stopping");
      controller.stop();
      Log.i(TAG, "Firing AfterSoundRecorded with " + controller.file);
      AfterSoundRecorded(controller.file);
    } catch (Throwable t) {
      form.dispatchErrorOccurredEvent(this, "Stop", ErrorMessages.ERROR_SOUND_RECORDER);
    } finally {
      controller = null;
      StoppedRecording();
    }
  }

  @SimpleEvent(description = "Provides the location of the newly created sound.")
  public void AfterSoundRecorded(final String sound) {
    EventDispatcher.dispatchEvent(this, "AfterSoundRecorded", sound);
  }

  @SimpleEvent(description = "Indicates that the recorder has started, and can be stopped.")
  public void StartedRecording() {
    EventDispatcher.dispatchEvent(this, "StartedRecording");
  }

  @SimpleEvent(description = "Indicates that the recorder has stopped, and can be started again.")
  public void StoppedRecording() {
    EventDispatcher.dispatchEvent(this, "StoppedRecording");
  }
}
