package org.commcare.views;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import androidx.annotation.IdRes;
import android.text.method.LinkMovementMethod;
import android.view.Display;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;

import org.commcare.activities.CommCareGraphActivity;
import org.commcare.cases.entity.Entity;
import org.commcare.core.graph.model.GraphData;
import org.commcare.core.graph.util.GraphException;
import org.commcare.dalvik.R;
import org.commcare.graph.view.GraphLoader;
import org.commcare.graph.view.GraphView;
import org.commcare.preferences.HiddenPreferences;
import org.commcare.suite.model.CalloutData;
import org.commcare.suite.model.Detail;
import org.commcare.util.LogTypes;
import org.commcare.utils.DetailCalloutListener;
import org.commcare.utils.FileUtil;
import org.commcare.utils.GeoUtils;
import org.commcare.utils.MarkupUtil;
import org.commcare.utils.MediaUtil;
import org.commcare.views.media.AudioPlaybackButton;
import org.commcare.views.media.ViewId;
import org.javarosa.core.reference.InvalidReferenceException;
import org.javarosa.core.reference.ReferenceManager;
import org.javarosa.core.services.Logger;
import org.javarosa.core.services.locale.Localization;

import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;
import java.util.Timer;

/**
 * @author ctsims
 */
public class EntityDetailView extends FrameLayout {

    private final TextView label;
    private final TextView data;
    private final TextView spacer;
    private final Button callout;
    private final View addressView;
    private final Button addressButton;
    private final TextView addressText;
    private final ImageView imageView;
    private final View calloutView;
    private final Button calloutButton;
    private final TextView calloutText;
    private final ImageButton calloutImageButton;
    private final AspectRatioLayout graphLayout;
    private final Hashtable<Integer, Hashtable<Integer, View>> graphViewsCache;    // index => { orientation => GraphView }
    private final Hashtable<Integer, Intent> graphIntentsCache;    // index => intent
    private final Set<Integer> graphsWithErrors;
    private final ImageButton videoButton;
    private final AudioPlaybackButton audioButton;
    private final View valuePane;
    private View currentView;
    private final LinearLayout detailRow;
    private final LinearLayout.LayoutParams origValue;
    private final LinearLayout.LayoutParams origLabel;
    private final LinearLayout.LayoutParams fill;

    // Potential "forms" of a detail field
    private static final String FORM_VIDEO = MediaUtil.FORM_VIDEO;
    private static final String FORM_AUDIO = MediaUtil.FORM_AUDIO;
    private static final String FORM_PHONE = "phone";
    private static final String FORM_ADDRESS = "address";
    private static final String FORM_IMAGE = MediaUtil.FORM_IMAGE;
    private static final String FORM_GRAPH = "graph";
    private static final String FORM_CALLOUT = "callout";
    private static final String FORM_MARKDOWN = "markdown";

    @IdRes
    private static final int IMAGE_VIEW_ID = 23422634;

    @IdRes
    private static final int CALLOUT_BUTTON_ID = 23422634;

    private static final int TEXT = 0;
    private static final int PHONE = 1;
    private static final int ADDRESS = 2;
    private static final int IMAGE = 3;
    private static final int VIDEO = 4;
    private static final int AUDIO = 5;
    private static final int GRAPH = 6;
    private static final int CALLOUT = 7;
    private static final int MARKDOWN = 8;

    private int current = TEXT;

    private DetailCalloutListener listener;

    public EntityDetailView(Context context, Detail d, Entity e,
                            int index, int detailNumber) {
        super(context);

        detailRow = (LinearLayout)View.inflate(context, R.layout.component_entity_detail_item, null);
        label = detailRow.findViewById(R.id.detail_type_text);
        spacer = detailRow.findViewById(R.id.entity_detail_spacer);
        data = detailRow.findViewById(R.id.detail_value_text);
        currentView = data;
        valuePane = detailRow.findViewById(R.id.detail_value_pane);
        videoButton = detailRow.findViewById(R.id.detail_video_button);

        ViewId uniqueId = ViewId.buildTableViewId(detailNumber, index, true);
        String audioText = e.getFieldString(index);
        audioButton = new AudioPlaybackButton(context, audioText, uniqueId, false);
        detailRow.addView(audioButton);
        audioButton.setVisibility(View.GONE);

        callout = detailRow.findViewById(R.id.detail_value_phone);
        addressView = detailRow.findViewById(R.id.detail_address_view);
        addressText = addressView.findViewById(R.id.detail_address_text);
        addressButton = addressView.findViewById(R.id.detail_address_button);

        imageView = detailRow.findViewById(R.id.detail_value_image);
        int height;
        if (HiddenPreferences.isSmartInflationEnabled()) {
            // If using smart inflation, we don't want to do any other artificial resizing of images
            height = LayoutParams.WRAP_CONTENT;
        } else {
            // otherwise, should let the image view stretch to fill the height of the row
            height = LayoutParams.MATCH_PARENT;
        }
        FrameLayout.LayoutParams imageViewParams =
                new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, height);
        imageView.setLayoutParams(imageViewParams);

        graphLayout = detailRow.findViewById(R.id.graph);
        calloutView = detailRow.findViewById(R.id.callout_view);
        calloutText = detailRow.findViewById(R.id.callout_text);
        calloutButton = detailRow.findViewById(R.id.callout_button);
        calloutImageButton = detailRow.findViewById(R.id.callout_image_button);
        graphViewsCache = new Hashtable<>();
        graphsWithErrors = new HashSet<>();
        graphIntentsCache = new Hashtable<>();
        origLabel = (LinearLayout.LayoutParams)label.getLayoutParams();
        origValue = (LinearLayout.LayoutParams)valuePane.getLayoutParams();

        fill = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        this.addView(detailRow, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
        setParams(d, e, index, detailNumber);
    }

    public void setCallListener(final DetailCalloutListener listener) {
        this.listener = listener;
    }

    public void setParams(Detail d, Entity e, int index, int detailNumber) {
        String labelText = d.getFields()[index].getHeader().evaluate();
        label.setText(labelText);
        spacer.setText(labelText);

        Object field = e.getField(index);
        String textField = e.getFieldString(index);
        boolean veryLong = false;
        String form = d.getTemplateForms()[index];
        if (FORM_PHONE.equals(form)) {
            setupPhoneNumber(textField);
        } else if (FORM_CALLOUT.equals(form) && (field instanceof CalloutData)) {
            veryLong = setupCallout((CalloutData)field);
        } else if (FORM_ADDRESS.equals(form)) {
            setupAddress(textField);
        } else if (FORM_IMAGE.equals(form)) {
            veryLong = setupImage(textField);
        } else if (FORM_GRAPH.equals(form) && field instanceof GraphData) {
            // if graph parsing had errors, they'll be stored as a string
            setupGraph(index, labelText, field);
        } else if (FORM_AUDIO.equals(form)) {
            ViewId uniqueId = ViewId.buildTableViewId(detailNumber, index, true);
            audioButton.modifyButtonForNewView(uniqueId, textField, true);
            updateCurrentView(AUDIO, audioButton);
        } else if (FORM_VIDEO.equals(form)) { //TODO: Why is this given a special string?
            setupVideo(textField);
        } else if (FORM_MARKDOWN.equals(form)) {
            veryLong = setUpMarkdown(textField);
        } else {
            veryLong = setUpText(textField);
        }

        if (veryLong) {
            detailRow.setOrientation(LinearLayout.VERTICAL);
            spacer.setVisibility(View.GONE);
            label.setLayoutParams(fill);
            valuePane.setLayoutParams(fill);
        } else {
            if (detailRow.getOrientation() != LinearLayout.HORIZONTAL) {
                detailRow.setOrientation(LinearLayout.HORIZONTAL);
                spacer.setVisibility(View.INVISIBLE);
                label.setLayoutParams(origLabel);
                valuePane.setLayoutParams(origValue);
            }
        }
    }

    private boolean setUpText(String textField) {
        data.setText((textField));
        updateCurrentView(TEXT, data);
        return isTextVeryLong(textField);
    }

    private boolean isTextVeryLong(String textField) {
        return textField != null && textField.length() > this.getContext().getResources().getInteger(R.integer.detail_size_cutoff);
    }

    private boolean setUpMarkdown(String textField) {
        // Links in a listview are not clickable by default - https://stackoverflow.com/questions/1697908/android-how-can-i-add-html-links-inside-a-listview
        data.setMovementMethod(LinkMovementMethod.getInstance());
        data.setText((MarkupUtil.returnMarkdown(getContext(), textField)));
        updateCurrentView(MARKDOWN, data);
        return isTextVeryLong(textField);
    }

    private void setupPhoneNumber(String textField) {
        callout.setText(textField);
        if (current != PHONE) {
            callout.setOnClickListener(v -> listener.callRequested(callout.getText().toString()));
            this.removeView(currentView);
            updateCurrentView(PHONE, callout);
        }
    }

    private boolean setupCallout(final CalloutData callout) {
        boolean veryLong = false;

        String imagePath = callout.getImage();

        if (imagePath != null) {
            // use image as button, if available
            calloutButton.setVisibility(View.GONE);
            calloutText.setVisibility(View.GONE);

            Bitmap b = MediaUtil.inflateDisplayImage(getContext(), imagePath);

            if (b == null) {
                calloutImageButton.setImageDrawable(null);
            } else {
                // Figure out whether our image small or large.
                if (b.getWidth() > (getScreenWidth() / 2)) {
                    veryLong = true;
                }

                calloutImageButton.setPadding(10, 10, 10, 10);
                calloutImageButton.setAdjustViewBounds(true);
                calloutImageButton.setImageBitmap(b);
                calloutImageButton.setId(CALLOUT_BUTTON_ID);
            }

            calloutImageButton.setOnClickListener(v -> listener.performCallout(callout, CALLOUT));
        } else {
            calloutImageButton.setVisibility(View.GONE);
            calloutText.setVisibility(View.GONE);

            String displayName = callout.getDisplayName();
            // use display name if available, otherwise use URI
            if (displayName != null) {
                calloutButton.setText(displayName);
            } else {
                String actionName = callout.getActionName();
                calloutButton.setText(actionName);
            }

            calloutButton.setOnClickListener(v -> listener.performCallout(callout, CALLOUT));
        }

        updateCurrentView(CALLOUT, calloutView);
        return veryLong;
    }

    private void setupAddress(final String address) {
        addressText.setText(address);
        if (current != ADDRESS) {
            addressButton.setText(Localization.get("select.address.show"));
            addressButton.setOnClickListener(v -> listener.addressRequested(GeoUtils.getGeoIntentURI(address)));
            updateCurrentView(ADDRESS, addressView);
        }
    }

    private boolean setupImage(String textField) {
        boolean veryLong = false;
        Bitmap b = MediaUtil.inflateDisplayImage(getContext(), textField);

        if (b == null) {
            imageView.setImageDrawable(null);
        } else {
            //Ok, so. We should figure out whether our image is large or small.
            if (b.getWidth() > (getScreenWidth() / 2)) {
                veryLong = true;
            }

            imageView.setPadding(10, 10, 10, 10);
            imageView.setAdjustViewBounds(true);
            imageView.setImageBitmap(b);
            imageView.setId(IMAGE_VIEW_ID);
        }

        updateCurrentView(IMAGE, imageView);
        return veryLong;
    }

    private void setupGraph(int index, String labelText, Object field) {
        // Get graph view and intent
        int orientation = getResources().getConfiguration().orientation;
        boolean cached = true;
        View graphView = getGraphViewFromCache(index, orientation);
        if (graphView == null) {
            cached = false;
            graphView = getGraphView(index, labelText, (GraphData)field, orientation);
        }
        final Intent finalIntent = getGraphIntent(index, labelText, (GraphData)field);

        // Open full-screen graph intent on double tap
        if (!graphsWithErrors.contains(index)) {
            enableGraphIntent((WebView)graphView, finalIntent);
        }

        // Add graph child views to graph layout
        graphLayout.removeAllViews();
        graphLayout.addView(graphView, GraphView.getLayoutParams());
        if (!cached && !graphsWithErrors.contains(index)) {
            addSpinnerToGraph((WebView)graphView, graphLayout);
        }

        if (current != GRAPH) {
            // Hide field label and expand value to take up full screen width
            LinearLayout.LayoutParams graphValueLayout = new LinearLayout.LayoutParams((ViewGroup.LayoutParams)origValue);
            graphValueLayout.weight = 10;
            valuePane.setLayoutParams(graphValueLayout);

            label.setVisibility(View.GONE);
            data.setVisibility(View.GONE);
            updateCurrentView(GRAPH, graphLayout);
        }
    }

    private void setupVideo(String textField) {
        String localLocation = null;
        try {
            localLocation = ReferenceManager.instance().DeriveReference(textField).getLocalURI();
            if (localLocation.startsWith("/")) {
                //TODO: This should likely actually be happening with the getLocalURI _anyway_.
                localLocation = FileUtil.getGlobalStringUri(localLocation);
            }
        } catch (InvalidReferenceException ire) {
            Logger.log(LogTypes.TYPE_ERROR_CONFIG_STRUCTURE, "Couldn't understand video reference format: " + localLocation + ". Error: " + ire.getMessage());
        }

        final String location = localLocation;

        videoButton.setOnClickListener(v -> listener.playVideo(location));

        if (location == null) {
            videoButton.setEnabled(false);
            Logger.log(LogTypes.TYPE_ERROR_CONFIG_STRUCTURE, "No local video reference available for ref: " + textField);
        } else {
            videoButton.setEnabled(true);
        }

        updateCurrentView(VIDEO, videoButton);
    }

    private void updateCurrentView(int newCurrent, View newView) {
        if (newCurrent != current) {
            currentView.setVisibility(View.GONE);
            newView.setVisibility(View.VISIBLE);
            currentView = newView;
            current = newCurrent;
        }

        if (current != GRAPH) {
            label.setVisibility(View.VISIBLE);
            LinearLayout.LayoutParams graphValueLayout = new LinearLayout.LayoutParams((ViewGroup.LayoutParams)origValue);
            graphValueLayout.weight = 10;
            valuePane.setLayoutParams(origValue);
        }
    }

    private int getScreenWidth() {
        Display display = ((WindowManager)this.getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        return display.getWidth();
    }

    @SuppressWarnings("AddJavascriptInterface")
    private void addSpinnerToGraph(WebView graphView, ViewGroup graphLayout) {
        // WebView.addJavascriptInterface should not be called with minSdkVersion < 17
        // for security reasons: JavaScript can use reflection to manipulate application
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
            return;
        }

        final ProgressBar spinner = new ProgressBar(this.getContext(), null, android.R.attr.progressBarStyleLarge);
        spinner.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
        GraphLoader graphLoader = new GraphLoader((Activity)this.getContext(), spinner);

        // Set up interface that JavaScript will call to hide the spinner
        // once the graph has finished rendering.
        graphView.addJavascriptInterface(graphLoader, "Android");

        // The above JavaScript interface doesn't load properly 100% of the time.
        // Worst case, hide the spinner after ten seconds.
        Timer spinnerTimer = new Timer();
        spinnerTimer.schedule(graphLoader, 10000);
        graphLayout.addView(spinner);
    }

    private View getGraphViewFromCache(int index, int orientation) {
        if (graphViewsCache.get(index) != null) {
            return graphViewsCache.get(index).get(orientation);
        }
        graphViewsCache.put(index, new Hashtable<Integer, View>());
        return null;
    }

    /**
     * Generate graph view. May return WebView displaying graph, or TextView displaying error.
     */
    private View getGraphView(int index, String title, GraphData field, int orientation) {
        Context context = getContext();
        View graphView;
        GraphView g = new GraphView(context, title, false);
        try {
            String graphHTML = field.getGraphHTML(title);
            graphView = g.getView(graphHTML);
            graphLayout.setRatio((float)g.getRatio(field), (float)1);
        } catch (GraphException ex) {
            graphView = new TextView(context);
            int padding = (int)context.getResources().getDimension(R.dimen.spacer_small);
            graphView.setPadding(padding, padding, padding, padding);
            ((TextView)graphView).setText(ex.getMessage());
            graphsWithErrors.add(index);
        }
        graphViewsCache.get(index).put(orientation, graphView);
        return graphView;
    }

    /**
     * Fetch full-screen graph intent from cache, or create it.
     */
    private Intent getGraphIntent(int index, String title, GraphData field) {
        Intent graphIntent = graphIntentsCache.get(index);
        if (graphIntent == null && !graphsWithErrors.contains(index)) {
            GraphView g = new GraphView(this.getContext(), title, true);
            try {
                String html = field.getGraphHTML(title);
                graphIntent = g.getIntent(html, CommCareGraphActivity.class);
                graphIntentsCache.put(index, graphIntent);
            } catch (GraphException ex) {
                graphsWithErrors.add(index);
            }
        }

        return graphIntent;
    }

    /**
     * Set up event handling so that full-screen graph intent opens on double tap of given view.
     */
    private void enableGraphIntent(WebView graphView, final Intent graphIntent) {
        final Context context = this.getContext();
        final GestureDetector detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                return true;
            }

            @Override
            public boolean onDoubleTap(MotionEvent e) {
                context.startActivity(graphIntent);
                return true;
            }
        });
        graphView.setOnTouchListener((view, event) -> detector.onTouchEvent(event));
    }
}