package com.secureline.notes.ui;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipboardManager;
import android.content.Intent;
import android.graphics.Color;
import android.util.Base64;
import android.text.Layout;
import android.text.style.UnderlineSpan;
import android.text.style.StyleSpan;
import android.text.style.AlignmentSpan;
import android.text.SpannableStringBuilder;
import android.text.Spannable;
import android.text.Spanned;
import android.text.Html;
import android.text.Editable;
import android.text.TextWatcher;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.method.HideReturnsTransformationMethod;
import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.PopupWindow;
import android.widget.LinearLayout;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import androidx.core.content.FileProvider;

import com.secureline.notes.BuildConfig;
import com.secureline.notes.LockActivity;
import com.secureline.notes.R;
import com.secureline.notes.data.Note;
import com.secureline.notes.data.NotesRepository;
import com.secureline.notes.export.ExportSingleNoteManager;
import com.secureline.notes.security.SessionManager;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class NoteEditorActivity extends Activity {

    private static final String RICH_PREFIX = "\u0001LNZRICH1:";

    private NotesRepository repo;

    private long noteId = -1;

    private EditText title;
    private SelectionAwareEditText content;

    private boolean typingBold;
    private boolean typingItalic;
    private boolean typingUnderline;
    private Layout.Alignment typingAlignment = Layout.Alignment.ALIGN_NORMAL;
    private boolean applyingFormatting;
    private boolean loadingNote;
    private boolean textMutationInProgress;
    private boolean pendingBold;
    private boolean pendingItalic;
    private boolean pendingUnderline;
    private Layout.Alignment pendingAlignment = Layout.Alignment.ALIGN_NORMAL;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (!SessionManager.isUnlocked()) {
            redirect();
            return;
        }

        setContentView(R.layout.activity_note_editor);

        getWindow().setFlags(
                WindowManager.LayoutParams.FLAG_SECURE,
                WindowManager.LayoutParams.FLAG_SECURE
        );

        getWindow().setSoftInputMode(
                WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
        );

        repo = new NotesRepository(this);

        title = findViewById(R.id.title_input);
        content = findViewById(R.id.content_input);

        noteId = getIntent().getLongExtra("id", -1);

        findViewById(R.id.save_btn).setOnClickListener(v -> {
            if (save()) {
                finish();
            }
        });

        findViewById(R.id.back_btn).setOnClickListener(v -> finish());

        findViewById(R.id.more_btn).setOnClickListener(v ->
                showMoreMenu(v)
        );

        setupFormattingToolbar();

        if (noteId != -1) {
            Note note = repo.getById(noteId);

            if (note != null) {
                title.setText(note.title);
                loadingNote = true;
                content.setText(decodeRichContent(note.content));
                loadingNote = false;
            }
        }

        content.requestFocus();
    }

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

        if (!SessionManager.isUnlocked()) {
            redirect();
            return;
        }

        SessionManager.refresh();
    }

    @Override
    protected void onPause() {
        super.onPause();

        try {
            if (SessionManager.isUnlocked()) {
                save();
            }
        } catch (Exception ignored) {
            // Autosave must never crash the activity.
        }
    }

    @Override
    public void onUserInteraction() {
        super.onUserInteraction();
        SessionManager.refresh();
    }

    private void redirect() {
        Intent intent = new Intent(this, LockActivity.class);

        intent.putExtra(
                "after_unlock",
                NoteEditorActivity.class.getName()
        );

        intent.addFlags(
                Intent.FLAG_ACTIVITY_CLEAR_TOP
                        | Intent.FLAG_ACTIVITY_NEW_TASK
        );

        startActivity(intent);
        finish();
    }

    private boolean save() {
        if (!SessionManager.isUnlocked()) {
            toast(R.string.session_expired, Toast.LENGTH_SHORT);
            redirect();
            return false;
        }

        String noteTitle = title.getText().toString();
        String noteContent = encodeRichContent(content.getText());

        if (noteTitle.trim().isEmpty()
                && content.getText().toString().trim().isEmpty()) {
            return false;
        }

        if (noteTitle.trim().isEmpty()) {
            noteTitle = getString(R.string.untitled_note);
            title.setText(noteTitle);
        }

        try {
            if (noteId == -1) {
                long insertedId = repo.insert(
                        noteTitle,
                        noteContent
                );

                if (insertedId == -1) {
                    toast(R.string.save_failed, Toast.LENGTH_SHORT);
                    return false;
                }

                noteId = insertedId;

            } else {
                repo.update(
                        noteId,
                        noteTitle,
                        noteContent
                );
            }

            title.clearFocus();
            content.clearFocus();

            return true;

        } catch (SecurityException e) {
            toast(R.string.session_locked, Toast.LENGTH_SHORT);
            redirect();
            return false;

        } catch (Exception e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }

            toast(R.string.unexpected_error, Toast.LENGTH_SHORT);
            return false;
        }
    }


    private void setupFormattingToolbar() {
        TextView bold = findViewById(R.id.format_bold);
        TextView italic = findViewById(R.id.format_italic);
        TextView underline = findViewById(R.id.format_underline);
        TextView left = findViewById(R.id.align_left);
        TextView center = findViewById(R.id.align_center);
        TextView right = findViewById(R.id.align_right);

        bold.setContentDescription(getString(R.string.format_bold));
        italic.setContentDescription(getString(R.string.format_italic));
        underline.setContentDescription(getString(R.string.format_underline));
        left.setContentDescription(getString(R.string.align_left));
        center.setContentDescription(getString(R.string.align_center));
        right.setContentDescription(getString(R.string.align_right));

        bold.setOnClickListener(v -> toggleCharacterStyle(Typeface.BOLD));
        italic.setOnClickListener(v -> toggleCharacterStyle(Typeface.ITALIC));
        underline.setOnClickListener(v -> toggleUnderline());

        left.setOnClickListener(v -> applyAlignment(Layout.Alignment.ALIGN_NORMAL));
        center.setOnClickListener(v -> applyAlignment(Layout.Alignment.ALIGN_CENTER));
        right.setOnClickListener(v -> applyAlignment(Layout.Alignment.ALIGN_OPPOSITE));

        content.setSelectionChangedListener((start, end) -> {
            if (!applyingFormatting
                    && !loadingNote
                    && !textMutationInProgress) {
                syncTypingStateFromSelection();
            }
        });

        content.addTextChangedListener(new TextWatcher() {
            private int changeStart;
            private int insertedCount;

            @Override
            public void beforeTextChanged(
                    CharSequence s,
                    int start,
                    int count,
                    int after
            ) {
                if (loadingNote || applyingFormatting) {
                    return;
                }

                textMutationInProgress = true;
                pendingBold = typingBold;
                pendingItalic = typingItalic;
                pendingUnderline = typingUnderline;
                pendingAlignment = typingAlignment;
            }

            @Override
            public void onTextChanged(
                    CharSequence s,
                    int start,
                    int before,
                    int count
            ) {
                changeStart = start;
                insertedCount = count;
            }

            @Override
            public void afterTextChanged(Editable editable) {
                if (loadingNote || applyingFormatting) {
                    insertedCount = 0;
                    textMutationInProgress = false;
                    return;
                }

                if (insertedCount > 0) {
                    int start = Math.max(
                            0,
                            Math.min(changeStart, editable.length())
                    );
                    int end = Math.max(
                            start,
                            Math.min(
                                    changeStart + insertedCount,
                                    editable.length()
                            )
                    );

                    applyingFormatting = true;
                    try {
                        applyPendingCharacterStyles(editable, start, end);
                        applyPendingAlignment(editable, start, end);
                    } finally {
                        applyingFormatting = false;
                    }

                    typingBold = pendingBold;
                    typingItalic = pendingItalic;
                    typingUnderline = pendingUnderline;
                    typingAlignment = pendingAlignment;
                }

                insertedCount = 0;
                textMutationInProgress = false;
                updateFormattingToolbar();
            }
        });

        content.setOnClickListener(v -> syncTypingStateFromSelection());
        content.setOnFocusChangeListener((v, hasFocus) -> {
            if (hasFocus) {
                syncTypingStateFromSelection();
            }
        });

        syncTypingStateFromSelection();
    }

    private void toggleCharacterStyle(int style) {
        Editable editable = content.getText();
        int start = Math.max(0, content.getSelectionStart());
        int end = Math.max(0, content.getSelectionEnd());

        if (start == end) {
            content.requestFocus();

            if (style == Typeface.BOLD) {
                typingBold = !typingBold;
            } else if (style == Typeface.ITALIC) {
                typingItalic = !typingItalic;
            }

            updateFormattingToolbar();
            return;
        }

        applyingFormatting = true;
        try {
            StyleSpan[] spans =
                    editable.getSpans(start, end, StyleSpan.class);

            boolean hasRequestedStyle = false;
            for (StyleSpan span : spans) {
                if (span.getStyle() == style) {
                    hasRequestedStyle = true;
                    editable.removeSpan(span);
                }
            }

            if (!hasRequestedStyle) {
                editable.setSpan(
                        new StyleSpan(style),
                        start,
                        end,
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                );
            }

            content.setSelection(start, end);
        } finally {
            applyingFormatting = false;
        }

        syncTypingStateFromSelection();
        content.requestFocus();
    }

    private void toggleUnderline() {
        Editable editable = content.getText();
        int start = Math.max(0, content.getSelectionStart());
        int end = Math.max(0, content.getSelectionEnd());

        if (start == end) {
            content.requestFocus();
            typingUnderline = !typingUnderline;
            updateFormattingToolbar();
            return;
        }

        applyingFormatting = true;
        try {
            UnderlineSpan[] spans =
                    editable.getSpans(start, end, UnderlineSpan.class);

            if (spans.length > 0) {
                for (UnderlineSpan span : spans) {
                    editable.removeSpan(span);
                }
            } else {
                editable.setSpan(
                        new UnderlineSpan(),
                        start,
                        end,
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
                );
            }

            content.setSelection(start, end);
        } finally {
            applyingFormatting = false;
        }

        syncTypingStateFromSelection();
        content.requestFocus();
    }

    private void applyPendingCharacterStyles(
            Editable editable,
            int start,
            int end
    ) {
        if (start >= end) {
            return;
        }

        if (pendingBold) {
            editable.setSpan(
                    new StyleSpan(Typeface.BOLD),
                    start,
                    end,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
            );
        }

        if (pendingItalic) {
            editable.setSpan(
                    new StyleSpan(Typeface.ITALIC),
                    start,
                    end,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
            );
        }

        if (pendingUnderline) {
            editable.setSpan(
                    new UnderlineSpan(),
                    start,
                    end,
                    Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
            );
        }
    }

    private void applyPendingAlignment(
            Editable editable,
            int start,
            int end
    ) {
        int cursor = Math.max(start, Math.min(end, editable.length()));
        int paraStart = paragraphStart(editable, start);
        int paraEnd = paragraphEnd(editable, cursor);

        AlignmentSpan[] old =
                editable.getSpans(paraStart, paraEnd, AlignmentSpan.class);
        for (AlignmentSpan span : old) {
            editable.removeSpan(span);
        }

        if (pendingAlignment != Layout.Alignment.ALIGN_NORMAL) {
            editable.setSpan(
                    new AlignmentSpan.Standard(pendingAlignment),
                    paraStart,
                    paraEnd,
                    Spannable.SPAN_PARAGRAPH
            );
        }
    }

    private int[] currentWordRange(CharSequence text, int cursor) {
        int length = text == null ? 0 : text.length();
        cursor = Math.max(0, Math.min(cursor, length));

        int start = cursor;
        while (start > 0 && !Character.isWhitespace(text.charAt(start - 1))) {
            start--;
        }

        int end = cursor;
        while (end < length && !Character.isWhitespace(text.charAt(end))) {
            end++;
        }

        return new int[]{start, end};
    }

    private void applyAlignment(Layout.Alignment alignment) {
        content.requestFocus();
        typingAlignment = alignment;

        Editable editable = content.getText();
        int start = Math.max(0, content.getSelectionStart());
        int end = Math.max(0, content.getSelectionEnd());

        int paraStart = paragraphStart(editable, start);
        int paraEnd = paragraphEnd(editable, Math.max(start, end));

        applyingFormatting = true;
        try {
            AlignmentSpan[] spans =
                    editable.getSpans(paraStart, paraEnd, AlignmentSpan.class);
            for (AlignmentSpan span : spans) {
                editable.removeSpan(span);
            }

            if (alignment != Layout.Alignment.ALIGN_NORMAL) {
                editable.setSpan(
                        new AlignmentSpan.Standard(alignment),
                        paraStart,
                        paraEnd,
                        Spannable.SPAN_PARAGRAPH
                );
            }

            if (start != end) {
                content.setSelection(start, end);
            } else {
                content.setSelection(Math.min(start, editable.length()));
            }
        } finally {
            applyingFormatting = false;
        }

        updateFormattingToolbar();
    }

    private int paragraphStart(CharSequence text, int position) {
        position = Math.max(0, Math.min(position, text.length()));
        while (position > 0 && text.charAt(position - 1) != '\n') {
            position--;
        }
        return position;
    }

    private int paragraphEnd(CharSequence text, int position) {
        position = Math.max(0, Math.min(position, text.length()));
        while (position < text.length() && text.charAt(position) != '\n') {
            position++;
        }
        if (position < text.length()) {
            position++;
        }
        return position;
    }

    private void syncTypingStateFromSelection() {
        Editable editable = content.getText();
        int start = Math.max(0, content.getSelectionStart());
        int end = Math.max(0, content.getSelectionEnd());

        typingBold = hasStyle(editable, start, end, Typeface.BOLD);
        typingItalic = hasStyle(editable, start, end, Typeface.ITALIC);
        typingUnderline = hasUnderline(editable, start, end);
        typingAlignment = alignmentAt(editable, start);

        updateFormattingToolbar();
    }

    private boolean hasStyle(
            Editable editable,
            int start,
            int end,
            int style
    ) {
        int[] range = probeRange(editable, start, end);
        if (range[0] >= range[1]) {
            return false;
        }

        for (StyleSpan span :
                editable.getSpans(range[0], range[1], StyleSpan.class)) {
            if (span.getStyle() == style) {
                return true;
            }
        }

        return false;
    }

    private boolean hasUnderline(
            Editable editable,
            int start,
            int end
    ) {
        int[] range = probeRange(editable, start, end);
        return range[0] < range[1]
                && editable.getSpans(
                        range[0],
                        range[1],
                        UnderlineSpan.class
                ).length > 0;
    }

    private int[] probeRange(
            Editable editable,
            int start,
            int end
    ) {
        int length = editable.length();

        if (length == 0) {
            return new int[]{0, 0};
        }

        if (start != end) {
            return new int[]{
                    Math.max(0, Math.min(start, length)),
                    Math.max(0, Math.min(end, length))
            };
        }

        int probe = Math.max(0, Math.min(start, length));

        // Prefer the character immediately before the cursor, matching
        // normal editor behavior when continuing formatted text.
        if (probe > 0) {
            return new int[]{probe - 1, probe};
        }

        return new int[]{0, Math.min(1, length)};
    }

    private Layout.Alignment alignmentAt(
            Editable editable,
            int cursor
    ) {
        int ps = paragraphStart(editable, cursor);
        int pe = paragraphEnd(editable, cursor);

        AlignmentSpan[] spans =
                editable.getSpans(ps, pe, AlignmentSpan.class);

        if (spans.length == 0) {
            return Layout.Alignment.ALIGN_NORMAL;
        }

        return spans[spans.length - 1].getAlignment();
    }

    private void updateFormattingToolbar() {
        setFormatSelected(
                findViewById(R.id.format_bold),
                typingBold
        );
        setFormatSelected(
                findViewById(R.id.format_italic),
                typingItalic
        );
        setFormatSelected(
                findViewById(R.id.format_underline),
                typingUnderline
        );

        setFormatSelected(
                findViewById(R.id.align_left),
                typingAlignment == Layout.Alignment.ALIGN_NORMAL
        );
        setFormatSelected(
                findViewById(R.id.align_center),
                typingAlignment == Layout.Alignment.ALIGN_CENTER
        );
        setFormatSelected(
                findViewById(R.id.align_right),
                typingAlignment == Layout.Alignment.ALIGN_OPPOSITE
        );
    }

    private void setFormatSelected(TextView view, boolean selected) {
        if (view == null) {
            return;
        }

        view.setTextColor(
                getColor(
                        selected
                                ? R.color.sl_color_blue
                                : R.color.sl_color_text_primary
                )
        );

        view.setBackgroundResource(
                selected
                        ? R.drawable.lemenzo_format_selected_bg
                        : 0
        );
    }

    private String encodeRichContent(Spanned richText) {
        if (richText == null || richText.length() == 0) {
            return "";
        }

        String html = Html.toHtml(
                richText,
                Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE
        );

        String encoded = Base64.encodeToString(
                html.getBytes(StandardCharsets.UTF_8),
                Base64.NO_WRAP
        );

        return RICH_PREFIX + encoded;
    }

    private Spanned decodeRichContent(String stored) {
        if (stored == null || stored.isEmpty()) {
            return new SpannableStringBuilder("");
        }

        if (!stored.startsWith(RICH_PREFIX)) {
            return new SpannableStringBuilder(stored);
        }

        try {
            byte[] decoded = Base64.decode(
                    stored.substring(RICH_PREFIX.length()),
                    Base64.NO_WRAP
            );
            String html = new String(decoded, StandardCharsets.UTF_8);
            Arrays.fill(decoded, (byte) 0);

            return Html.fromHtml(
                    html,
                    Html.FROM_HTML_MODE_LEGACY
            );
        } catch (Exception e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }
            return new SpannableStringBuilder(stored);
        }
    }

    private String richContentToPlainText(String stored) {
        return decodeRichContent(stored).toString();
    }

    private void showMoreMenu(View anchorView) {
        if (anchorView == null) return;

        LinearLayout container = new LinearLayout(this);
        container.setOrientation(LinearLayout.VERTICAL);
        container.setPadding(dp(8), dp(8), dp(8), dp(8));
        container.setBackgroundResource(R.drawable.lemenzo_note_menu_bg);

        TextView export = new TextView(this);
        export.setText(R.string.export_note);
        export.setTextColor(getColor(R.color.sl_color_text_primary));
        export.setTextSize(16);
        export.setGravity(Gravity.CENTER_VERTICAL);
        export.setPadding(dp(16), dp(13), dp(16), dp(13));

        TextView delete = new TextView(this);
        delete.setText(R.string.delete_note);
        delete.setTextColor(getColor(R.color.sl_color_danger));
        delete.setTextSize(16);
        delete.setGravity(Gravity.CENTER_VERTICAL);
        delete.setPadding(dp(16), dp(13), dp(16), dp(13));

        View divider = new View(this);
        divider.setBackgroundColor(Color.rgb(34, 46, 67));

        container.addView(export, new LinearLayout.LayoutParams(dp(190), ViewGroup.LayoutParams.WRAP_CONTENT));
        LinearLayout.LayoutParams dividerParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(1));
        dividerParams.setMargins(dp(10), 0, dp(10), 0);
        container.addView(divider, dividerParams);
        container.addView(delete, new LinearLayout.LayoutParams(dp(190), ViewGroup.LayoutParams.WRAP_CONTENT));

        PopupWindow popup = new PopupWindow(container, dp(206), ViewGroup.LayoutParams.WRAP_CONTENT, true);
        popup.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
        popup.setOutsideTouchable(true);
        popup.setElevation(dp(12));

        export.setOnClickListener(v -> { popup.dismiss(); prepareShare(); });
        delete.setOnClickListener(v -> { popup.dismiss(); showDeleteDialog(); });

        popup.showAsDropDown(anchorView, -dp(156), -dp(6), Gravity.END);
    }

    private void showDeleteDialog() {
        if (noteId == -1) {
            return;
        }

        View dialogView = LayoutInflater.from(this).inflate(
                R.layout.dialog_delete,
                null,
                false
        );

        AlertDialog dialog =
                new AlertDialog.Builder(this, R.style.SecureLineUI_Dialog)
                        .setView(dialogView)
                        .create();

        dialog.setCanceledOnTouchOutside(true);

        dialogView.findViewById(R.id.delete_btn)
                .setOnClickListener(v -> {
                    try {
                        repo.delete(noteId);

                        dialog.dismiss();
                        finish();

                    } catch (SecurityException e) {
                        dialog.dismiss();

                        toast(
                                R.string.session_locked,
                                Toast.LENGTH_SHORT
                        );

                        redirect();

                    } catch (Exception e) {
                        if (BuildConfig.DEBUG) {
                            e.printStackTrace();
                        }

                        toast(
                                R.string.operation_failed,
                                Toast.LENGTH_LONG
                        );
                    }
                });

        View.OnClickListener cancelListener = v ->
                dialog.dismiss();

        dialogView.findViewById(R.id.close_btn)
                .setOnClickListener(cancelListener);

        dialogView.findViewById(R.id.cancel_btn)
                .setOnClickListener(cancelListener);

        showSecureLineDialog(dialog);
    }

    /*
     * The open note must be saved before it can be shared.
     */
    private void prepareShare() {
        if (!save() || noteId == -1) {
            toast(R.string.nothing_to_export, Toast.LENGTH_SHORT);
            return;
        }

        showShareFormatDialog();
    }

    /*
     * .lzn = encrypted and password protected.
     * .txt = unencrypted plain UTF-8 text.
     */
    private void showShareFormatDialog() {
        View dialogView = LayoutInflater.from(this).inflate(
                R.layout.dialog_export_format,
                null,
                false
        );

        TextView titleView =
                dialogView.findViewById(R.id.dialog_title);

        TextView subtitleView =
                dialogView.findViewById(R.id.dialog_subtitle);

        titleView.setText(R.string.share_format_title);
        subtitleView.setText(R.string.share_format_subtitle);

        AlertDialog dialog =
                new AlertDialog.Builder(this, R.style.SecureLineUI_Dialog)
                        .setView(dialogView)
                        .create();

        dialog.setCanceledOnTouchOutside(true);

        dialogView.findViewById(R.id.encrypted_option)
                .setOnClickListener(v -> {
                    dialog.dismiss();
                    showPasswordDialog();
                });

        dialogView.findViewById(R.id.plaintext_option)
                .setOnClickListener(v -> {
                    dialog.dismiss();
                    showPlainTextWarning();
                });

        View.OnClickListener cancelListener = v ->
                dialog.dismiss();

        dialogView.findViewById(R.id.close_btn)
                .setOnClickListener(cancelListener);

        dialogView.findViewById(R.id.cancel_btn)
                .setOnClickListener(cancelListener);

        showSecureLineDialog(dialog);
    }

    /*
     * Password is only requested for encrypted .lzn files.
     */
    private void showPasswordDialog() {
        View dialogView = LayoutInflater.from(this).inflate(
                R.layout.dialog_password,
                null,
                false
        );

        TextView titleView =
                dialogView.findViewById(R.id.dialog_title);

        TextView messageView =
                dialogView.findViewById(R.id.dialog_message);

        TextView primaryButton =
                dialogView.findViewById(R.id.primary_btn);

        TextView passwordToggle =
                dialogView.findViewById(R.id.password_toggle);

        EditText passwordInput =
                dialogView.findViewById(R.id.password_input);

        titleView.setText(
                R.string.create_export_password
        );

        messageView.setText(
                R.string.create_export_password_message
        );

        primaryButton.setText(
                R.string.export_encrypted_file
        );

        passwordInput.setImportantForAutofill(
                View.IMPORTANT_FOR_AUTOFILL_NO
        );

        passwordInput.setLongClickable(false);
        passwordInput.setTextIsSelectable(false);

        passwordInput.setTransformationMethod(
                PasswordTransformationMethod.getInstance()
        );

        final boolean[] passwordVisible = {false};

        passwordToggle.setOnClickListener(v -> {
            passwordVisible[0] = !passwordVisible[0];

            if (passwordVisible[0]) {
                passwordInput.setTransformationMethod(
                        HideReturnsTransformationMethod.getInstance()
                );

                passwordToggle.setText(
                        R.string.hide_password
                );

            } else {
                passwordInput.setTransformationMethod(
                        PasswordTransformationMethod.getInstance()
                );

                passwordToggle.setText(
                        R.string.show_password
                );
            }

            passwordInput.setSelection(
                    passwordInput.length()
            );
        });

        AlertDialog dialog =
                new AlertDialog.Builder(this, R.style.SecureLineUI_Dialog)
                        .setView(dialogView)
                        .create();

        dialog.setCanceledOnTouchOutside(true);

        dialog.setOnCancelListener(ignored ->
                passwordInput.setText("")
        );

        primaryButton.setOnClickListener(v -> {
            String password = passwordInput
                    .getText()
                    .toString()
                    .trim();

            if (password.length() < 8) {
                toast(
                        R.string.password_too_short,
                        Toast.LENGTH_LONG
                );

                passwordInput.setText("");
                passwordInput.requestFocus();
                return;
            }

            exportLZN(password);

            passwordInput.setText("");
            password = null;

            dialog.dismiss();
        });

        View.OnClickListener cancelListener = v -> {
            passwordInput.setText("");
            dialog.dismiss();
        };

        dialogView.findViewById(R.id.close_btn)
                .setOnClickListener(cancelListener);

        dialogView.findViewById(R.id.cancel_btn)
                .setOnClickListener(cancelListener);

        showSecureLineDialog(dialog);

        passwordInput.requestFocus();

        if (dialog.getWindow() != null) {
            dialog.getWindow().setSoftInputMode(
                    WindowManager.LayoutParams
                            .SOFT_INPUT_STATE_ALWAYS_VISIBLE
            );
        }
    }

    /*
     * Clearly warn the user before sharing an unencrypted text file.
     */
    private void showPlainTextWarning() {
        View dialogView = LayoutInflater.from(this).inflate(
                R.layout.dialog_warning,
                null,
                false
        );

        TextView titleView =
                dialogView.findViewById(R.id.dialog_title);

        TextView messageView =
                dialogView.findViewById(R.id.dialog_message);

        TextView primaryButton =
                dialogView.findViewById(R.id.primary_btn);

        titleView.setText(
                R.string.plain_text_warning_title
        );

        messageView.setText(
                R.string.plain_text_warning_message
        );

        primaryButton.setText(
                R.string.share_unencrypted
        );

        AlertDialog dialog =
                new AlertDialog.Builder(this, R.style.SecureLineUI_Dialog)
                        .setView(dialogView)
                        .create();

        dialog.setCanceledOnTouchOutside(true);

        primaryButton.setOnClickListener(v -> {
            dialog.dismiss();
            exportTXT();
        });

        View.OnClickListener cancelListener = v ->
                dialog.dismiss();

        dialogView.findViewById(R.id.close_btn)
                .setOnClickListener(cancelListener);

        dialogView.findViewById(R.id.cancel_btn)
                .setOnClickListener(cancelListener);

        showSecureLineDialog(dialog);
    }

    private void exportLZN(String password) {
        SessionManager.refresh();

        if (!SessionManager.isUnlocked()) {
            toast(R.string.session_expired, Toast.LENGTH_SHORT);
            redirect();
            return;
        }

        byte[] encrypted = null;

        try {
            Note note = repo.getById(noteId);

            if (note == null) {
                toast(R.string.nothing_to_export, Toast.LENGTH_SHORT);
                return;
            }

            encrypted = ExportSingleNoteManager.buildSingleNote(
                    note.title,
                    note.content,
                    password
            );

            File file = new File(
                    getCacheDir(),
                    safeFilename(note.title) + ".lzn"
            );

            try (FileOutputStream output =
                         new FileOutputStream(file)) {
                output.write(encrypted);
                output.flush();
            }

            shareFile(
                    file,
                    "application/octet-stream",
                    R.string.share_encrypted_note
            );

        } catch (SecurityException e) {
            toast(R.string.session_locked, Toast.LENGTH_SHORT);
            redirect();

        } catch (Exception e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }

            toast(R.string.export_failed, Toast.LENGTH_LONG);

        } finally {
            if (encrypted != null) {
                Arrays.fill(encrypted, (byte) 0);
            }
        }
    }

    /*
     * Plain text export:
     *
     * - no password
     * - no CryptoManager
     * - no encryption
     * - standard UTF-8 text
     */
    private void exportTXT() {
        SessionManager.refresh();

        if (!SessionManager.isUnlocked()) {
            toast(R.string.session_expired, Toast.LENGTH_SHORT);
            redirect();
            return;
        }

        byte[] plain = null;

        try {
            Note note = repo.getById(noteId);

            if (note == null) {
                toast(R.string.nothing_to_export, Toast.LENGTH_SHORT);
                return;
            }

            String combined =
                    note.title
                            + "\n"
                            + "========================================"
                            + "\n\n"
                            + richContentToPlainText(note.content);

            plain = combined.getBytes(StandardCharsets.UTF_8);

            File file = new File(
                    getCacheDir(),
                    safeFilename(note.title) + ".txt"
            );

            try (FileOutputStream output =
                         new FileOutputStream(file)) {
                output.write(plain);
                output.flush();
            }

            shareFile(
                    file,
                    "text/plain",
                    R.string.share_plain_text_note
            );

        } catch (SecurityException e) {
            toast(R.string.session_locked, Toast.LENGTH_SHORT);
            redirect();

        } catch (Exception e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }

            toast(R.string.export_failed, Toast.LENGTH_LONG);

        } finally {
            if (plain != null) {
                Arrays.fill(plain, (byte) 0);
            }
        }
    }

    private void shareFile(
            File file,
            String mimeType,
            int chooserTitle
    ) {
        if (!SessionManager.isUnlocked()) {
            redirect();
            return;
        }

        try {
            Uri uri = FileProvider.getUriForFile(
                    this,
                    getPackageName() + ".provider",
                    file
            );

            Intent shareIntent = new Intent(Intent.ACTION_SEND);

            shareIntent.setType(mimeType);
            shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
            shareIntent.addFlags(
                    Intent.FLAG_GRANT_READ_URI_PERMISSION
            );

            startActivity(
                    Intent.createChooser(
                            shareIntent,
                            getString(chooserTitle)
                    )
            );

            ClipboardManager clipboard =
                    getSystemService(ClipboardManager.class);

            if (clipboard != null
                    && clipboard.hasPrimaryClip()) {
                clipboard.clearPrimaryClip();
            }

        } catch (Exception e) {
            if (BuildConfig.DEBUG) {
                e.printStackTrace();
            }

            toast(R.string.share_failed, Toast.LENGTH_SHORT);
        }
    }

    private void toast(int stringResource, int duration) {
        Toast.makeText(
                this,
                stringResource,
                duration
        ).show();
    }

    private String safeFilename(String value) {
        if (value == null || value.trim().isEmpty()) {
            return "lemenzo_note";
        }

        String result = value
                .trim()
                .replaceAll("[^a-zA-Z0-9._-]", "_")
                .toLowerCase();

        if (result.isEmpty()) {
            return "lemenzo_note";
        }

        if (result.length() > 80) {
            return result.substring(0, 80);
        }

        return result;
    }

    private void showSecureLineDialog(AlertDialog dialog) {
        if (dialog == null) {
            return;
        }

        dialog.show();

        if (dialog.getWindow() == null) {
            return;
        }

        dialog.getWindow().setBackgroundDrawable(
                new ColorDrawable(Color.TRANSPARENT)
        );

        dialog.getWindow().setLayout(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
        );

        dialog.getWindow().getDecorView().setPadding(
                dp(18),
                0,
                dp(18),
                0
        );
    }

    private int dp(int value) {
        return Math.round(
                value * getResources()
                        .getDisplayMetrics()
                        .density
        );
    }
}
