/*
 * Copyright 2018 Google LLC
 *
 * 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.grafika;

import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.hardware.display.DisplayManager;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;


/**
 * Activity demonstrating the use of MediaProjectionManager and VirtualDisplay to create a
 * recording of the screen and save it as a movie.
 * <p>
 * This activity extends the PlayMovieSurfaceActivity so there is something going on in the activity
 * so the recording is more interesting :).
 * <p>
 * The APIs used require API level 23 (Marshmallow), which at the time of writing this (Jan. 2018)
 * covers ~54% of all Android devices see:
 * https://developer.android.com/about/dashboards/index.html
 */
public class ScreenRecordActivity extends PlayMovieSurfaceActivity {
  private static final String TAG = "ScreenRecordActivity";
  private MediaProjectionManager mediaProjectionManager;
  private MediaProjection mediaProjection;
  private MediaMuxer muxer;
  private Surface inputSurface;
  private MediaCodec videoEncoder;
  private boolean muxerStarted;
  private int trackIndex = -1;

  private static final int REQUEST_CODE_CAPTURE_PERM = 1234;
  private static final String VIDEO_MIME_TYPE = "video/avc";

  private android.media.MediaCodec.Callback encoderCallback;

  @Override
  protected int getContentViewId() {
    return R.layout.activity_screen_record;
  }

  @TargetApi(Build.VERSION_CODES.M)
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
      new AlertDialog.Builder(this)
              .setTitle("Error")
              .setMessage("This activity only works on Marshmallow or later.")
              .setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                  ScreenRecordActivity.this.finish();
                }
              })
              .show();
      return;
    }


    Button toggleRecording = findViewById(R.id.screen_record_button);

    toggleRecording.setOnClickListener(new View.OnClickListener() {
      @RequiresApi(api = Build.VERSION_CODES.M)
      @Override
      public void onClick(View v) {
        if (v.getId() == R.id.screen_record_button) {
          if (muxerStarted) {
            stopRecording();
            ((Button) findViewById(R.id.screen_record_button)).setText(R.string.toggleRecordingOn);
          } else {
            Intent permissionIntent = mediaProjectionManager.createScreenCaptureIntent();
            startActivityForResult(permissionIntent, REQUEST_CODE_CAPTURE_PERM);
            findViewById(R.id.screen_record_button).setEnabled(false);
          }
        }
      }

    });

    mediaProjectionManager = (MediaProjectionManager) getSystemService(
            android.content.Context.MEDIA_PROJECTION_SERVICE);

    encoderCallback = new MediaCodec.Callback() {
      @Override
      public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
        Log.d(TAG, "Input Buffer Avail");
      }

      @Override
      public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
        ByteBuffer encodedData = videoEncoder.getOutputBuffer(index);
        if (encodedData == null) {
          throw new RuntimeException("couldn't fetch buffer at index " + index);
        }

        if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
          info.size = 0;
        }

        if (info.size != 0) {
          if (muxerStarted) {
            encodedData.position(info.offset);
            encodedData.limit(info.offset + info.size);
            muxer.writeSampleData(trackIndex, encodedData, info);
          }
        }

        videoEncoder.releaseOutputBuffer(index, false);

      }

      @Override
      public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e) {
        Log.e(TAG, "MediaCodec " + codec.getName() + " onError:", e);
      }

      @Override
      public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) {
        Log.d(TAG, "Output Format changed");
        if (trackIndex >= 0) {
          throw new RuntimeException("format changed twice");
        }
        trackIndex = muxer.addTrack(videoEncoder.getOutputFormat());
        if (!muxerStarted && trackIndex >= 0) {
          muxer.start();
          muxerStarted = true;
        }
      }
    };
  }

  @Override
  protected void onResume() {
    super.onResume();
    if (!PermissionHelper.hasWriteStoragePermission(this)) {
      PermissionHelper.requestWriteStoragePermission(this);
    }
  }

  @Override
  public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (!PermissionHelper.hasWriteStoragePermission(this)) {
      Toast.makeText(this,
              "Writing to external storage permission is needed to run this application", Toast.LENGTH_LONG).show();
      PermissionHelper.launchPermissionSettings(this);
      finish();
    }
  }

  @RequiresApi(api = Build.VERSION_CODES.M)
  private void startRecording() {
    DisplayManager dm = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE);
    Display defaultDisplay;
    if (dm != null) {
      defaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
    } else {
      throw new IllegalStateException("Cannot display manager?!?");
    }
    if (defaultDisplay == null) {
      throw new RuntimeException("No display found.");
    }

    // Get the display size and density.
    DisplayMetrics metrics = getResources().getDisplayMetrics();
    int screenWidth = metrics.widthPixels;
    int screenHeight = metrics.heightPixels;
    int screenDensity = metrics.densityDpi;

    prepareVideoEncoder(screenWidth, screenHeight);

    try {
      File outputFile = new File(Environment.getExternalStoragePublicDirectory(
              Environment.DIRECTORY_PICTURES) + "/grafika", "Screen-record-" +
              Long.toHexString(System.currentTimeMillis()) + ".mp4");
      if (!outputFile.getParentFile().exists()) {
        outputFile.getParentFile().mkdirs();
      }
      muxer = new MediaMuxer(outputFile.getCanonicalPath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    } catch (IOException ioe) {
      throw new RuntimeException("MediaMuxer creation failed", ioe);
    }


    // Start the video input.
    mediaProjection.createVirtualDisplay("Recording Display", screenWidth,
            screenHeight, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR/* flags */, inputSurface,
            null /* callback */, null /* handler */);
  }

  @RequiresApi(api = Build.VERSION_CODES.M)
  private void prepareVideoEncoder(int width, int height) {
    MediaFormat format = MediaFormat.createVideoFormat(VIDEO_MIME_TYPE, width, height);
    int frameRate = 30; // 30 fps

    // Set some required properties. The media codec may fail if these aren't defined.
    format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
            MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
    format.setInteger(MediaFormat.KEY_BIT_RATE, 6000000); // 6Mbps
    format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
    format.setInteger(MediaFormat.KEY_CAPTURE_RATE, frameRate);
    format.setInteger(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000 / frameRate);
    format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
    format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 1 seconds between I-frames

    // Create a MediaCodec encoder and configure it. Get a Surface we can use for recording into.
    try {
      videoEncoder = MediaCodec.createEncoderByType(VIDEO_MIME_TYPE);
      videoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
      inputSurface = videoEncoder.createInputSurface();
      videoEncoder.setCallback(encoderCallback);
      videoEncoder.start();
    } catch (IOException e) {
      releaseEncoders();
    }
  }

  @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
  private void releaseEncoders() {
    if (muxer != null) {
      if (muxerStarted) {
        muxer.stop();
      }
      muxer.release();
      muxer = null;
      muxerStarted = false;
    }
    if (videoEncoder != null) {
      videoEncoder.stop();
      videoEncoder.release();
      videoEncoder = null;
    }
    if (inputSurface != null) {
      inputSurface.release();
      inputSurface = null;
    }
    if (mediaProjection != null) {
      mediaProjection.stop();
      mediaProjection = null;
    }
    trackIndex = -1;
  }

  @RequiresApi(api = Build.VERSION_CODES.M)
  private void stopRecording() {
    releaseEncoders();
  }

  @RequiresApi(api = Build.VERSION_CODES.M)
  public void onActivityResult(int requestCode, int resultCode, Intent intent) {
    if (REQUEST_CODE_CAPTURE_PERM == requestCode) {
      Button b = findViewById(R.id.screen_record_button);
      b.setEnabled(true);
      if (resultCode == RESULT_OK) {
        mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, intent);
        startRecording();
        b.setText(R.string.toggleRecordingOff);
      } else {
        // user did not grant permissions
        new AlertDialog.Builder(this)
                .setTitle("Error")
                .setMessage("Permission is required to record the screen.")
                .setNeutralButton(android.R.string.ok, null)
                .show();
      }
    }
  }
}