/*
 * 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.android.documentsui.inspector;

import android.content.Context;
import android.content.res.Resources;
import android.location.Address;
import android.location.Geocoder;
import android.media.ExifInterface;
import android.media.MediaMetadata;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.DocumentsContract;
import androidx.annotation.VisibleForTesting;
import android.text.format.DateUtils;
import android.util.AttributeSet;

import com.android.documentsui.R;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.inspector.InspectorController.MediaDisplay;
import com.android.documentsui.inspector.InspectorController.TableDisplay;

import java.io.IOException;
import java.util.function.Consumer;

import javax.annotation.Nullable;

/**
 * Organizes and Displays the debug information about a file. This view
 * should only be made visible when build is debuggable and system policies
 * allow debug "stuff".
 */
public class MediaView extends TableView implements MediaDisplay {

    private final Resources mResources;
    private final Context mContext;

    public MediaView(Context context) {
        this(context, null);
    }

    public MediaView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MediaView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        mResources = context.getResources();
    }

    @Override
    public void accept(DocumentInfo doc, Bundle metadata, @Nullable Runnable geoClickListener) {
        putTitle("", true);

        Bundle exif = metadata.getBundle(DocumentsContract.METADATA_EXIF);
        if (exif != null) {
            showExifData(this, mResources, doc, exif, geoClickListener, this::getAddress);
        }

        Bundle video = metadata.getBundle(Shared.METADATA_KEY_VIDEO);
        if (video != null) {
            showVideoData(this, mResources, doc, video, geoClickListener);
        }

        Bundle audio = metadata.getBundle(Shared.METADATA_KEY_AUDIO);
        if (audio != null) {
            showAudioData(this, audio);
        }

        setVisible(!isEmpty());
    }

    @VisibleForTesting
    public static void showAudioData(TableDisplay table, Bundle tags) {

        if (tags.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) {
            table.put(R.string.metadata_artist, tags.getString(MediaMetadata.METADATA_KEY_ARTIST));
        }

        if (tags.containsKey(MediaMetadata.METADATA_KEY_COMPOSER)) {
            table.put(R.string.metadata_composer,
                    tags.getString(MediaMetadata.METADATA_KEY_COMPOSER));
        }

        if (tags.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) {
            table.put(R.string.metadata_album, tags.getString(MediaMetadata.METADATA_KEY_ALBUM));
        }

        if (tags.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
            int millis = tags.getInt(MediaMetadata.METADATA_KEY_DURATION);
            table.put(R.string.metadata_duration, DateUtils.formatElapsedTime(millis / 1000));
        }
    }

    @VisibleForTesting
    public static void showVideoData(
            TableDisplay table,
            Resources resources,
            DocumentInfo doc,
            Bundle tags,
            @Nullable Runnable geoClickListener) {

        addDimensionsRow(table, resources, tags);

        if (MetadataUtils.hasVideoCoordinates(tags)) {
            float[] coords = MetadataUtils.getVideoCoords(tags);
            showCoordiantes(table, resources, coords, geoClickListener);
        }

        if (tags.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
            int millis = tags.getInt(MediaMetadata.METADATA_KEY_DURATION);
            table.put(R.string.metadata_duration, DateUtils.formatElapsedTime(millis / 1000));
        }
    }

    @VisibleForTesting
    public static void showExifData(
            TableDisplay table,
            Resources resources,
            DocumentInfo doc,
            Bundle tags,
            @Nullable Runnable geoClickListener,
            Consumer<float[]> geoAddressFetcher) {

        addDimensionsRow(table, resources, tags);

        if (tags.containsKey(ExifInterface.TAG_DATETIME)) {
            String date = tags.getString(ExifInterface.TAG_DATETIME);
            table.put(R.string.metadata_date_time, date);
        }

        if (tags.containsKey(ExifInterface.TAG_GPS_ALTITUDE)) {
            double altitude = tags.getDouble(ExifInterface.TAG_GPS_ALTITUDE);
            table.put(R.string.metadata_altitude, String.valueOf(altitude));
        }

        if (tags.containsKey(ExifInterface.TAG_MAKE) || tags.containsKey(ExifInterface.TAG_MODEL)) {
                String make = tags.getString(ExifInterface.TAG_MAKE);
                String model = tags.getString(ExifInterface.TAG_MODEL);
                make = make != null ? make : "";
                model = model != null ? model : "";
                table.put(
                        R.string.metadata_camera,
                        resources.getString(R.string.metadata_camera_format, make, model));
        }

        if (tags.containsKey(ExifInterface.TAG_APERTURE)) {
            table.put(R.string.metadata_aperture, resources.getString(
                    R.string.metadata_aperture_format, tags.getDouble(ExifInterface.TAG_APERTURE)));
        }

        if (tags.containsKey(ExifInterface.TAG_SHUTTER_SPEED_VALUE)) {
            String shutterSpeed = String.valueOf(
                    formatShutterSpeed(tags.getDouble(ExifInterface.TAG_SHUTTER_SPEED_VALUE)));
            table.put(R.string.metadata_shutter_speed, shutterSpeed);
        }

        if (tags.containsKey(ExifInterface.TAG_FOCAL_LENGTH)) {
            double length = tags.getDouble(ExifInterface.TAG_FOCAL_LENGTH);
            table.put(R.string.metadata_focal_length,
                    String.format(resources.getString(R.string.metadata_focal_format), length));
        }

        if (tags.containsKey(ExifInterface.TAG_ISO_SPEED_RATINGS)) {
            int iso = tags.getInt(ExifInterface.TAG_ISO_SPEED_RATINGS);
            table.put(R.string.metadata_iso_speed_ratings,
                    String.format(resources.getString(R.string.metadata_iso_format), iso));
        }

        if (MetadataUtils.hasExifGpsFields(tags)) {
            float[] coords = MetadataUtils.getExifGpsCoords(tags);
            showCoordiantes(table, resources, coords, geoClickListener);
            geoAddressFetcher.accept(coords);
        }
    }

    private static void showCoordiantes(
            TableDisplay table,
            Resources resources,
            float[] coords,
            @Nullable Runnable geoClickListener) {

        String value = resources.getString(
                R.string.metadata_coordinates_format, coords[0], coords[1]);
        if (geoClickListener != null) {
            table.put(
                    R.string.metadata_coordinates,
                    value,
                    view -> {
                        geoClickListener.run();
                    }
            );
        } else {
            table.put(R.string.metadata_coordinates, value);
        }
    }

    /**
     * Attempts to retrieve an approximate address and displays the address if it can find one.
     * @param coords the coordinates that gets an address.
     */
    private void getAddress(float[] coords) {
        new AsyncTask<Float, Void, Address>() {
            @Override
            protected Address doInBackground(Float... coords) {
                assert (coords.length == 2);
                Geocoder geocoder = new Geocoder(mContext);
                try {
                    Address address = geocoder.getFromLocation(coords[0], // latitude
                            coords[1], // longitude
                            1 // amount of results returned
                    ).get(0);
                    return address;
                } catch (IOException e) {
                    return null;
                }
            }
            @Override
            protected void onPostExecute(@Nullable Address address) {
                if (address != null) {
                    TableDisplay table = MediaView.this;
                    if (address.getMaxAddressLineIndex() >= 0) {
                        String formattedAddress;
                        StringBuilder addressBuilder = new StringBuilder("");
                        addressBuilder.append(address.getAddressLine(0));
                        for (int i = 1; i <= address.getMaxAddressLineIndex(); i++) {
                            addressBuilder.append("\n");
                            String addressLine = address.getAddressLine(i);
                            if (addressLine != null) {
                                addressBuilder.append(addressLine);
                            }
                        }
                        formattedAddress = addressBuilder.toString();
                        table.put(R.string.metadata_address, formattedAddress);
                    } else if (address.getLocality() != null) {
                        table.put(R.string.metadata_address, address.getLocality());
                    } else if (address.getSubAdminArea() != null) {
                        table.put(R.string.metadata_address, address.getSubAdminArea());
                    } else if (address.getAdminArea() != null) {
                        table.put(R.string.metadata_address, address.getAdminArea());
                    } else if (address.getCountryName() != null) {
                        table.put(R.string.metadata_address, address.getCountryName());
                    }                }
            }
        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, coords[0], coords[1]);
    }

    /**
     * @param speed a value n, where shutter speed equals 1/(2^n)
     * @return a String containing either a fraction that displays 1 over a positive integer, or a
     * double rounded to one decimal, depending on if 1/(2^n) is less than or greater than 1,
     * respectively.
     */
    private static String formatShutterSpeed(double speed) {
        if (speed <= 0) {
            double shutterSpeed = Math.pow(2, -1 * speed);
            String formattedSpeed = String.valueOf(Math.round(shutterSpeed * 10.0) / 10.0);
            return formattedSpeed;
        } else {
            int approximateSpeedDenom = (int) Math.pow(2, speed) + 1;
            String formattedSpeed = "1/" + String.valueOf(approximateSpeedDenom);
            return formattedSpeed;
        }
    }

    /**
     * @param table
     * @param resources
     * @param tags
     */
    private static void addDimensionsRow(TableDisplay table, Resources resources, Bundle tags) {
        if (tags.containsKey(ExifInterface.TAG_IMAGE_WIDTH)
            && tags.containsKey(ExifInterface.TAG_IMAGE_LENGTH)) {
            int width = tags.getInt(ExifInterface.TAG_IMAGE_WIDTH);
            int height = tags.getInt(ExifInterface.TAG_IMAGE_LENGTH);
            float megaPixels = height * width / 1000000f;
            table.put(R.string.metadata_dimensions,
                    resources.getString(
                            R.string.metadata_dimensions_format, width, height, megaPixels));
        }
    }
}
