/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.devsupport;

import javax.annotation.Nullable;

import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.net.Uri;
import android.os.AsyncTask;
import android.text.SpannedString;
import android.text.method.LinkMovementMethod;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.facebook.common.logging.FLog;
import com.facebook.infer.annotation.Assertions;
import com.facebook.react.R;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.devsupport.StackTraceHelper.StackFrame;
import com.facebook.react.devsupport.RedBoxHandler.ReportCompletedListener;

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import org.json.JSONObject;

/**
 * Dialog for displaying JS errors in an eye-catching form (red box).
 */
/* package */ class RedBoxDialog extends Dialog implements AdapterView.OnItemClickListener {

  private final DevSupportManager mDevSupportManager;
  private final DoubleTapReloadRecognizer mDoubleTapReloadRecognizer;
  private final @Nullable RedBoxHandler mRedBoxHandler;

  private ListView mStackView;
  private Button mReloadJsButton;
  private Button mDismissButton;
  private Button mCopyToClipboardButton;
  private @Nullable Button mReportButton;
  private @Nullable TextView mReportTextView;
  private @Nullable ProgressBar mLoadingIndicator;
  private @Nullable View mLineSeparator;
  private boolean isReporting = false;

  private ReportCompletedListener mReportCompletedListener = new ReportCompletedListener() {
    @Override
    public void onReportSuccess(final SpannedString spannedString) {
      isReporting = false;
      Assertions.assertNotNull(mReportButton).setEnabled(true);
      Assertions.assertNotNull(mLoadingIndicator).setVisibility(View.GONE);
      Assertions.assertNotNull(mReportTextView).setText(spannedString);
    }
    @Override
    public void onReportError(final SpannedString spannedString) {
      isReporting = false;
      Assertions.assertNotNull(mReportButton).setEnabled(true);
      Assertions.assertNotNull(mLoadingIndicator).setVisibility(View.GONE);
      Assertions.assertNotNull(mReportTextView).setText(spannedString);
    }
  };

  private View.OnClickListener mReportButtonOnClickListener = new View.OnClickListener() {
    @Override
    public void onClick(View view) {
      if (mRedBoxHandler == null || !mRedBoxHandler.isReportEnabled() || isReporting) {
        return;
      }
      isReporting = true;
      Assertions.assertNotNull(mReportTextView).setText("Reporting...");
      Assertions.assertNotNull(mReportTextView).setVisibility(View.VISIBLE);
      Assertions.assertNotNull(mLoadingIndicator).setVisibility(View.VISIBLE);
      Assertions.assertNotNull(mLineSeparator).setVisibility(View.VISIBLE);
      Assertions.assertNotNull(mReportButton).setEnabled(false);

      String title = Assertions.assertNotNull(mDevSupportManager.getLastErrorTitle());
      StackFrame[] stack = Assertions.assertNotNull(mDevSupportManager.getLastErrorStack());
      String sourceUrl = mDevSupportManager.getSourceUrl();

      mRedBoxHandler.reportRedbox(
        title,
        stack,
        sourceUrl,
        Assertions.assertNotNull(mReportCompletedListener));
    }
  };

  private static class StackAdapter extends BaseAdapter {
    private static final int VIEW_TYPE_COUNT = 2;
    private static final int VIEW_TYPE_TITLE = 0;
    private static final int VIEW_TYPE_STACKFRAME = 1;

    private final String mTitle;
    private final StackFrame[] mStack;

    private static class FrameViewHolder {
      private final TextView mMethodView;
      private final TextView mFileView;

      private FrameViewHolder(View v) {
        mMethodView = (TextView) v.findViewById(R.id.rn_frame_method);
        mFileView = (TextView) v.findViewById(R.id.rn_frame_file);
      }
    }

    public StackAdapter(String title, StackFrame[] stack) {
      mTitle = title;
      mStack = stack;
    }

    @Override
    public boolean areAllItemsEnabled() {
      return false;
    }

    @Override
    public boolean isEnabled(int position) {
      return position > 0;
    }

    @Override
    public int getCount() {
      return mStack.length + 1;
    }

    @Override
    public Object getItem(int position) {
      return position == 0 ? mTitle : mStack[position - 1];
    }

    @Override
    public long getItemId(int position) {
      return position;
    }

    @Override
    public int getViewTypeCount() {
      return VIEW_TYPE_COUNT;
    }

    @Override
    public int getItemViewType(int position) {
      return position == 0 ? VIEW_TYPE_TITLE : VIEW_TYPE_STACKFRAME;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
      if (position == 0) {
        TextView title = convertView != null
            ? (TextView) convertView
            : (TextView) LayoutInflater.from(parent.getContext())
                .inflate(R.layout.redbox_item_title, parent, false);
        title.setText(mTitle);
        return title;
      } else {
        if (convertView == null) {
          convertView = LayoutInflater.from(parent.getContext())
              .inflate(R.layout.redbox_item_frame, parent, false);
          convertView.setTag(new FrameViewHolder(convertView));
        }
        StackFrame frame = mStack[position - 1];
        FrameViewHolder holder = (FrameViewHolder) convertView.getTag();
        holder.mMethodView.setText(frame.getMethod());
        holder.mFileView.setText(StackTraceHelper.formatFrameSource(frame));
        return convertView;
      }
    }
  }

  private static class OpenStackFrameTask extends AsyncTask<StackFrame, Void, Void> {
    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

    private final DevSupportManager mDevSupportManager;

    private OpenStackFrameTask(DevSupportManager devSupportManager) {
      mDevSupportManager = devSupportManager;
    }

    @Override
    protected Void doInBackground(StackFrame... stackFrames) {
      try {
        String openStackFrameUrl =
            Uri.parse(mDevSupportManager.getSourceUrl()).buildUpon()
                .path("/open-stack-frame")
                .query(null)
                .build()
                .toString();
        OkHttpClient client = new OkHttpClient();
        for (StackFrame frame: stackFrames) {
          String payload = stackFrameToJson(frame).toString();
          RequestBody body = RequestBody.create(JSON, payload);
          Request request = new Request.Builder().url(openStackFrameUrl).post(body).build();
          client.newCall(request).execute();
        }
      } catch (Exception e) {
        FLog.e(ReactConstants.TAG, "Could not open stack frame", e);
      }
      return null;
    }

    private static JSONObject stackFrameToJson(StackFrame frame) {
      return new JSONObject(
          MapBuilder.of(
              "file", frame.getFile(),
              "methodName", frame.getMethod(),
              "lineNumber", frame.getLine(),
              "column", frame.getColumn()
          ));
    }
  }

  private static class CopyToHostClipBoardTask extends AsyncTask<String, Void, Void> {
    private final DevSupportManager mDevSupportManager;

    private CopyToHostClipBoardTask(DevSupportManager devSupportManager) {
      mDevSupportManager = devSupportManager;
    }

    @Override
    protected Void doInBackground(String... clipBoardString) {
      try {
        String sendClipBoardUrl =
            Uri.parse(mDevSupportManager.getSourceUrl()).buildUpon()
                .path("/copy-to-clipboard")
                .query(null)
                .build()
                .toString();
        for (String string: clipBoardString) {
          OkHttpClient client = new OkHttpClient();
          RequestBody body = RequestBody.create(null, string);
          Request request = new Request.Builder().url(sendClipBoardUrl).post(body).build();
          client.newCall(request).execute();
        }
      } catch (Exception e) {
        FLog.e(ReactConstants.TAG, "Could not copy to the host clipboard", e);
      }
      return null;
    }
  }

  protected RedBoxDialog(
    Context context,
    DevSupportManager devSupportManager,
    @Nullable RedBoxHandler redBoxHandler) {
    super(context, R.style.Theme_Catalyst_RedBox);

    requestWindowFeature(Window.FEATURE_NO_TITLE);

    setContentView(R.layout.redbox_view);

    mDevSupportManager = devSupportManager;
    mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
    mRedBoxHandler = redBoxHandler;

    mStackView = (ListView) findViewById(R.id.rn_redbox_stack);
    mStackView.setOnItemClickListener(this);

    mReloadJsButton = (Button) findViewById(R.id.rn_redbox_reload_button);
    mReloadJsButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        mDevSupportManager.handleReloadJS();
      }
    });
    mDismissButton = (Button) findViewById(R.id.rn_redbox_dismiss_button);
    mDismissButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        dismiss();
      }
    });
    mCopyToClipboardButton = (Button) findViewById(R.id.rn_redbox_copy_button);
    mCopyToClipboardButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        String title = mDevSupportManager.getLastErrorTitle();
        StackFrame[] stack = mDevSupportManager.getLastErrorStack();
        Assertions.assertNotNull(title);
        Assertions.assertNotNull(stack);
        new CopyToHostClipBoardTask(mDevSupportManager).executeOnExecutor(
            AsyncTask.THREAD_POOL_EXECUTOR,
            StackTraceHelper.formatStackTrace(title, stack));
      }
    });

    if (mRedBoxHandler != null && mRedBoxHandler.isReportEnabled()) {
      mLoadingIndicator = (ProgressBar) findViewById(R.id.rn_redbox_loading_indicator);
      mLineSeparator = (View) findViewById(R.id.rn_redbox_line_separator);
      mReportTextView = (TextView) findViewById(R.id.rn_redbox_report_label);
      mReportTextView.setMovementMethod(LinkMovementMethod.getInstance());
      mReportTextView.setHighlightColor(Color.TRANSPARENT);
      mReportButton = (Button) findViewById(R.id.rn_redbox_report_button);
      mReportButton.setOnClickListener(mReportButtonOnClickListener);
    }
  }

  public void setExceptionDetails(String title, StackFrame[] stack) {
    mStackView.setAdapter(new StackAdapter(title, stack));
  }

  /**
   * Show the report button, hide the report textview and the loading indicator.
   */
  public void resetReporting(boolean enabled) {
    if (mRedBoxHandler == null || !mRedBoxHandler.isReportEnabled()) {
      return;
    }
    isReporting = false;
    Assertions.assertNotNull(mReportTextView).setVisibility(View.GONE);
    Assertions.assertNotNull(mLoadingIndicator).setVisibility(View.GONE);
    Assertions.assertNotNull(mLineSeparator).setVisibility(View.GONE);
    Assertions.assertNotNull(mReportButton).setVisibility(
      enabled ? View.VISIBLE : View.GONE);
    Assertions.assertNotNull(mReportButton).setEnabled(true);
  }

  @Override
  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    new OpenStackFrameTask(mDevSupportManager).executeOnExecutor(
        AsyncTask.THREAD_POOL_EXECUTOR,
        (StackFrame) mStackView.getAdapter().getItem(position));
  }

  @Override
  public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_MENU) {
      mDevSupportManager.showDevOptionsDialog();
      return true;
    }
    if (mDoubleTapReloadRecognizer.didDoubleTapR(keyCode, getCurrentFocus())) {
      mDevSupportManager.handleReloadJS();
    }
    return super.onKeyUp(keyCode, event);
  }
}
