/*
 * MoasdaWiki App
 * Copyright (C) 2008 - 2022 Herbert Reiter (herbert@moasdawiki.net)
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3 as published
 * by the Free Software Foundation (GPL-3.0-only).
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
 */

package net.moasdawiki.app;

import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.webkit.CookieManager;
import android.webkit.WebBackForwardList;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.menu.MenuBuilder;
import androidx.preference.PreferenceManager;

import net.moasdawiki.base.ServiceException;
import net.moasdawiki.base.Settings;
import net.moasdawiki.server.HttpRequest;
import net.moasdawiki.server.RequestDispatcher;
import net.moasdawiki.service.HttpResponse;
import net.moasdawiki.service.repository.RepositoryService;
import net.moasdawiki.util.EscapeUtils;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Displays the main window with the embedded wiki browser.
 */
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static final String SERVER_BASE_URL = "http://localhost:1/";

    private RepositoryService repositoryService;
    private Settings settings;
    private SynchronizeWikiClient synchronizeWikiClient;
    private RequestDispatcher requestDispatcher;

    private WebView webview;

    private ExecutorService synchronizationExecutorService;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);

        WikiEngineApplication app = (WikiEngineApplication) getApplication();
        repositoryService = app.getRepositoryService();
        settings = app.getSettings();
        synchronizeWikiClient = app.getSynchronizeWikiClient();
        requestDispatcher = app.getRequestDispatcher();
        synchronizationExecutorService = Executors.newSingleThreadExecutor();

        initWebView();

        EditText searchInput = findViewById(R.id.search_input);
        searchInput.setOnEditorActionListener((TextView v, int actionId, KeyEvent event) -> {
            // event==null for on screen keyboard
            // filter for key up from physical keyboard
            if (event == null || event.getAction() == KeyEvent.ACTION_DOWN) {
                onSearch(null);
                return true;
            } else {
                return false;
            }
        });
    }

    /**
     * Configure the embedded web browser.
     */
    @SuppressLint("SetJavaScriptEnabled")
    private void initWebView() {
        CookieManager cookieManager = CookieManager.getInstance();
        cookieManager.setAcceptCookie(false);

        webview = findViewById(R.id.web_browser);
        webview.setWebChromeClient(new WebChromeClient());
        webview.setWebViewClient(new WebViewClient() {
            /**
             * Dispatch URLs the user clicks on.
             * External URLs will be shown in an external browser.
             */
            @Override
            public boolean shouldOverrideUrlLoading(@NotNull WebView view, @NotNull WebResourceRequest webResourceRequest) {
                Uri uri = webResourceRequest.getUrl();
                String host = uri.getHost();
                if ("localhost".equals(host)) {
                    // wiki URL, show in embedded browser
                    return false;
                } else {
                    // external URL, show in external browser
                    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                    startActivity(intent);
                    return true;
                }
            }

            /**
             * Resolve internal URLs without running a real HTTP server,
             * so we don't have to open a port.
             */
            @Override
            @Nullable
            public WebResourceResponse shouldInterceptRequest (@NotNull WebView view, @NotNull WebResourceRequest request) {
                try {
                    // determine URL path
                    Uri uri = request.getUrl();
                    String encodedUrl = uri.toString();
                    String url = URLDecoder.decode(encodedUrl, "UTF-8");
                    if (!url.startsWith(SERVER_BASE_URL)) {
                        return null;
                    }
                    String urlPath = url.substring(SERVER_BASE_URL.length() - 1); // extract URL path
                    int hashPos = urlPath.indexOf('#');
                    if (hashPos >= 0) {
                        // cut off anchor beginning with '#'
                        urlPath = urlPath.substring(0, hashPos);
                    }

                    // dispatch URL path
                    HttpRequest httpRequest = new HttpRequest(Collections.emptyMap(),
                            "GET", urlPath, urlPath, convertParameters(uri), new byte[0]);
                    HttpResponse response = requestDispatcher.handleRequest(httpRequest);

                    // send wiki content to browser
                    InputStream responseData = new ByteArrayInputStream(response.content);
                    return new WebResourceResponse(response.contentType, "UTF-8", responseData);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                return null;  // load HTTP data
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                hideProgressBar();
            }
        });

        WebSettings webSettings = webview.getSettings();
        webSettings.setAllowFileAccess(false);
        webSettings.setJavaScriptEnabled(true);
    }

    @NotNull
    private Map<String, String> convertParameters(@NotNull Uri uri) {
        Map<String, String> result = new HashMap<>();
        for (String name : uri.getQueryParameterNames()) {
            String value = uri.getQueryParameter(name);
            result.put(name, value);
        }
        return result;
    }

    @SuppressLint("RestrictedApi")
    @Override
    public boolean onCreateOptionsMenu(@NotNull Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_layout, menu);
        if(menu instanceof MenuBuilder){
            ((MenuBuilder) menu).setOptionalIconsVisible(true);
        }
        return true;
    }

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

    @Override
    public boolean onOptionsItemSelected(@NotNull MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.

        int itemId = item.getItemId();
        if (itemId == R.id.action_synchronize) {
            synchronizeWithServer();
            return true;
        } else if (itemId == R.id.action_startpage) {
            loadUrl(SERVER_BASE_URL);
            return true;
        } else if (itemId == R.id.action_settings) {
            showSettingsDialog();
            return true;
        } else if (itemId == R.id.action_help) {
            String pagePathHelp = getWikiserverHelpUrl();
            loadUrl(pagePathHelp);
            return true;
        } else if (itemId == R.id.action_about) {
            showAboutDialog();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        Log.d(TAG, "Permission granted by user: requestCode=" + requestCode
            + ", permissions=" + Arrays.toString(permissions) + ", grantResults=" + Arrays.toString(grantResults));
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    @Override
    public void onBackPressed() {
        // webview.canGoBack() always returns false
        WebBackForwardList backForwardList = webview.copyBackForwardList();
        Log.d(TAG, "Back button pressed, backForwardList index == " + backForwardList.getCurrentIndex());
        if (backForwardList.getCurrentIndex() > 0) {
            webview.goBack();
        } else {
            // Close app
            finish();
        }
    }

    public void onConfigurationHintClicked(View view) {
        showSettingsDialog();
    }

    public void onSynchronizeHintClicked(View view) {
        synchronizeWithServer();
    }

    public void onSearch(View view) {
        EditText searchInput = findViewById(R.id.search_input);
        String query = searchInput.getText().toString();
        query = query.trim();
        if (query.isEmpty()) {
            return;
        }

        // Remove focus + on screen keyboard from input field
        searchInput.clearFocus();
        InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        if (manager != null) {
            manager.hideSoftInputFromWindow(searchInput.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
        }

        Log.i(TAG, "Full text search for: " + query);
        showProgressBarAnimation();
        String url = getWikiserverSearchUrl(query);
        loadUrl(url);

        // The wait dialog is closed by closeProgressDialog() after the search result is shown
    }

    private void showSettingsDialog() {
        Intent intent = new Intent(this, SettingsActivity.class);
        startActivity(intent);
    }

    private void showAboutDialog() {
        // insert version number
        @SuppressLint("InflateParams")
        View dialogView = getLayoutInflater().inflate(R.layout.about_layout, null);
        TextView versionText = dialogView.findViewById(R.id.label_version);
        versionText.setText(getString(R.string.about_version, settings.getVersion()));
        int year = Calendar.getInstance().get(Calendar.YEAR);
        TextView copyrightText = dialogView.findViewById(R.id.label_copyright);
        copyrightText.setText(getString(R.string.about_copyright, year));

        // show dialog
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setView(dialogView);
        builder.setTitle(R.string.app_name);
        builder.setIcon(R.mipmap.ic_cow);
        builder.setPositiveButton(R.string.button_close, (dialog, id) -> {});
        Dialog dialog = builder.create();
        dialog.show();
    }

    /**
     * Called if user presses the synchronization button.
     */
    private void synchronizeWithServer() {
        synchronizationExecutorService.submit(this::runSynchronizationWithServer);
    }

    /**
     * Run synchronization of wiki content with wiki server.
     * This method is run in an asynchronous background thread.
     */
    private void runSynchronizationWithServer() {
        int filesCount;
        try {
            filesCount = synchronizeWikiClient.synchronizeRepository(this::syncProgress);
        } catch (ServiceException e) {
            Log.e(TAG, "Error synchronizing repository with server", e);
            runOnUiThread(() -> showToast(getString(R.string.settings_synchronize_failed)));
            return;
        }

        if (filesCount > 0) {
            runOnUiThread(() -> showToast(getString(R.string.settings_synchronize_successful, filesCount)));

            WikiEngineApplication app = (WikiEngineApplication) getApplication();
            app.resetServices();

            CalendarSyncAdapter.requestCalendarSync(this);
        } else {
            runOnUiThread(() -> showToast(getString(R.string.settings_synchronize_not_necessary)));
        }

        runOnUiThread(this::hideProgressBar);
        runOnUiThread(this::updateLayoutVisibility);
    }

    private void syncProgress(int progress, int max) {
        runOnUiThread(() -> showProgressBar(progress, max));
    }

    /**
     * Open a URL in the embedded browser.
     */
    private void loadUrl(@NotNull String url) {
        Log.d(TAG, "Open URL " + url);
        webview.loadUrl(url);
    }

    @NotNull
    private String getWikiserverSearchUrl(@NotNull String query) {
        return SERVER_BASE_URL + "search/?text=" + EscapeUtils.encodeUrlParameter(query);
    }

    @NotNull
    private String getWikiserverHelpUrl() {
        return SERVER_BASE_URL + "view/wiki/";
    }

    private void updateLayoutVisibility() {
        // Show/hide hint on unconfigured host name
        LinearLayout hostUnconfiguredHint = findViewById(R.id.hint_host_unconfigured);
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        String host = preferences.getString(Constants.PREFERENCES_SYNC_SERVER_HOST, null);
        boolean hostUnconfiguredHintVisible = (host == null || host.isEmpty());
        if (hostUnconfiguredHintVisible) {
            hostUnconfiguredHint.setVisibility(View.VISIBLE);
        } else {
            hostUnconfiguredHint.setVisibility(View.GONE);
        }

        // Show/hide hint on synchronization
        LinearLayout repositoryEmptyHint = findViewById(R.id.hint_repository_empty);
        boolean repositoryEmptyHintVisible = (!hostUnconfiguredHintVisible && repositoryService.getFiles().size() < 3);
        if (repositoryEmptyHintVisible) {
            repositoryEmptyHint.setVisibility(View.VISIBLE);
        } else {
            repositoryEmptyHint.setVisibility(View.GONE);
        }

        // Show/hide search bar and wiki view
        LinearLayout searchArea = findViewById(R.id.search_area);
        WebView webView = findViewById(R.id.web_browser);
        boolean webViewVisible = !hostUnconfiguredHintVisible && !repositoryEmptyHintVisible;
        if (webViewVisible) {
            searchArea.setVisibility(View.VISIBLE);
            boolean previousVisible = (webView.getVisibility() == View.VISIBLE);
            webView.setVisibility(View.VISIBLE);

            // if WebView is shown the first time, go to start page
            if (!previousVisible || webView.getUrl() == null) {
                loadUrl(SERVER_BASE_URL);
            }
        } else {
            searchArea.setVisibility(View.GONE);
            webView.setVisibility(View.GONE);
        }
    }

    private void showProgressBarAnimation() {
        ProgressBar progressBar = findViewById(R.id.search_progressbar);
        progressBar.setIndeterminate(true);
        progressBar.setVisibility(View.VISIBLE);
    }

    private void showProgressBar(int progress, int max) {
        ProgressBar progressBar = findViewById(R.id.search_progressbar);
        progressBar.setIndeterminate(false);
        progressBar.setMin(0);
        progressBar.setMax(max);
        progressBar.setProgress(progress);
        progressBar.setVisibility(View.VISIBLE);
    }

    private void hideProgressBar() {
        ProgressBar progressBar = findViewById(R.id.search_progressbar);
        progressBar.setVisibility(View.GONE);
    }

    private void showToast(String message) {
        Toast toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
        toast.show();
    }
}
