/*
 * Copyright (C) 2017 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.googlecode.android_scripting.activity;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.media.AudioManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Selection;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.style.UnderlineSpan;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import com.googlecode.android_scripting.BaseApplication;
import com.googlecode.android_scripting.Constants;
import com.googlecode.android_scripting.FileUtils;
import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.R;
import com.googlecode.android_scripting.ScriptStorageAdapter;
import com.googlecode.android_scripting.interpreter.Interpreter;
import com.googlecode.android_scripting.interpreter.InterpreterConfiguration;
import com.googlecode.android_scripting.service.ScriptingLayerService;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A text editor for scripts.
 *
 */
public class ScriptEditor extends Activity implements OnClickListener {
  private static final int DIALOG_FIND_REPLACE = 2;
  private static final int DIALOG_LINE = 1;
  private EditText mNameText;
  private EditText mContentText;
  private boolean mScheduleMoveLeft;
  private String mLastSavedContent;
  private SharedPreferences mPreferences;
  private InterpreterConfiguration mConfiguration;
  private ContentTextWatcher mWatcher;
  private EditHistory mHistory;
  private File mScript;
  private EditText mLineNo;

  private boolean mIsUndoOrRedo = false;
  private boolean mEnableAutoClose;
  private boolean mAutoIndent;

  private EditText mSearchFind;
  private EditText mSearchReplace;
  private CheckBox mSearchCase;
  private CheckBox mSearchWord;
  private CheckBox mSearchAll;
  private CheckBox mSearchStart;

  private static enum MenuId {
    SAVE, SAVE_AND_RUN, PREFERENCES, API_BROWSER, HELP, SHARE, GOTO, SEARCH;
    public int getId() {
      return ordinal() + Menu.FIRST;
    }
  }

  private static enum RequestCode {
    RPC_HELP
  }

  private int readIntPref(String key, int defaultValue, int maxValue) {
    int val;
    try {
      val = Integer.parseInt(mPreferences.getString(key, Integer.toString(defaultValue)));
    } catch (NumberFormatException e) {
      val = defaultValue;
    }
    val = Math.max(0, Math.min(val, maxValue));
    return val;
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.script_editor);
    mNameText = (EditText) findViewById(R.id.script_editor_title);
    mContentText = (EditText) findViewById(R.id.script_editor_body);
    mHistory = new EditHistory();
    mWatcher = new ContentTextWatcher(mHistory);
    mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
    updatePreferences();

    mScript = new File(getIntent().getStringExtra(Constants.EXTRA_SCRIPT_PATH));
    mNameText.setText(mScript.getName());
    mNameText.setSelected(true);
    // NOTE: This appears to be the only way to get Android to put the cursor to the beginning of
    // the EditText field.
    mNameText.setSelection(1);
    mNameText.extendSelection(0);
    mNameText.setSelection(0);
    mLastSavedContent = getIntent().getStringExtra(Constants.EXTRA_SCRIPT_CONTENT);
    if (mLastSavedContent == null) {
      try {
        mLastSavedContent = FileUtils.readToString(mScript);
      } catch (IOException e) {
        Log.e("Failed to read script.", e);
        mLastSavedContent = "";
      } finally {
      }
    }

    mContentText.setText(mLastSavedContent);
    InputFilter[] oldFilters = mContentText.getFilters();
    List<InputFilter> filters = new ArrayList<InputFilter>(oldFilters.length + 1);
    filters.addAll(Arrays.asList(oldFilters));
    filters.add(new ContentInputFilter());
    mContentText.setFilters(filters.toArray(oldFilters));
    mContentText.addTextChangedListener(mWatcher);
    mConfiguration = ((BaseApplication) getApplication()).getInterpreterConfiguration();
    // Disables volume key beep.
    setVolumeControlStream(AudioManager.STREAM_MUSIC);
    mLineNo = new EditText(this);
    mLineNo.setInputType(InputType.TYPE_CLASS_NUMBER);
    int lastLocation = mPreferences.getInt("lasteditpos." + mScript, -1);
    if (lastLocation >= 0) {
      mContentText.requestFocus();
      mContentText.setSelection(lastLocation);
    }
  }

  @Override
  protected void onResume() {
    super.onResume();
    updatePreferences();
  }

  private void updatePreferences() {
    mContentText.setTextSize(readIntPref("editor_fontsize", 10, 30));
    mEnableAutoClose = mPreferences.getBoolean("enableAutoClose", true);
    mAutoIndent = mPreferences.getBoolean("editor_auto_indent", false);
    mContentText.setHorizontallyScrolling(mPreferences.getBoolean("editor_no_wrap", false));
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    menu.add(0, MenuId.SAVE.getId(), 0, "Save & Exit").setIcon(android.R.drawable.ic_menu_save);
    menu.add(0, MenuId.SAVE_AND_RUN.getId(), 0, "Save & Run").setIcon(
        android.R.drawable.ic_media_play);
    menu.add(0, MenuId.PREFERENCES.getId(), 0, "Preferences").setIcon(
        android.R.drawable.ic_menu_preferences);
    menu.add(0, MenuId.API_BROWSER.getId(), 0, "API Browser").setIcon(
        android.R.drawable.ic_menu_info_details);
    menu.add(0, MenuId.SHARE.getId(), 0, "Share").setIcon(android.R.drawable.ic_menu_share);
    menu.add(0, MenuId.GOTO.getId(), 0, "GoTo").setIcon(android.R.drawable.ic_menu_directions);
    menu.add(0, MenuId.SEARCH.getId(), 0, "Find").setIcon(android.R.drawable.ic_menu_search);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId() == MenuId.SAVE.getId()) {
      save();
      finish();
    } else if (item.getItemId() == MenuId.SAVE_AND_RUN.getId()) {
      save();
      Interpreter interpreter =
          mConfiguration.getInterpreterForScript(mNameText.getText().toString());
      if (interpreter != null) { // We may be editing an unknown type.
        Intent intent = new Intent(this, ScriptingLayerService.class);
        intent.setAction(Constants.ACTION_LAUNCH_FOREGROUND_SCRIPT);
        intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mScript.getAbsolutePath());
        startService(intent);
      } else {
        // TODO(damonkohler): Should remove menu option.
        Toast.makeText(this, "Can't run this type.", Toast.LENGTH_SHORT).show();
      }
      finish();
    } else if (item.getItemId() == MenuId.PREFERENCES.getId()) {
      startActivity(new Intent(this, Preferences.class));
    } else if (item.getItemId() == MenuId.API_BROWSER.getId()) {
      Intent intent = new Intent(this, ApiBrowser.class);
      intent.putExtra(Constants.EXTRA_SCRIPT_PATH, mNameText.getText().toString());
      intent.putExtra(Constants.EXTRA_INTERPRETER_NAME,
          mConfiguration.getInterpreterForScript(mNameText.getText().toString()).getName());
      intent.putExtra(Constants.EXTRA_SCRIPT_TEXT, mContentText.getText().toString());
      startActivityForResult(intent, RequestCode.RPC_HELP.ordinal());
    } else if (item.getItemId() == MenuId.SHARE.getId()) {
      Intent intent = new Intent(Intent.ACTION_SEND);
      intent.putExtra(Intent.EXTRA_TEXT, mContentText.getText().toString());
      intent.putExtra(Intent.EXTRA_SUBJECT, "Share " + mNameText.getText().toString());
      intent.setType("text/plain");
      startActivity(Intent.createChooser(intent, "Send Script to:"));
    } else if (item.getItemId() == MenuId.GOTO.getId()) {
      showDialog(DIALOG_LINE);
    } else if (item.getItemId() == MenuId.SEARCH.getId()) {
      showDialog(DIALOG_FIND_REPLACE);
    }
    return super.onOptionsItemSelected(item);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    RequestCode request = RequestCode.values()[requestCode];

    if (resultCode == RESULT_OK) {
      switch (request) {
      case RPC_HELP:
        String rpcText = data.getStringExtra(Constants.EXTRA_RPC_HELP_TEXT);
        insertContent(rpcText);
        break;
      default:
        break;
      }
    } else {
      switch (request) {
      case RPC_HELP:
        break;
      default:
        break;
      }
    }
  }

  private void save() {
    int start = mContentText.getSelectionStart();
    mLastSavedContent = mContentText.getText().toString();
    mScript = new File(mScript.getParent(), mNameText.getText().toString());
    ScriptStorageAdapter.writeScript(mScript, mLastSavedContent);
    Toast.makeText(this, "Saved " + mNameText.getText().toString(), Toast.LENGTH_SHORT).show();
    Editor e = mPreferences.edit();
    e.putInt("lasteditpos." + mScript, start);
    e.commit();
  }

  private void insertContent(String text) {
    int selectionStart = Math.min(mContentText.getSelectionStart(), mContentText.getSelectionEnd());
    int selectionEnd = Math.max(mContentText.getSelectionStart(), mContentText.getSelectionEnd());
    mContentText.getEditableText().replace(selectionStart, selectionEnd, text);
  }

  @Override
  public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_BACK && hasContentChanged()) {
      AlertDialog.Builder alert = new AlertDialog.Builder(this);
      setVolumeControlStream(AudioManager.STREAM_MUSIC);
      alert.setCancelable(false);
      alert.setTitle("Confirm exit");
      alert.setMessage("Would you like to save?");
      alert.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int whichButton) {
          save();
          finish();
        }
      });
      alert.setNegativeButton("No", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int whichButton) {
          finish();
        }
      });
      alert.setNeutralButton("Cancel", new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int whichButton) {
        }
      });
      alert.show();
      return true;
    } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
      redo();
      return true;
    } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
      undo();
      return true;
    } else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
      showDialog(DIALOG_FIND_REPLACE);
      return true;
    } else {
      return super.onKeyDown(keyCode, event);
    }
  }

  @Override
  protected Dialog onCreateDialog(int id, Bundle args) {
    AlertDialog.Builder b = new AlertDialog.Builder(this);
    if (id == DIALOG_LINE) {
      b.setTitle("Goto Line");
      b.setView(mLineNo);
      b.setPositiveButton("Ok", new OnClickListener() {

        @Override
        public void onClick(DialogInterface dialog, int which) {
          gotoLine(Integer.parseInt(mLineNo.getText().toString()));
        }
      });
      b.setNegativeButton("Cancel", null);
      return b.create();
    } else if (id == DIALOG_FIND_REPLACE) {
      View v = getLayoutInflater().inflate(R.layout.findreplace, null);
      mSearchFind = (EditText) v.findViewById(R.id.searchFind);
      mSearchReplace = (EditText) v.findViewById(R.id.searchReplace);
      mSearchAll = (CheckBox) v.findViewById(R.id.searchAll);
      mSearchCase = (CheckBox) v.findViewById(R.id.searchCase);
      mSearchStart = (CheckBox) v.findViewById(R.id.searchStart);
      mSearchWord = (CheckBox) v.findViewById(R.id.searchWord);
      b.setTitle("Search and Replace");
      b.setView(v);
      b.setPositiveButton("Find", this);
      b.setNeutralButton("Next", this);
      b.setNegativeButton("Replace", this);
      return b.create();
    }

    return super.onCreateDialog(id, args);
  }

  @Override
  protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
    if (id == DIALOG_LINE) {
      mLineNo.setText(String.valueOf(getLineNo()));
    } else if (id == DIALOG_FIND_REPLACE) {
      mSearchStart.setChecked(false);
    }
    super.onPrepareDialog(id, dialog, args);
  }

  protected int getLineNo() {
    int pos = mContentText.getSelectionStart();
    String text = mContentText.getText().toString();
    int i = 0;
    int n = 1;
    while (i < pos) {
      int j = text.indexOf("\n", i);
      if (j < 0) {
        break;
      }
      i = j + 1;
      if (i < pos) {
        n += 1;
      }
    }
    return n;
  }

  protected void gotoLine(int line) {
    String text = mContentText.getText().toString();
    if (text.length() < 1) {
      return;
    }
    int i = 0;
    int n = 1;
    while (i < text.length() && n < line) {
      int j = text.indexOf("\n", i);
      if (j < 0) {
        break;
      }
      i = j + 1;
      n += 1;
    }
    mContentText.setSelection(Math.min(text.length() - 1, i));
  }

  @Override
  protected void onUserLeaveHint() {
    if (hasContentChanged()) {
      save();
    }
  }

  private boolean hasContentChanged() {
    return !mLastSavedContent.equals(mContentText.getText().toString());
  }

  private final class ContentInputFilter implements InputFilter {
    @Override
    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart,
        int dend) {
      if (end - start == 1) {
        Interpreter ip = mConfiguration.getInterpreterForScript(mNameText.getText().toString());
        String auto = null;
        if (ip != null && mEnableAutoClose) {
          auto = ip.getLanguage().autoClose(source.charAt(start));
        }
        // Auto indent code?
        if (auto == null && source.charAt(start) == '\n' && mAutoIndent) {
          int i = dstart - 1;
          int spaces = 0;
          while ((i >= 0) && dest.charAt(i) != '\n') {
            i -= 1; // Find start of line.
          }
          i += 1;
          while (i < dest.length() && dest.charAt(i++) == ' ') {
            spaces += 1;
          }
          if (spaces > 0) {
            return String.format("\n%" + spaces + "s", " ");
          }
        }
        if (auto != null) {
          mScheduleMoveLeft = true;
          return auto;
        }
      }
      return null;
    }
  }

  private final class ContentTextWatcher implements TextWatcher {
    private final EditHistory mmEditHistory;
    private CharSequence mmBeforeChange;
    private CharSequence mmAfterChange;

    private ContentTextWatcher(EditHistory history) {
      mmEditHistory = history;
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
      if (!mIsUndoOrRedo) {
        mmAfterChange = s.subSequence(start, start + count);
        mmEditHistory.add(new EditItem(start, mmBeforeChange, mmAfterChange));
      }
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
      if (!mIsUndoOrRedo) {
        mmBeforeChange = s.subSequence(start, start + count);
      }
    }

    @Override
    public void afterTextChanged(Editable s) {
      if (mScheduleMoveLeft) {
        mScheduleMoveLeft = false;
        Selection.moveLeft(mContentText.getText(), mContentText.getLayout());
      }
    }
  }

  /**
   * Keeps track of all the edit history of a text.
   */
  private final class EditHistory {
    int mmPosition = 0;
    private final Vector<EditItem> mmHistory = new Vector<EditItem>();

    /**
     * Adds a new edit operation to the history at the current position. If executed after a call to
     * getPrevious() removes all the future history (elements with positions >= current history
     * position).
     *
     */
    private void add(EditItem item) {
      mmHistory.setSize(mmPosition);
      mmHistory.add(item);
      mmPosition++;
    }

    /**
     * Traverses the history backward by one position, returns and item at that position.
     */
    private EditItem getPrevious() {
      if (mmPosition == 0) {
        return null;
      }
      mmPosition--;
      return mmHistory.get(mmPosition);
    }

    /**
     * Traverses the history forward by one position, returns and item at that position.
     */
    private EditItem getNext() {
      if (mmPosition == mmHistory.size()) {
        return null;
      }
      EditItem item = mmHistory.get(mmPosition);
      mmPosition++;
      return item;
    }
  }

  /**
   * Represents a single edit operation.
   */
  private final class EditItem {
    private final int mmIndex;
    private final CharSequence mmBefore;
    private final CharSequence mmAfter;

    /**
     * Constructs EditItem of a modification that was applied at position start and replaced
     * CharSequence before with CharSequence after.
     */
    public EditItem(int start, CharSequence before, CharSequence after) {
      mmIndex = start;
      mmBefore = before;
      mmAfter = after;
    }
  }

  private void undo() {
    EditItem edit = mHistory.getPrevious();
    if (edit == null) {
      return;
    }
    Editable text = mContentText.getText();
    int start = edit.mmIndex;
    int end = start + (edit.mmAfter != null ? edit.mmAfter.length() : 0);
    mIsUndoOrRedo = true;
    text.replace(start, end, edit.mmBefore);
    mIsUndoOrRedo = false;
    // This will get rid of underlines inserted when editor tries to come up with a suggestion.
    for (Object o : text.getSpans(0, text.length(), UnderlineSpan.class)) {
      text.removeSpan(o);
    }
    Selection.setSelection(text, edit.mmBefore == null ? start : (start + edit.mmBefore.length()));
  }

  private void redo() {
    EditItem edit = mHistory.getNext();
    if (edit == null) {
      return;
    }
    Editable text = mContentText.getText();
    int start = edit.mmIndex;
    int end = start + (edit.mmBefore != null ? edit.mmBefore.length() : 0);
    mIsUndoOrRedo = true;
    text.replace(start, end, edit.mmAfter);
    mIsUndoOrRedo = false;
    for (Object o : text.getSpans(0, text.length(), UnderlineSpan.class)) {
      text.removeSpan(o);
    }
    Selection.setSelection(text, edit.mmAfter == null ? start : (start + edit.mmAfter.length()));
  }

  @Override
  public void onClick(DialogInterface dialog, int which) {
    int start = mContentText.getSelectionStart();
    int end = mContentText.getSelectionEnd();
    String original = mContentText.getText().toString();
    if (start == end || which != AlertDialog.BUTTON_NEGATIVE) {
      end = original.length();
    }
    if (which == AlertDialog.BUTTON_NEUTRAL) {
      start += 1;
    }
    if (mSearchStart.isChecked()) {
      start = 0;
      end = original.length();
    }
    String findText = mSearchFind.getText().toString();
    String replaceText = mSearchReplace.getText().toString();
    String search = Pattern.quote(findText);
    int flags = 0;
    if (!mSearchCase.isChecked()) {
      flags |= Pattern.CASE_INSENSITIVE;
    }
    if (mSearchWord.isChecked()) {
      search = "\\b" + search + "\\b";
    }
    Pattern p = Pattern.compile(search, flags);
    Matcher m = p.matcher(original);
    m.region(start, end);
    if (!m.find()) {
      Toast.makeText(this, "Search not found.", Toast.LENGTH_SHORT).show();
      return;
    }
    int foundpos = m.start();
    if (which != AlertDialog.BUTTON_NEGATIVE) { // Find
      mContentText.setSelection(foundpos, foundpos + findText.length());
    } else { // Replace
      String s;
      // Seems to be a bug in the android 2.2 implementation of replace... regions not returning
      // whole string.
      m = p.matcher(original.substring(start, end));
      String replace = Matcher.quoteReplacement(replaceText);
      if (mSearchAll.isChecked()) {
        s = m.replaceAll(replace);
      } else {
        s = m.replaceFirst(replace);
      }
      mContentText.setText(original.substring(0, start) + s + original.substring(end));
      mContentText.setSelection(foundpos, foundpos + replaceText.length());
    }
    mContentText.requestFocus();
  }
}
